fix(nc/webdav): honour Overwrite on MOVE; restore-onto-existing → 412
Closes G4 / G5 / K5.
handle_move now resolves the destination once before the file/folder
dispatch and applies RFC 4918 §9.9.4:
- Overwrite: F on a collision → 412 Precondition Failed, source
untouched, destination untouched.
- Overwrite: T (or absent) on a collision → delete the existing
destination, then proceed → 204 No Content.
- No collision → 201 Created (unchanged).
The same destination lookup powers the 201-vs-204 status decision, so
adding the precondition guard adds zero extra DB hits on the happy
path.
handle_restore now catches the unique-index collision out of
restore_item and returns 412 instead of letting it bubble as 500.
Mirrors the G4 semantics for the trashbin surface (restore has no
Overwrite header so the refusal is unconditional; client resolves by
renaming the live file first).
Sabre/DAV's CorePlugin and our test pins agreed independently — NC
clients expect this exact behavior, and the new G5b/G5c positive-case
tests guard against a regression that hard-rejected every MOVE.
This commit is contained in:
@@ -108,15 +108,40 @@ async fn handle_restore(
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
||||
|
||||
trash_svc
|
||||
.restore_item(&id, user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to restore item: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::CREATED)
|
||||
.body(Body::empty())
|
||||
.unwrap())
|
||||
match trash_svc.restore_item(&id, user.id).await {
|
||||
Ok(()) => Ok(Response::builder()
|
||||
.status(StatusCode::CREATED)
|
||||
.body(Body::empty())
|
||||
.unwrap()),
|
||||
Err(e) => {
|
||||
// Collision at the original path — a live file/folder is sitting
|
||||
// where the trashed one wants to come back to. Mirrors the G4/G5
|
||||
// semantics in webdav_handler::handle_move ("Overwrite: F to an
|
||||
// existing path → 412"); restore has no Overwrite header so the
|
||||
// refusal is unconditional. The caller can resolve by renaming
|
||||
// or trashing the conflicting live resource first.
|
||||
//
|
||||
// We string-match for the unique-index / duplicate-key signature
|
||||
// because restore_item currently re-wraps every storage error as
|
||||
// InternalError, so the original DomainError::AlreadyExists kind
|
||||
// is not propagated. A follow-up should thread the kind through
|
||||
// and let this be a kind-based check.
|
||||
let msg = format!("{}", e);
|
||||
if msg.contains("duplicate key")
|
||||
|| msg.contains("unique constraint")
|
||||
|| msg.to_ascii_lowercase().contains("already exists")
|
||||
{
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::PRECONDITION_FAILED)
|
||||
.body(Body::empty())
|
||||
.unwrap());
|
||||
}
|
||||
Err(AppError::internal_error(format!(
|
||||
"Failed to restore item: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── DELETE (empty trash) ────────────────────
|
||||
|
||||
Reference in New Issue
Block a user