fix(webdav): make PATCH's concurrency guard a real compare-and-swap
The app-level ETag re-check before the write still left a gap between the check and the actual UPDATE for a concurrent writer to land in. Push the check into the write path itself: swap_blob_hash now takes an expected_hash and only applies the SET under the same FOR UPDATE row lock it already held, closing the race instead of just narrowing it. Adds ErrorKind::PreconditionFailed (412) for the CAS-miss path; PUT/WOPI/chunked-upload keep blind-overwrite semantics by passing None
This commit is contained in:
@@ -99,6 +99,16 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
|
||||
/// on the overwrite branch and `authz.require(caller, Create,
|
||||
/// Folder|Drive(id))` on the new-file branch. Handlers just plumb
|
||||
/// `caller_id` through — no protocol-layer authz.
|
||||
///
|
||||
/// `expected_hash`: forwarded to
|
||||
/// `FileWritePort::update_file_content_with_blob` on the overwrite
|
||||
/// branch for compare-and-swap; ignored on the new-file branch
|
||||
/// (nothing to compare against). Pass `None` for plain PUT/WOPI/
|
||||
/// chunked-upload last-write-wins semantics; pass the pre-write
|
||||
/// snapshot's content hash for PATCH, where a concurrent write
|
||||
/// during the (potentially slow) splice must be rejected rather
|
||||
/// than silently clobbered.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn update_file_streaming_with_perms(
|
||||
&self,
|
||||
path: &str,
|
||||
@@ -107,6 +117,7 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
|
||||
content_type: &str,
|
||||
modified_at: Option<i64>,
|
||||
caller_id: Uuid,
|
||||
expected_hash: Option<&str>,
|
||||
) -> Result<FileDto, DomainError>;
|
||||
}
|
||||
|
||||
|
||||
@@ -299,6 +299,14 @@ pub trait FileWritePort: Send + Sync + 'static {
|
||||
///
|
||||
/// `caller_id` is stamped into `updated_by` alongside the
|
||||
/// `updated_at` bump (§14 provenance).
|
||||
///
|
||||
/// `expected_hash`: when `Some`, makes this a true compare-and-swap —
|
||||
/// the write only takes effect if the row's current `blob_hash`
|
||||
/// still equals it, checked and applied atomically under the same
|
||||
/// row lock (no gap between check and write for a concurrent writer
|
||||
/// to land in). A mismatch returns `ErrorKind::PreconditionFailed`
|
||||
/// and leaves the row untouched. `None` keeps the previous
|
||||
/// blind-overwrite behaviour (PUT/WOPI/chunked-upload finalize).
|
||||
async fn update_file_content_with_blob(
|
||||
&self,
|
||||
file_id: &str,
|
||||
@@ -306,6 +314,7 @@ pub trait FileWritePort: Send + Sync + 'static {
|
||||
size: u64,
|
||||
modified_at: Option<i64>,
|
||||
caller_id: Uuid,
|
||||
expected_hash: Option<&str>,
|
||||
) -> Result<(String, i64), DomainError>;
|
||||
|
||||
/// Registers file metadata WITHOUT writing content to disk (write-behind).
|
||||
|
||||
Reference in New Issue
Block a user