diff --git a/src/infrastructure/services/dedup_service.rs b/src/infrastructure/services/dedup_service.rs index 2de40c59..907cf453 100755 --- a/src/infrastructure/services/dedup_service.rs +++ b/src/infrastructure/services/dedup_service.rs @@ -276,9 +276,20 @@ impl DedupService { })?; if let Err(e) = fs::rename(&temp_path, &blob_path).await { - // Another writer already placed the blob — discard ours - let _ = fs::remove_file(&temp_path).await; - tracing::debug!("Blob file already placed by concurrent writer: {}", e); + if e.raw_os_error() == Some(18) { + // EXDEV: cross-device link — fall back to copy+delete + fs::copy(&temp_path, &blob_path).await.map_err(|ce| { + DomainError::internal_error( + "Dedup", + format!("Failed to copy temp blob cross-device: {}", ce), + ) + })?; + let _ = fs::remove_file(&temp_path).await; + } else { + // Another writer already placed the blob — discard ours + let _ = fs::remove_file(&temp_path).await; + tracing::debug!("Blob file already placed by concurrent writer: {}", e); + } } } @@ -365,8 +376,17 @@ impl DedupService { // dirs live on different filesystems (rare), this falls back to // copy+delete which is slower but still correct. if let Err(e) = fs::rename(source_path, &blob_path).await { - // Another writer may have placed the blob concurrently - if blob_path.exists() { + if e.raw_os_error() == Some(18) { + // EXDEV: cross-device link — fall back to copy+delete + fs::copy(source_path, &blob_path).await.map_err(|ce| { + DomainError::internal_error( + "Dedup", + format!("Failed to copy file to blob store: {}", ce), + ) + })?; + let _ = fs::remove_file(source_path).await; + } else if blob_path.exists() { + // Another writer may have placed the blob concurrently let _ = fs::remove_file(source_path).await; tracing::debug!("Blob file placed by concurrent writer: {}", e); } else { diff --git a/src/interfaces/nextcloud/webdav_handler.rs b/src/interfaces/nextcloud/webdav_handler.rs index cc5fd9ea..5fe4b4e2 100644 --- a/src/interfaces/nextcloud/webdav_handler.rs +++ b/src/interfaces/nextcloud/webdav_handler.rs @@ -572,15 +572,23 @@ async fn handle_mkcol( parent_id: Some(parent_folder.id.clone()), }; - folder_service - .create_folder(dto) - .await - .map_err(|e| AppError::internal_error(format!("Failed to create folder: {}", e)))?; - - Ok(Response::builder() - .status(StatusCode::CREATED) - .body(Body::empty()) - .unwrap()) + match folder_service.create_folder(dto).await { + Ok(_) => Ok(Response::builder() + .status(StatusCode::CREATED) + .body(Body::empty()) + .unwrap()), + Err(e) if e.message.contains("already exists") || e.message.contains("Already Exists") => { + // RFC 4918 §9.3.1: MKCOL on existing resource → 405 + Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Body::empty()) + .unwrap()) + } + Err(e) => Err(AppError::internal_error(format!( + "Failed to create folder: {}", + e + ))), + } } // ──────────────────── DELETE ────────────────────