Files
FabledCurator/backend/app/services/archive_extractor.py
T
2026-05-19 11:11:37 -04:00

58 lines
1.9 KiB
Python

"""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()