feat(by-hash): allow /by-hash even if blob is trashed

- permit reuse of blob where file is trashed (by-hash and chunked)
    - add hurl test on /api/files/by-hash
    - add anti enumeration of blob (404 is always blob_not_owned_by_caller)
This commit is contained in:
Edouard Vanbelle
2026-06-16 22:29:51 +02:00
parent 9519c1fee2
commit 68abb891d7
5 changed files with 350 additions and 13 deletions
+25 -1
View File
@@ -116,7 +116,31 @@ impl FileHandler {
.await
{
Ok(file) => Self::created_json_response(&file).into_response(),
Err(err) => Self::domain_error_response(err).into_response(),
Err(err) => {
// Anti-enumeration shape: every "caller cannot reach this
// hash" outcome collapses into the same 404 with an
// `upload_path` hint, regardless of whether the hash exists
// globally, is owned by another tenant, or got GC'd in a
// race against trash-empty. Hides the cross-tenant content
// existence oracle and tells the client where to fall back.
//
// Three NotFound("Blob", _) paths in the service map here:
// 1. user_owns_blob_reference returned false
// 2. get_blob_metadata returned None (blob row vanished)
// 3. add_reference lost the race with GC (rows_affected==0)
use crate::common::errors::ErrorKind;
if err.kind == ErrorKind::NotFound && err.entity_type == "Blob" {
return Response::builder()
.status(StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
r#"{"error":"blob_not_owned_by_caller","upload_path":"/api/files/upload"}"#,
))
.unwrap()
.into_response();
}
Self::domain_error_response(err).into_response()
}
}
}