"""Best-effort archive member extraction (filesystem-import aid). zip/cbz via stdlib zipfile; rar via rarfile (needs an unrar/bsdtar binary); 7z via py7zr (pure-Python). NEVER raises: on any failure the context yields an empty list, and the caller still preserves the archive file itself as a PostAttachment, so nothing is lost. """ import logging import zipfile from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory log = logging.getLogger(__name__) ARCHIVE_EXTS = {".zip", ".cbz", ".rar", ".7z"} def is_archive(path: Path) -> bool: return Path(path).suffix.lower() in ARCHIVE_EXTS @contextmanager def extract_archive(path: Path): """Context manager yielding a list of (member_name, extracted_path) for every regular-file member. Temp dir is removed on exit. Members are only ever under the temp dir (libs' safe extractall + rglob from base). Fail-soft: any error → yields [].""" tmp = TemporaryDirectory(prefix="fc_arch_") base = Path(tmp.name) members: list[tuple[str, Path]] = [] try: try: ext = Path(path).suffix.lower() if ext in (".zip", ".cbz"): with zipfile.ZipFile(path) as zf: zf.extractall(base) elif ext == ".rar": import rarfile with rarfile.RarFile(path) as rf: rf.extractall(base) elif ext == ".7z": import py7zr with py7zr.SevenZipFile(path, "r") as zf: zf.extractall(base) for p in sorted(base.rglob("*")): if p.is_file(): members.append((str(p.relative_to(base)), p)) except Exception as exc: log.warning("archive extract failed for %s: %s", path, exc) members = [] yield members finally: tmp.cleanup()