5bb25245a5
Patreon attachment downloads land with sanitized URL-blob filenames
(01_https___www.patreon.com_media-u_v3_<id>) whose Path.suffix is junk, never
.zip — so the extension-only is_archive() filed them as opaque PostAttachments
and never extracted them ("No images attached to this post").
Add archive_extractor.detect_archive_format() — extension first, then magic-byte
sniff (zipfile.is_zipfile + RAR/7z signatures). is_archive(), extract_archive(),
and safe_probe._inspect_archive() (the bomb-guard) all route through it, so a
mis-named/extension-less archive is now detected, bomb-guarded, integrity-tested,
AND extracted regardless of filename. Stops new ones; part 2 re-extracts the
already-imported backlog.
Tests: mis-named zip detected + extracted; non-archive dotted name not
misdetected; _inspect_archive on a mis-named zip; signature updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from backend.app.services.archive_extractor import extract_archive, is_archive
|
|
|
|
|
|
def test_is_archive():
|
|
assert is_archive(Path("a.zip"))
|
|
assert is_archive(Path("a.CBZ"))
|
|
assert is_archive(Path("a.rar"))
|
|
assert is_archive(Path("a.7z"))
|
|
assert not is_archive(Path("a.jpg"))
|
|
|
|
|
|
def _zip(path, entries):
|
|
with zipfile.ZipFile(path, "w") as zf:
|
|
for name, data in entries.items():
|
|
zf.writestr(name, data)
|
|
|
|
|
|
def test_zip_members_enumerated(tmp_path):
|
|
z = tmp_path / "set.zip"
|
|
_zip(z, {"a.jpg": b"img", "sub/b.png": b"img2", "notes.txt": b"hi"})
|
|
with extract_archive(z) as members:
|
|
names = sorted(n for n, _ in members)
|
|
assert names == ["a.jpg", "notes.txt", "sub/b.png"]
|
|
for _, p in members:
|
|
assert p.is_file()
|
|
for _, p in members:
|
|
assert not p.exists() # temp cleaned on exit
|
|
|
|
|
|
def test_cbz_alias(tmp_path):
|
|
z = tmp_path / "set.cbz"
|
|
_zip(z, {"p1.jpg": b"i"})
|
|
with extract_archive(z) as members:
|
|
assert [n for n, _ in members] == ["p1.jpg"]
|
|
|
|
|
|
def test_misnamed_zip_detected_and_extracted(tmp_path):
|
|
"""A real zip whose filename has no usable extension (Patreon attachment
|
|
URL-blob name) is detected by magic bytes and its members extracted —
|
|
previously it was filed as an opaque attachment and never opened."""
|
|
z = tmp_path / "01_https___www.patreon.com_media-u_v3_131083093"
|
|
_zip(z, {"a.jpg": b"img", "b.png": b"img2"})
|
|
assert is_archive(z)
|
|
with extract_archive(z) as members:
|
|
assert sorted(n for n, _ in members) == ["a.jpg", "b.png"]
|
|
|
|
|
|
def test_non_archive_with_dotted_name_is_not_archive(tmp_path):
|
|
"""A non-archive file with a dotted/extension-less name isn't misdetected."""
|
|
f = tmp_path / "01_https___www.patreon.com_media-u_v3_999"
|
|
f.write_bytes(b"\x89PNG\r\n\x1a\n not really but not a zip")
|
|
assert not is_archive(f)
|
|
|
|
|
|
def test_corrupt_archive_yields_nothing(tmp_path):
|
|
bad = tmp_path / "broken.zip"
|
|
bad.write_bytes(b"not a zip at all")
|
|
with extract_archive(bad) as members:
|
|
assert members == []
|
|
|
|
|
|
def test_7z_roundtrip(tmp_path):
|
|
py7zr = pytest.importorskip("py7zr")
|
|
a = tmp_path / "s.7z"
|
|
(tmp_path / "x.jpg").write_bytes(b"img")
|
|
with py7zr.SevenZipFile(a, "w") as zf:
|
|
zf.write(tmp_path / "x.jpg", "x.jpg")
|
|
with extract_archive(a) as members:
|
|
assert [n for n, _ in members] == ["x.jpg"]
|
|
|
|
|
|
def test_rar_corrupt_is_failsoft(tmp_path):
|
|
pytest.importorskip("rarfile")
|
|
bad = tmp_path / "b.rar"
|
|
bad.write_bytes(b"Rar! not really")
|
|
with extract_archive(bad) as members:
|
|
assert members == [] # invalid / no unrar → fail-soft, nothing lost
|