"""Safe discovery of local source documents for the guided import workflow.""" import os import stat from dataclasses import dataclass from hashlib import sha256 from pathlib import Path from django.conf import settings from .parsers import MAX_BYTES, ParseError, parse_document @dataclass(frozen=True) class DiscoveredFile: name: str document_hash: str profile: str | None sections: tuple[str, ...] code: str | None _PROFILES_BY_SUFFIX = { ".csv": ("BANK_CSV_V1",), ".txt": ("OUROCARD_FULL_V1", "OUROCARD_PARTIAL_V1"), } def _profile_and_sections(path: Path, data: bytes) -> tuple[str | None, tuple[str, ...]]: for profile in _PROFILES_BY_SUFFIX.get(path.suffix.casefold(), ()): try: result = parse_document(data, profile) except ParseError: continue items = tuple(item for item in result.items if not item.errors) if items: return profile, tuple(dict.fromkeys(item.section for item in items)) return None, () def _unreadable(path: Path) -> DiscoveredFile: return DiscoveredFile(path.name, "", None, (), "UNREADABLE_FILE") def _read_discovered_file(path: Path, expected: os.stat_result) -> DiscoveredFile: descriptor = None try: flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) if no_follow := getattr(os, "O_NOFOLLOW", 0): flags |= no_follow descriptor = os.open(path, flags) opened = os.fstat(descriptor) if not stat.S_ISREG(opened.st_mode) or not os.path.samestat(expected, opened): return _unreadable(path) with os.fdopen(descriptor, "rb", closefd=False) as stream: data = stream.read(MAX_BYTES + 1) except OSError: return _unreadable(path) finally: if descriptor is not None: os.close(descriptor) if len(data) > MAX_BYTES: return DiscoveredFile(path.name, "", None, (), "INPUT_TOO_LARGE") document_hash = sha256(data).hexdigest() if path.suffix.casefold() not in _PROFILES_BY_SUFFIX: return DiscoveredFile(path.name, document_hash, None, (), "UNSUPPORTED_FILE") profile, sections = _profile_and_sections(path, data) if profile is None: return DiscoveredFile(path.name, document_hash, None, (), "UNRECOGNIZED_LAYOUT") return DiscoveredFile(path.name, document_hash, profile, sections, None) def discover_imports() -> tuple[DiscoveredFile, ...]: """Return safe metadata for direct regular files under ``IMPORT_ROOT``.""" try: root = Path(settings.IMPORT_ROOT).resolve() entries = tuple(root.iterdir()) except (OSError, RuntimeError): return () discovered = [] for path in entries: try: expected = path.lstat() if not stat.S_ISREG(expected.st_mode): continue except OSError: discovered.append(_unreadable(path)) continue discovered.append(_read_discovered_file(path, expected)) return tuple(sorted(discovered, key=lambda row: row.name))