89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""
|
|
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
|