""" Walks a vault root and builds FileRecord objects. Two jobs: filter out junk (config.IGNORE_*), and make a best-guess starting date for each real file. That date is explicitly NOT trusted as correct — you said the metadata is often wrong, so this only has to be a reasonable starting point that you can freely overwrite, never a claim of truth. """ from datetime import datetime from pathlib import Path from typing import List from . import config from .model import FileRecord def _is_ignored_file(name: str) -> bool: if name in config.TOOL_STATE_FILENAMES: return True return any(p.match(name) for p in config.IGNORE_FILE_PATTERNS) def _is_inside_ignored_dir(rel_parts) -> bool: return any( part in config.IGNORE_DIR_NAMES or part.startswith(".") for part in rel_parts[:-1] # everything except the filename itself ) def _best_guess_date(stat) -> str: """True creation time isn't reliably available on ext4 via stat() — this is a heuristic (earliest of mtime/ctime, or real birthtime if the platform exposes it), not an authoritative answer.""" ts = getattr(stat, "st_birthtime", None) if ts is None: ts = min(stat.st_mtime, stat.st_ctime) return datetime.fromtimestamp(ts).strftime("%Y-%m-%d") def discover_folders(root: Path) -> List[str]: """Every real directory under root, as paths relative to root. This is the live, always-current list of possible destinations — it updates automatically as your folder structure changes, nothing hardcoded.""" folders = [] for p in root.rglob("*"): if p.is_dir() and p.name not in config.IGNORE_DIR_NAMES and not p.name.startswith("."): folders.append(str(p.relative_to(root))) return sorted(folders) def scan(root: Path) -> List[FileRecord]: """Scan root recursively and return one FileRecord per real, non-junk file.""" records = [] for p in root.rglob("*"): if p.is_dir(): continue rel_parts = p.relative_to(root).parts if _is_inside_ignored_dir(rel_parts): continue if _is_ignored_file(p.name): continue stat = p.stat() fs_date = _best_guess_date(stat) name_ok = bool(config.ALREADY_CONVENTIONED.match(p.name)) near = (not name_ok) and any( pat.match(p.name) for pat in config.NEAR_CONVENTION_PATTERNS ) rec = FileRecord( path=p, root=root, filename=p.name, ext=p.suffix, fs_date=fs_date, name_matches_convention=name_ok, near_miss=near, ) # Defaults: no change unless you edit something. Files that already # match the convention default to "keep name, move only" — see # FileRecord.keep_filename for why. rec.date_override = fs_date rec.dest_folder = rec.current_folder rec.keep_filename = name_ok records.append(rec) return records