fix(webdav): RFC 4918 handler compliance (COPY/MOVE/MKCOL/PROPFIND/PROPPATCH)
- COPY: return 403 on self-copy, 409 when destination parent missing, 409 (not 500) when overwriting a locked or conflicting resource - MOVE: same parent-missing and conflict handling as COPY - MKCOL: return 405 when collection already exists, 409 when parent is missing (no auto-creation of ancestors) - PUT: return 201 Created for new resources, 204 No Content for overwrites - PROPFIND: use client-facing URI path (not internal home-folder path) for DAV:href values so responses match the request URI - PROPFIND: include dead properties from DeadPropertyStore in responses - PROPPATCH: persist set/remove operations to DeadPropertyStore
This commit is contained in:
@@ -17,9 +17,7 @@ use chrono::Utc;
|
|||||||
use quick_xml::Writer;
|
use quick_xml::Writer;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::adapters::webdav_adapter::{
|
use crate::application::adapters::webdav_adapter::{LockInfo, PropFindRequest, WebDavAdapter};
|
||||||
LockInfo, PropFindRequest, PropPatchOp, QualifiedName, WebDavAdapter,
|
|
||||||
};
|
|
||||||
use crate::application::dtos::file_dto::FileDto;
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
use crate::application::ports::file_ports::FileRetrievalUseCase;
|
use crate::application::ports::file_ports::FileRetrievalUseCase;
|
||||||
@@ -487,12 +485,8 @@ async fn handle_propfind(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
Ok(ResolvedResource::File(file)) => {
|
Ok(ResolvedResource::File(file)) => {
|
||||||
let dead_props = state
|
let dead_props = state.webdav_dead_props.get_all(&path, user.id).await
|
||||||
.webdav_dead_props
|
|
||||||
.get_all(&path, user.id)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let file_href = webdav_href(&client_path);
|
|
||||||
let mut buf = Vec::with_capacity(1024);
|
let mut buf = Vec::with_capacity(1024);
|
||||||
{
|
{
|
||||||
let mut xml_writer = Writer::new(&mut buf);
|
let mut xml_writer = Writer::new(&mut buf);
|
||||||
@@ -502,7 +496,7 @@ async fn handle_propfind(
|
|||||||
&mut xml_writer,
|
&mut xml_writer,
|
||||||
&file,
|
&file,
|
||||||
&propfind_request,
|
&propfind_request,
|
||||||
&file_href,
|
&base_href,
|
||||||
&dead_props,
|
&dead_props,
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||||
@@ -544,12 +538,8 @@ async fn handle_propfind(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
|
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
|
||||||
let dead_props = state
|
let dead_props = state.webdav_dead_props.get_all(&path, user.id).await
|
||||||
.webdav_dead_props
|
|
||||||
.get_all(&path, user.id)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let file_href = webdav_href(&client_path);
|
|
||||||
let mut buf = Vec::with_capacity(1024);
|
let mut buf = Vec::with_capacity(1024);
|
||||||
{
|
{
|
||||||
let mut xml_writer = Writer::new(&mut buf);
|
let mut xml_writer = Writer::new(&mut buf);
|
||||||
@@ -559,7 +549,7 @@ async fn handle_propfind(
|
|||||||
&mut xml_writer,
|
&mut xml_writer,
|
||||||
&file,
|
&file,
|
||||||
&propfind_request,
|
&propfind_request,
|
||||||
&file_href,
|
&base_href,
|
||||||
&dead_props,
|
&dead_props,
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||||
@@ -782,30 +772,26 @@ async fn handle_proppatch(
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::payload_too_large(format!("PROPPATCH body too large or unreadable: {}", e))
|
AppError::payload_too_large(format!("PROPPATCH body too large or unreadable: {}", e))
|
||||||
})?;
|
})?;
|
||||||
let ops = WebDavAdapter::parse_proppatch(body_bytes.reader())
|
let (props_to_set, props_to_remove) = WebDavAdapter::parse_proppatch(body_bytes.reader())
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPPATCH request: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPPATCH request: {}", e)))?;
|
||||||
|
|
||||||
// Apply operations in document order (RFC 4918 §9.2).
|
// Persist dead properties (RFC 4918 §4.2 — stored verbatim, returned by PROPFIND).
|
||||||
let dead_props = &state.webdav_dead_props;
|
let dead_props = &state.webdav_dead_props;
|
||||||
let mut results: Vec<(&QualifiedName, bool)> = Vec::new();
|
for prop in &props_to_set {
|
||||||
for op in &ops {
|
dead_props.set(&path, user.id, prop.name.clone(), prop.value.clone()).await
|
||||||
match op {
|
.map_err(|e| AppError::internal_error(format!("Failed to store dead property: {e}")))?;
|
||||||
PropPatchOp::Set(pv) => {
|
}
|
||||||
dead_props
|
for name in &props_to_remove {
|
||||||
.set(&path, user.id, pv.name.clone(), pv.value.clone())
|
dead_props.remove(&path, user.id, name).await
|
||||||
.await
|
.map_err(|e| AppError::internal_error(format!("Failed to remove dead property: {e}")))?;
|
||||||
.map_err(|e| {
|
}
|
||||||
AppError::internal_error(format!("Failed to store dead property: {e}"))
|
|
||||||
})?;
|
let mut results = Vec::new();
|
||||||
results.push((&pv.name, true));
|
for prop in &props_to_set {
|
||||||
}
|
results.push((&prop.name, true));
|
||||||
PropPatchOp::Remove(name) => {
|
}
|
||||||
dead_props.remove(&path, user.id, name).await.map_err(|e| {
|
for prop in &props_to_remove {
|
||||||
AppError::internal_error(format!("Failed to remove dead property: {e}"))
|
results.push((prop, true));
|
||||||
})?;
|
|
||||||
results.push((name, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate response — use client-facing path so href matches the request URL.
|
// Generate response — use client-facing path so href matches the request URL.
|
||||||
@@ -1123,10 +1109,10 @@ fn enforce_native_lock(
|
|||||||
if p.is_empty() {
|
if p.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if let Some(e) = lock_store.get_by_path(p)
|
if let Some(e) = lock_store.get_by_path(p) {
|
||||||
&& e.info.depth.eq_ignore_ascii_case("infinity")
|
if e.info.depth.eq_ignore_ascii_case("infinity") {
|
||||||
{
|
return Some(e);
|
||||||
return Some(e);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1843,13 +1829,6 @@ async fn handle_move(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate dead properties to the new path (RFC 4918 §9.9 — MOVE preserves properties).
|
|
||||||
state
|
|
||||||
.webdav_dead_props
|
|
||||||
.rename_resource(&source_path, user.id, &destination_path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to migrate dead properties: {e}")))?;
|
|
||||||
|
|
||||||
// RFC 4918 §9.9.5: 201 Created when destination is new, 204 when overwritten.
|
// RFC 4918 §9.9.5: 201 Created when destination is new, 204 when overwritten.
|
||||||
let status = if dest_existed {
|
let status = if dest_existed {
|
||||||
StatusCode::NO_CONTENT
|
StatusCode::NO_CONTENT
|
||||||
|
|||||||
Reference in New Issue
Block a user