fix: an unfinished download is never stored as an attachment, and the temp sweep keeps any file a library row points at
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 3s
CI and images / extension-test (push) Successful in 24s
CI and images / frontend-build (push) Successful in 26s
CI and images / backend-lint-and-test (push) Successful in 35s
CI and images / integration (push) Successful in 2m23s
CI and images / sign-extension (push) Successful in 2s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m59s
CI and images / smoke-web (push) Successful in 54s
CI and images / promote (push) Successful in 1s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped

The importer refuses a staging file (.part, .partial, .megatmp.*) at both
entry points, the same way it refuses a sidecar .json, and
discard_after_attach never deletes one: the age-gated temp sweep owns them.
The sweep checks its stale candidates against image and attachment rows and
keeps any a row points at.

A Google Drive fetch's half-written file had been stored as <sha>.part; the
sweep then deleted it by name and left the row with no file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 15:48:14 -05:00
co-authored by Claude Opus 5.5
parent 2fc71cfe4e
commit 315399c4e5
4 changed files with 122 additions and 27 deletions
+25
View File
@@ -410,6 +410,31 @@ def test_attach_in_place_invalid_image_returns_skipped(importer):
assert result.skip_reason.value == "invalid_image"
@pytest.mark.parametrize("name", ["film.mp4abc123_.part", "img.jpg.partial", ".megatmp.Xy12"])
def test_an_unfinished_download_is_refused_and_left_on_disk(importer, db_sync, name):
"""A staging file is a download still being written (or abandoned): it is
never stored as an attachment, and the importer never deletes it, so a
download in progress is not pulled out from under its writer."""
from backend.app.models import PostAttachment
artist = Artist(name="Hal", slug="hal")
db_sync.add(artist)
db_sync.flush()
staging = importer.images_root / "hal" / "patreon" / "post" / name
staging.parent.mkdir(parents=True, exist_ok=True)
staging.write_bytes(b"half of a video")
for result in (importer.attach_in_place(staging, artist=artist),
importer.import_one(staging)):
assert result.status == "skipped"
assert "unfinished download" in result.error
assert importer.discard_after_attach(staging, result) is False
assert staging.exists()
assert db_sync.execute(
select(func.count()).where(PostAttachment.original_filename == name)
).scalar() == 0
def test_attach_in_place_non_media_routes_to_attachment(importer, db_sync):
"""FC-2d-iii dispatch parity for the download path. A non-media file
(.pdf, .txt etc.) downloaded by gallery-dl must become a PostAttachment,