feat(provenance): capture which archive an extracted image came from (#87)
Images pulled out of a .zip/.rar previously kept no record of WHICH archive
they came from — the member->archive link was computed during extraction and
discarded, leaving only image->post. So the provenance modal could only scope
attachments to the whole post, showing every archive a 'High Resolution Files'
bundle carried instead of the one a given file lives in.
- ImageProvenance.from_attachment_id: nullable FK -> post_attachment.id
(SET NULL), migration 0055.
- importer: _import_archive stamps from_attachment_id on every member's
provenance row for the post (new + superseded + deduped members), resolving
the archive's own PostAttachment by (post, sha). Post-pass UPDATE, NULL-only
and idempotent, so it doesn't touch the dedup/supersede branches and the
backfill is safe to re-run. Nested members link to the outer stored archive.
- provenance_service.for_image: when the originating post's provenance row
records from_attachment_id, return ONLY that archive; else fall back to the
primary-post scoping from 068def2.
- ProvenancePanel: heading pluralizes ('Attachment' for a single file).
- Backfill: re-running reextract_archive_attachments (ArchiveReextractCard)
routes through _import_archive and stamps existing rows — no new code.
Tests: capture stamps on fresh import, nested-archive attribution, per-post
archive on dedup; for_image filters to the containing archive; reextract
backfill stamps the link.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,10 @@ class ProvenanceService:
|
||||
).scalars().all()
|
||||
return [_attachment_dict(a) for a in rows]
|
||||
|
||||
async def _attachment_by_id(self, attachment_id: int) -> list[dict]:
|
||||
att = await self.session.get(PostAttachment, attachment_id)
|
||||
return [_attachment_dict(att)] if att is not None else []
|
||||
|
||||
async def for_image(self, image_id: int) -> dict | None:
|
||||
rec = await self.session.get(ImageRecord, image_id)
|
||||
if rec is None:
|
||||
@@ -85,22 +89,33 @@ class ProvenanceService:
|
||||
)
|
||||
rows = (await self.session.execute(stmt)).all()
|
||||
post_ids = [ip.post_id for ip, _p, _s, _a in rows]
|
||||
# Scope attachments to the image's ORIGINATING post only, not every
|
||||
# post it's pHash-linked to. An image dupe gets a provenance row per
|
||||
# post it reappears in (enrich-on-duplicate), and one of those is
|
||||
# often a "High Resolution Files" mega-bundle carrying dozens of
|
||||
# unrelated archives — aggregating across all linked posts ballooned
|
||||
# the panel with files that have nothing to do with this image.
|
||||
# primary_post_id is the post this file was actually captured from;
|
||||
# fall back to all linked posts only when it's unset (older rows /
|
||||
# filesystem imports). NB: FC stores archives as opaque blobs and
|
||||
# never records which archive an extracted image came from, so we
|
||||
# cannot scope tighter than the post — see milestone for the
|
||||
# image->archive capture work.
|
||||
attach_post_ids = (
|
||||
[rec.primary_post_id] if rec.primary_post_id is not None else post_ids
|
||||
# Prefer the EXACT archive this file came out of (milestone #87): if the
|
||||
# originating post's provenance row records from_attachment_id, the image
|
||||
# was extracted from that one .zip/.rar, so show only it — not the dozens
|
||||
# of unrelated archives a "High Resolution Files" bundle post carries.
|
||||
from_att_id = next(
|
||||
(
|
||||
ip.from_attachment_id
|
||||
for ip, _p, _s, _a in rows
|
||||
if ip.post_id == rec.primary_post_id
|
||||
and ip.from_attachment_id is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
attachments = await self._attachments_for_posts(attach_post_ids)
|
||||
if from_att_id is not None:
|
||||
attachments = await self._attachment_by_id(from_att_id)
|
||||
else:
|
||||
# No recorded containing archive (loose download, or pre-backfill):
|
||||
# scope to the originating post only, not every pHash-linked post.
|
||||
# primary_post_id is the post this file was actually captured from;
|
||||
# fall back to all linked posts when it's unset (older rows /
|
||||
# filesystem imports).
|
||||
attach_post_ids = (
|
||||
[rec.primary_post_id]
|
||||
if rec.primary_post_id is not None
|
||||
else post_ids
|
||||
)
|
||||
attachments = await self._attachments_for_posts(attach_post_ids)
|
||||
return {
|
||||
"image_id": image_id,
|
||||
"provenance": [
|
||||
|
||||
Reference in New Issue
Block a user