From 4f085f228aecf809681f184532a35e86fe627be4 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 13 Jul 2026 21:33:57 +0200 Subject: [PATCH] fix: commit cover DB pointer before mutating the old blob put_cover/delete_cover mutated storage before the DB pointer, so a transient DB failure could leave the row pointing at a deleted blob. Reorder to put->set-DB->delete-old and clear-DB->delete-blob, so a failure only orphans a blob, never dangles the pointer. Happy path covered by the 16 existing cover tests. Co-Authored-By: Claude Opus 4.8 --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- backend/src/api/mangas.rs | 22 +++++++++++++++------- frontend/package.json | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index a285ab0..8cf1648 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.128.15" +version = "0.128.16" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 231e9e9..c8efba6 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.128.15" +version = "0.128.16" edition = "2021" default-run = "mangalord" diff --git a/backend/src/api/mangas.rs b/backend/src/api/mangas.rs index df82ff0..d3f6b41 100644 --- a/backend/src/api/mangas.rs +++ b/backend/src/api/mangas.rs @@ -409,6 +409,17 @@ async fn put_cover( let old_key = repo::manga::get(&state.db, id).await?.cover_image_path; let new_key = format!("mangas/{}/cover.{}", id, img.ext); state.storage.put(&new_key, &img.bytes).await?; + // Cover keys are reused (mangas/{id}/cover.{ext}), so a same-extension + // replacement overwrites the blob at an existing key — drop any thumbnails + // cached for it so we don't serve a stale variant of the old cover. + crate::api::files::purge_thumbnails(state.storage.as_ref(), &new_key).await; + + // Commit the DB pointer to the new blob BEFORE removing the old one. If we + // deleted the old blob first and this write then failed, the row would point + // at a deleted blob (a cover that 404s) and the new blob would be orphaned. + // Ordering it first means a failure here leaves the row pointing at the + // still-present old blob; only a harmless orphan (new blob) can result. + repo::manga::set_cover_image_path(&state.db, id, &new_key, img.bytes.len() as i64).await?; if let Some(prev) = old_key.as_deref() { if prev != new_key { @@ -423,12 +434,6 @@ async fn put_cover( crate::api::files::purge_thumbnails(state.storage.as_ref(), prev).await; } } - // Cover keys are reused (mangas/{id}/cover.{ext}), so a same-extension - // replacement overwrites the blob at an existing key — drop any thumbnails - // cached for it so we don't serve a stale variant of the old cover. - crate::api::files::purge_thumbnails(state.storage.as_ref(), &new_key).await; - - repo::manga::set_cover_image_path(&state.db, id, &new_key, img.bytes.len() as i64).await?; Ok(Json(repo::manga::get_detail(&state.db, id).await?)) } @@ -446,12 +451,15 @@ async fn delete_cover( } require_can_edit(&state, id, user.id, admin_via_session(&session)).await?; if let Some(key) = repo::manga::get(&state.db, id).await?.cover_image_path { + // Clear the DB pointer BEFORE deleting the blob, so a storage-delete + // failure can't leave the row pointing at a removed blob. A failure + // after the clear only orphans the blob (harmless). + repo::manga::clear_cover_image_path(&state.db, id).await?; match state.storage.delete(&key).await { Ok(()) | Err(StorageError::NotFound) => {} Err(e) => return Err(e.into()), } crate::api::files::purge_thumbnails(state.storage.as_ref(), &key).await; - repo::manga::clear_cover_image_path(&state.db, id).await?; } Ok(Json(repo::manga::get_detail(&state.db, id).await?)) } diff --git a/frontend/package.json b/frontend/package.json index 3d2b40e..46afaed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.128.15", + "version": "0.128.16", "private": true, "type": "module", "scripts": {