feat(attachments): fail-soft archive_extractor (zip/cbz/rar/7z)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user