fix(export): stop the keepsake guards from destroying the keepsake
Two guards added to protect the archive each had a failure mode worse than the one they prevented, and both were unrecoverable — which is what makes them worth reverting rather than tuning. The completeness gate refused to publish once skips passed max(2, 10% of expected). That refusal is DETERMINISTIC ACROSS RETRIES: the unreadable files are still unreadable when the host taps "Neu erzeugen", and the gallery is already released so the uploads cannot be collected again. On a 30-photo event, four bad files meant nobody ever got the other 26. That is precisely the "one-photo gap becomes total loss" outcome MAX_SKIPPED_FRACTION's own comment says it exists to avoid. Anything short of an empty archive now publishes and logs the counts at error level. `written == 0` stays fatal — a wrong MEDIA_PATH is a misconfiguration the host CAN fix and retry, and it once shipped a few-hundred-byte ZIP containing zero photos that passed every automated check. The space reclaim refused to prune unless it freed the entire shortfall, to protect an archive that no handler can serve: a download resolves through `export_current`, which requires `job.epoch = event.export_epoch`, and the epoch only increments. Meanwhile `reclaimable` is scoped to the caller's own prefix — one old archive — while `deficit` is sized for both halves plus the reserve. So on a tight disk each worker measured its own share as insufficient and neither pruned, though the two shares were jointly sufficient. Every "Neu erzeugen" reran the identical arithmetic and refused identically: permanently stuck, with dead archives nothing would reclaim and nothing could serve. It now prunes what it can and lets the re-check decide, so the sibling's prune lets the host's retry converge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -448,6 +448,12 @@ async fn sweep_orphan_originals(pool: &PgPool, media_path: &std::path::Path) {
|
||||
}
|
||||
// Too young to judge: an upload committing RIGHT NOW is indistinguishable from an
|
||||
// orphan, because the rename precedes the commit.
|
||||
//
|
||||
// This sweep is also the backstop for the one case the upload handler's drop guard
|
||||
// deliberately leaks: a client disconnect while `tx.commit()` is in flight disarms
|
||||
// the guard first (so a COMMIT that Postgres applied anyway keeps its file), which
|
||||
// means a COMMIT that did NOT apply leaves a final-named file with no row. The
|
||||
// `NOT EXISTS` check below is what reclaims it. See `upload.rs`, the disarm site.
|
||||
let recent = meta
|
||||
.modified()
|
||||
.ok()
|
||||
|
||||
Reference in New Issue
Block a user