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
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:
@@ -43,6 +43,7 @@ from ..utils.paths import (
|
||||
from ..utils.phash import compute_phash, find_similar, fingerprint_path, fingerprints_match
|
||||
from ..utils.sidecar import find_sidecar, parse_sidecar
|
||||
from ..utils.slug import slugify
|
||||
from ..utils.staging import is_temp_staging_file
|
||||
from .archive_extractor import extract_archive, is_archive
|
||||
from .attachment_store import AttachmentStore
|
||||
from .audits import single_color
|
||||
@@ -115,6 +116,21 @@ def is_video(path: Path) -> bool:
|
||||
return path.suffix.lower() in VIDEO_EXTS
|
||||
|
||||
|
||||
def _not_content(path: Path) -> ImportResult | None:
|
||||
"""The skip result for a file that is never library content: a sidecar
|
||||
`.json` (metadata) or a downloader's staging file (an unfinished download,
|
||||
which becomes content only under its final name). None for anything else."""
|
||||
if path.suffix.lower() == ".json":
|
||||
error = "sidecar json is metadata, not content"
|
||||
elif is_temp_staging_file(path):
|
||||
error = "unfinished download (staging file), not content"
|
||||
else:
|
||||
return None
|
||||
return ImportResult(
|
||||
status="skipped", skip_reason=SkipReason.invalid_image, error=error,
|
||||
)
|
||||
|
||||
|
||||
def _safe_ext(path: Path) -> str:
|
||||
"""Conservatively extract a file extension for PostAttachment.ext
|
||||
(varchar(32)). Thin wrapper over the shared `utils.paths.safe_ext` (kept for
|
||||
@@ -410,11 +426,8 @@ class Importer:
|
||||
media members (one Post via the archive-adjacent sidecar) and
|
||||
preserve the archive itself. Other non-media → PostAttachment.
|
||||
Nothing is skipped for being non-media (FC-2d-iii)."""
|
||||
if source.suffix.lower() == ".json":
|
||||
return ImportResult(
|
||||
status="skipped", skip_reason=SkipReason.invalid_image,
|
||||
error="sidecar json is metadata, not content",
|
||||
)
|
||||
if (refused := _not_content(source)) is not None:
|
||||
return refused
|
||||
if is_archive(source):
|
||||
return self._import_archive(source)
|
||||
if not is_supported(source):
|
||||
@@ -1219,11 +1232,8 @@ class Importer:
|
||||
caller may delete the on-disk original (mirrors duplicate_hash)
|
||||
- failed → file untouched; caller decides
|
||||
"""
|
||||
if path.suffix.lower() == ".json":
|
||||
return ImportResult(
|
||||
status="skipped", skip_reason=SkipReason.invalid_image,
|
||||
error="sidecar json is metadata, not content",
|
||||
)
|
||||
if (refused := _not_content(path)) is not None:
|
||||
return refused
|
||||
if is_archive(path):
|
||||
return self._import_archive(
|
||||
path, artist=artist, source_row=source,
|
||||
@@ -1420,7 +1430,11 @@ class Importer:
|
||||
|
||||
An imported or superseded media file is kept: it IS the record's file.
|
||||
So is any path an ImageRecord points at, whatever the status, because a
|
||||
re-fetch can write identical bytes over a record's own file."""
|
||||
re-fetch can write identical bytes over a record's own file. A staging
|
||||
file is kept too: it may be a download still being written, and the
|
||||
temp-file sweep removes it once it is old enough to be abandoned."""
|
||||
if is_temp_staging_file(path):
|
||||
return False
|
||||
if result.status in ("imported", "superseded"):
|
||||
redundant = is_archive(path)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user