Fix code-review findings on multi-tenant branch

- images.rs: scope /api/img and /api/photos/{id}/original by album owner —
  close the cross-tenant original/thumbnail leak (tenancy test now covers
  these routes)
- migration 0003: refuse to run when albums exist and users != 1 instead of
  silently reassigning every album to the oldest user
- Gallery: callback-ref ResizeObserver so a gallery mounted empty still
  lays out once photos arrive (was permanently blank)
- upload dedup: re-uploading identical content whose photo is in 'error'
  resets and re-enqueues it instead of returning the broken row
- client hashing: skip (and fall back to plain upload) above 512MB to avoid
  whole-file arrayBuffer OOM / the ~2GiB cap
- zip: always spool each entry (no unread prefetched S3 body held across a
  slow client stream) and backfill BOTH sha256 and crc32 for legacy photos
- tests/auth: share one session_payload builder instead of re-implementing
  the cookie format in the test and mint_session
This commit is contained in:
2026-07-17 15:16:25 +02:00
parent 40f7f2fb5e
commit 84323ab637
9 changed files with 149 additions and 78 deletions
+21 -2
View File
@@ -41,6 +41,25 @@ fn sanitize_filename(raw: &str) -> Result<String, ApiError> {
Ok(cleaned.chars().take(150).collect())
}
/// A dedup hit on a photo that previously failed processing means the user is
/// re-uploading to fix it — reset it and re-enqueue instead of handing back a
/// broken row that the UI would report as "already uploaded".
async fn heal_if_errored(state: &AppState, photo: Photo) -> ApiResult<Photo> {
if photo.status != PhotoStatus::Error {
return Ok(photo);
}
let mut tx = state.db.begin().await?;
let healed: Photo =
sqlx::query_as("update photos set status = $2, error = null where id = $1 returning *")
.bind(photo.id)
.bind(PhotoStatus::Uploaded.as_str())
.fetch_one(&mut *tx)
.await?;
jobs::ensure_process_photo(&mut tx, photo.id).await?;
tx.commit().await?;
Ok(healed)
}
pub async fn upload(
State(state): State<AppState>,
user: AuthUser,
@@ -94,7 +113,7 @@ pub async fn upload(
.fetch_optional(&state.db)
.await?;
if let Some(existing) = existing {
return Ok(Json(existing));
return Ok(Json(heal_if_errored(&state, existing).await?));
}
// Upload to S3 first, then create the row and enqueue processing in one
@@ -153,7 +172,7 @@ pub async fn upload(
.fetch_optional(&state.db)
.await?;
if let Some(winner) = winner {
return Ok(Json(winner));
return Ok(Json(heal_if_errored(&state, winner).await?));
}
}
return Err(e.into());