fix(maintenance): stage re-extract under the artist dir so members link — #713 part 2 fix
The integration test caught it: _import_media re-derives the artist by path-walk from the attribution path (ignoring the explicit artist) AND _copy_to_library lands members relative to that path. Staging the archive in /tmp meant the artist didn't resolve (provenance skipped) and members would land in the temp dir. Stage under images_root/<slug>/<platform>/<post>/ instead so the artist resolves and members land in the real library; remove only the staged archive + sidecar afterward (members stay). Require a real artist+slug (skip + count otherwise). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -675,23 +675,29 @@ def cancel_audit_run(session: Session, *, audit_id: int) -> None:
|
||||
_ARCHIVE_EXT_FOR_FORMAT = {"zip": ".zip", "rar": ".rar", "7z": ".7z"}
|
||||
|
||||
|
||||
def _reextract_archive_to_post(importer, archive_path: Path, post, source_row, artist) -> list[int]:
|
||||
def _reextract_archive_to_post(
|
||||
importer, archive_path: Path, post, source_row, artist, images_root: Path,
|
||||
) -> list[int]:
|
||||
"""Extract one stored archive and link its members to `post`.
|
||||
|
||||
The stored attachment has no adjacent sidecar (it lives in the sha-addressed
|
||||
attachment store), so reconstruct the post's sidecar from the DB and re-run
|
||||
the normal import path: `attach_in_place` extracts the members and
|
||||
`_apply_sidecar` → `find_or_create_post` re-attaches them to the SAME Post
|
||||
(matched by source_id + external_post_id). Returns the new member image ids.
|
||||
attachment store). Stage a copy + a reconstructed sidecar UNDER the artist's
|
||||
library dir (`images_root/<slug>/<platform>/<post>/`) — the importer
|
||||
re-derives the artist from the path AND copies members relative to it, so the
|
||||
members land in the real library and resolve to the right artist — then re-run
|
||||
`attach_in_place`: the archive extracts and `find_or_create_post` re-attaches
|
||||
the members to the SAME Post (source_id + external_post_id). Removes only the
|
||||
staged archive + sidecar afterward; the imported member files stay. Returns
|
||||
the new member image ids.
|
||||
"""
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from .archive_extractor import detect_archive_format
|
||||
|
||||
fmt = detect_archive_format(archive_path)
|
||||
ext = _ARCHIVE_EXT_FOR_FORMAT.get(fmt or "", ".zip")
|
||||
platform = source_row.platform if source_row is not None else "imported"
|
||||
sidecar = {
|
||||
"category": source_row.platform if source_row is not None
|
||||
else (post.raw_metadata or {}).get("category"),
|
||||
@@ -701,12 +707,20 @@ def _reextract_archive_to_post(importer, archive_path: Path, post, source_row, a
|
||||
"published_at": post.post_date.isoformat() if post.post_date else None,
|
||||
"url": post.post_url,
|
||||
}
|
||||
with tempfile.TemporaryDirectory(prefix="fc_reextract_") as td:
|
||||
tmp = Path(td) / f"archive{ext}" # clean ext → is_archive + find_sidecar
|
||||
shutil.copy2(archive_path, tmp)
|
||||
tmp.with_suffix(".json").write_text(json.dumps(sidecar))
|
||||
res = importer.attach_in_place(tmp, artist=artist, source=source_row)
|
||||
work = images_root / artist.slug / platform / str(post.external_post_id)
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
staged = work / f"archive{ext}" # clean ext → is_archive + find_sidecar
|
||||
sidecar_path = staged.with_suffix(".json")
|
||||
try:
|
||||
shutil.copy2(archive_path, staged)
|
||||
sidecar_path.write_text(json.dumps(sidecar))
|
||||
res = importer.attach_in_place(staged, artist=artist, source=source_row)
|
||||
return list(res.member_image_ids or [])
|
||||
finally:
|
||||
# Drop only the staged archive + sidecar; the extracted member files
|
||||
# were copied into the library alongside them and must stay.
|
||||
staged.unlink(missing_ok=True)
|
||||
sidecar_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def reextract_archive_attachments(session: Session, *, images_root: Path) -> dict:
|
||||
@@ -727,7 +741,8 @@ def reextract_archive_attachments(session: Session, *, images_root: Path) -> dic
|
||||
|
||||
summary = {
|
||||
"scanned": 0, "archives": 0, "members_imported": 0,
|
||||
"posts_touched": 0, "skipped_no_post": 0, "errors": 0,
|
||||
"posts_touched": 0, "skipped_no_post": 0, "skipped_no_artist": 0,
|
||||
"errors": 0,
|
||||
}
|
||||
settings = ImportSettings.load_sync(session)
|
||||
importer = Importer(
|
||||
@@ -756,9 +771,19 @@ def reextract_archive_attachments(session: Session, *, images_root: Path) -> dic
|
||||
summary["skipped_no_post"] += 1
|
||||
continue
|
||||
artist = session.get(Artist, att.artist_id) if att.artist_id else None
|
||||
if artist is None and post.artist_id:
|
||||
artist = session.get(Artist, post.artist_id)
|
||||
if artist is None or not artist.slug:
|
||||
# The importer re-derives the artist from the staged path, so we need
|
||||
# a real artist+slug to anchor under. (Shouldn't happen for
|
||||
# subscription posts; skip rather than orphan the members.)
|
||||
summary["skipped_no_artist"] += 1
|
||||
continue
|
||||
source_row = session.get(Source, post.source_id) if post.source_id else None
|
||||
try:
|
||||
ids = _reextract_archive_to_post(importer, stored, post, source_row, artist)
|
||||
ids = _reextract_archive_to_post(
|
||||
importer, stored, post, source_row, artist, images_root,
|
||||
)
|
||||
session.commit()
|
||||
except Exception as exc: # one bad archive must not strand the rest
|
||||
session.rollback()
|
||||
|
||||
Reference in New Issue
Block a user