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:
M.Schmidt
2026-07-15 10:50:48 +02:00
parent d57f7bfe3a
commit af74c94028
12 changed files with 157 additions and 78 deletions
+18
View File
@@ -39,6 +39,12 @@ pub enum ErrorKind {
/// `AlreadyExists` (which is a uniqueness violation) so audit
/// readers can tell them apart.
Conflict,
/// RFC 7232 precondition failure — a caller-supplied conditional
/// (If-Match, or an internal compare-and-swap standing in for one)
/// did not hold against the resource's current state. Maps to
/// HTTP 412. Distinct from `Conflict` (409): this is specifically
/// "the state you thought you were writing against has moved."
PreconditionFailed,
}
impl ErrorKind {
@@ -58,6 +64,7 @@ impl ErrorKind {
ErrorKind::DatabaseError => "Database Error",
ErrorKind::QuotaExceeded => "Quota Exceeded",
ErrorKind::Conflict => "Conflict",
ErrorKind::PreconditionFailed => "Precondition Failed",
}
}
}
@@ -196,6 +203,17 @@ impl DomainError {
}
}
/// Creates a precondition-failed error (RFC 7232 / CAS mismatch)
pub fn precondition_failed<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::PreconditionFailed,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates a validation error
pub fn validation_error<S: Into<String>>(message: S) -> Self {
Self {