fix(export): four ways the keepsake could be lost, stranded, or published empty
* The HTML completeness guard counted manifest ROWS, and there are up to two per upload — a thumbnail and a full variant. Thumbnails are 400px JPEGs the export generates itself into its own temp dir, so they are no evidence that any original was captured. After the guard was relaxed to bail only on "nothing written at all", that case could no longer fire while thumbnails kept succeeding: if the media volume became unreadable after the stat pass, every original open failed, every thumb open succeeded, and a keepsake with 100 thumbnails and ZERO full-resolution photos published green, done at the live epoch, with the download button lit. Boot recovery skips a done job, so nothing would ever have rebuilt it. Now counts photos, not files. * A decoder panic failed the ENTIRE keepsake. The `?` was on the JoinError, not on the closure's Result, so a panic in the image crate propagated out where the same file merely failing costs one tile — and it was deterministic, because "Neu erzeugen" reads the same poison file and dies the same way. That is the exact failure shape the completeness guard was relaxed to eliminate, arriving through the other door. * delete_account armed both export jobs and then spawned the workers AFTER an awaited file-removal loop. Axum drops a handler future on client disconnect, and every other invalidate_and_arm call site spawns with no intervening await. Dropped inside that loop, the keepsake is left with the epoch bumped, both rows pending at that epoch, and no worker: the downloads 404 and the UI sits on "Wird vorbereitet..." until someone reboots the app. Deleting your account from a phone that walks out of range is enough. * The daily download quota was charged before the ticket could fail, so a store-capacity 503 — a server-side condition the guest cannot see or cause — still cost one of their three downloads. There is no refund path.
This commit is contained in:
@@ -206,6 +206,17 @@ pub async fn delete_account(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
// IMMEDIATELY after the commit, before any other `.await`. Every other `invalidate_and_arm`
|
||||
// call site does this; this one used to spawn the workers *after* the file-removal loop below,
|
||||
// and axum drops a handler future the moment the client disconnects. Drop it inside that loop
|
||||
// and the keepsake is left with the epoch bumped, both `export_job` rows armed `pending` at
|
||||
// that epoch, and NO WORKER: `/export/zip` and `/export/html` 404, the UI sits on
|
||||
// "Wird vorbereitet…" forever, and `recover_exports` only runs at boot. Deleting your account
|
||||
// from a phone that walks out of wifi range is enough to do it.
|
||||
if let Some(r) = regen {
|
||||
crate::handlers::host::start_regen(&state, r);
|
||||
}
|
||||
|
||||
// Best effort, after the commit. Anything missed here is an orphan with no row pointing at it,
|
||||
// which `sweep_orphan_originals` reclaims on its next pass — so a failure delays reclamation
|
||||
// rather than leaving the file referenced.
|
||||
@@ -228,10 +239,6 @@ pub async fn delete_account(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(r) = regen {
|
||||
crate::handlers::host::start_regen(&state, r);
|
||||
}
|
||||
|
||||
// Evict their content from every open feed and the projector. `user-hidden` is exactly the
|
||||
// right signal — it already means "this user's cards must go" — and reusing it means every
|
||||
// client already handles this with no new event type.
|
||||
|
||||
Reference in New Issue
Block a user