From 19eb4e9388465dbecd9f74714c67fffbfef4ac7f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 6 Jun 2026 19:01:22 -0400 Subject: [PATCH] feat(import): surface WHY an archive was captured without extracting images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recurring "post shows a zip but no images" report had no diagnostic: when _import_archive captured an archive as a bare PostAttachment — because the bomb-guard probe rejected it, or extraction yielded zero members (corrupt / unsupported / missing extractor backend), or it held only non-media files — it returned status="attached" silently. Now those paths set ImportResult.error with the specific reason and log a warning, and download_service records each as {file, reason} under the event's metadata.unextracted_archives (None when every archive extracted cleanly). So the next run names exactly which archives failed and why, instead of leaving the operator to guess. No behaviour change to the happy path. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/services/download_service.py | 20 ++++++++++++++++++++ backend/app/services/importer.py | 20 ++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/backend/app/services/download_service.py b/backend/app/services/download_service.py index 66e960b..0fc04e6 100644 --- a/backend/app/services/download_service.py +++ b/backend/app/services/download_service.py @@ -269,6 +269,11 @@ class DownloadService: source_row = self.sync_session.get(Source, ctx["source_id"]) import_summary = {"attached": 0, "skipped": 0, "errors": 0} + # Archives detected but captured WITHOUT extracting any image (probe + # rejected / corrupt / missing extractor backend). Surfaced on the event + # so a post showing "no images" beside a zip is diagnosable (plan + # follow-up 2026-06-06 — the recurring archive-association report). + unextracted_archives: list[dict] = [] bytes_downloaded = 0 loop = asyncio.get_running_loop() @@ -322,6 +327,17 @@ class DownloadService: # mirror duplicate_hash cleanup so we don't keep two copies. # Operator-flagged 2026-06-02 (Lustria OST zip). import_summary["attached"] += 1 + # An archive captured WITHOUT extracting any image carries a + # reason on result.error — record it so the event explains the + # "no images beside a zip" symptom instead of staying silent. + if result.error: + unextracted_archives.append( + {"file": path.name, "reason": result.error} + ) + log.warning( + "archive captured unextracted (%s): %s", + path.name, result.error, + ) try: bytes_downloaded += path.stat().st_size # noqa: ASYNC240 except OSError: @@ -403,6 +419,10 @@ class DownloadService: "duration_seconds": dl_result.duration_seconds, "quarantined_paths": dl_result.quarantined_paths or None, "import_summary": import_summary, + # Archives detected but captured without extracting an image — the + # recurring "post shows a zip but no images" report. Each entry is + # {file, reason}; None when every archive extracted cleanly. + "unextracted_archives": unextracted_archives or None, } await self._update_source_health( source_id=ctx["source_id"], status=status, error_message=ev.error, diff --git a/backend/app/services/importer.py b/backend/app/services/importer.py index e1acc6e..c954bfe 100644 --- a/backend/app/services/importer.py +++ b/backend/app/services/importer.py @@ -430,13 +430,17 @@ class Importer: self._capture_attachment( source, post=post, artist=artist_use, resolved=True, ) - return ImportResult(status="attached") + reason = f"archive probe rejected, captured unextracted: {probe.reason}" + log.warning("%s: %s", source.name, reason) + return ImportResult(status="attached", error=reason) artist_use = artist if artist is not None else self._resolve_artist(source) post = self._post_for_sidecar(source, artist_use) member_ids: list[int] = [] + member_total = 0 with extract_archive(source) as members: for _name, member_path in members: + member_total += 1 if not is_supported(member_path): continue # non-media preserved via the stored archive res = self._import_media( @@ -453,7 +457,19 @@ class Importer: status="imported", image_id=member_ids[0], member_image_ids=member_ids, ) - return ImportResult(status="attached") + # No images landed — surface WHY so a post showing "no images" beside an + # archive is diagnosable instead of silent. Zero members usually means + # the extractor backend is missing/failed (unar for rar, py7zr for 7z) + # or the file is corrupt; non-zero-but-no-images means it held only + # non-media files. + reason = ( + "archive extracted but held no supported image/video members" + if member_total + else "archive yielded no members (unsupported/corrupt, or the " + "extractor backend failed)" + ) + log.warning("%s: %s", source.name, reason) + return ImportResult(status="attached", error=reason) def _import_media( self, source: Path, attribution_path: Path,