fix(webdav): translate paths for all operations and fix MKCOL recursive creation
- Add resolve_webdav_path() helper that prepends user's home folder prefix to raw WebDAV paths before routing to handlers - Rewrite handle_mkcol to walk path segments, creating folders with proper parent_id chain instead of relying on path-only lookup - Fix double path stripping in update_file_streaming where get_parent_folder_id received an already-stripped parent path instead of the full file path it expects Fixes #165
This commit is contained in:
@@ -279,15 +279,17 @@ impl FileUploadUseCase for FileUploadService {
|
|||||||
|
|
||||||
// File doesn't exist — create it via streaming upload
|
// File doesn't exist — create it via streaming upload
|
||||||
let path_normalized = path.trim_start_matches('/').trim_end_matches('/');
|
let path_normalized = path.trim_start_matches('/').trim_end_matches('/');
|
||||||
let (parent_path, filename) = if let Some(idx) = path_normalized.rfind('/') {
|
let (_, filename) = if let Some(idx) = path_normalized.rfind('/') {
|
||||||
(&path_normalized[..idx], &path_normalized[idx + 1..])
|
(&path_normalized[..idx], &path_normalized[idx + 1..])
|
||||||
} else {
|
} else {
|
||||||
("", path_normalized)
|
("", path_normalized)
|
||||||
};
|
};
|
||||||
|
|
||||||
let parent_id = if !parent_path.is_empty() {
|
// get_parent_folder_id expects the full file path — it strips the
|
||||||
|
// last segment (filename) internally to find the parent folder.
|
||||||
|
let parent_id = if path_normalized.contains('/') {
|
||||||
if let Some(file_read) = &self.file_read {
|
if let Some(file_read) = &self.file_read {
|
||||||
file_read.get_parent_folder_id(parent_path).await.ok()
|
file_read.get_parent_folder_id(path_normalized).await.ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,28 @@ async fn handle_webdav_methods(
|
|||||||
handle_webdav_dispatch(state, req, path).await
|
handle_webdav_dispatch(state, req, path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If `path` doesn't already start with the user's home folder name, prepend
|
||||||
|
/// the home folder path so downstream services can find the resource in the DB.
|
||||||
|
/// Returns `None` when the path already includes the prefix or resolution fails.
|
||||||
|
async fn resolve_webdav_path(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
user_id: &str,
|
||||||
|
path: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
let folder_service = &state.applications.folder_service;
|
||||||
|
let home_folders = folder_service
|
||||||
|
.list_folders_for_owner(None, user_id)
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
let home = home_folders.first()?;
|
||||||
|
|
||||||
|
if path.starts_with(&home.name) {
|
||||||
|
None // Already prefixed
|
||||||
|
} else {
|
||||||
|
Some(format!("{}/{}", home.path, path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_webdav_dispatch(
|
async fn handle_webdav_dispatch(
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
@@ -187,6 +209,25 @@ async fn handle_webdav_dispatch(
|
|||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
|
|
||||||
|
// Translate WebDAV path → DB path by prepending user's home folder
|
||||||
|
// prefix when the path doesn't already include it.
|
||||||
|
// Extract user_id before any async call to keep the future Send.
|
||||||
|
let path = if !path.is_empty() && method.as_str() != "OPTIONS" {
|
||||||
|
let user_id = req
|
||||||
|
.extensions()
|
||||||
|
.get::<CurrentUser>()
|
||||||
|
.map(|u| u.id.clone());
|
||||||
|
if let Some(uid) = user_id {
|
||||||
|
resolve_webdav_path(&state, &uid, &path)
|
||||||
|
.await
|
||||||
|
.unwrap_or(path)
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
match method.as_str() {
|
match method.as_str() {
|
||||||
"OPTIONS" => handle_options(path).await,
|
"OPTIONS" => handle_options(path).await,
|
||||||
"GET" => handle_get(state, req, path).await,
|
"GET" => handle_get(state, req, path).await,
|
||||||
@@ -927,22 +968,15 @@ async fn handle_mkcol(
|
|||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let user = extract_user(&req)?;
|
|
||||||
|
|
||||||
// Get folder service from state
|
|
||||||
let folder_service = &state.applications.folder_service;
|
let folder_service = &state.applications.folder_service;
|
||||||
|
|
||||||
// Check if path is empty (root folder)
|
|
||||||
if path.is_empty() || path == "/" {
|
if path.is_empty() || path == "/" {
|
||||||
return Err(AppError::conflict("Root folder already exists"));
|
return Err(AppError::conflict("Root folder already exists"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read request body - must be empty for MKCOL
|
// Read request body - must be empty for MKCOL (RFC 4918 §9.3)
|
||||||
let body_bytes = {
|
let body_bytes = {
|
||||||
// Convert the request into a body
|
|
||||||
let body = req.into_body();
|
let body = req.into_body();
|
||||||
|
|
||||||
// Read request body (MKCOL — must be empty per RFC 4918)
|
|
||||||
body::to_bytes(body, MAX_MKCOL_BODY)
|
body::to_bytes(body, MAX_MKCOL_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::payload_too_large(format!("MKCOL body too large: {}", e)))?
|
.map_err(|e| AppError::payload_too_large(format!("MKCOL body too large: {}", e)))?
|
||||||
@@ -954,50 +988,41 @@ async fn handle_mkcol(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract folder name from path
|
// Path is already translated by dispatch (e.g. "My Folder - jared/03/01").
|
||||||
let folder_name = path.split('/').next_back().unwrap_or("unnamed");
|
// Walk each segment: the first is the home folder (already exists),
|
||||||
|
// subsequent segments are created as needed with proper parent_id.
|
||||||
|
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
let mut parent_id: Option<String> = None;
|
||||||
|
let mut accumulated_path = String::new();
|
||||||
|
|
||||||
// Get parent folder path
|
for segment in &segments {
|
||||||
let parent_path = if let Some(idx) = path.rfind('/') {
|
if !accumulated_path.is_empty() {
|
||||||
&path[..idx]
|
accumulated_path.push('/');
|
||||||
} else {
|
}
|
||||||
""
|
accumulated_path.push_str(segment);
|
||||||
};
|
|
||||||
|
|
||||||
// ── Resolve parent folder (user-scoped) ────────────────────
|
match folder_service.get_folder_by_path(&accumulated_path).await {
|
||||||
let parent_id = if parent_path.is_empty() {
|
Ok(existing) => {
|
||||||
None
|
parent_id = Some(existing.id);
|
||||||
} else if let Some(resolver) = &state.path_resolver {
|
|
||||||
match resolver.resolve_path_for_user(parent_path, &user.id).await {
|
|
||||||
Ok(ResolvedResource::Folder(parent)) => Some(parent.id),
|
|
||||||
_ => {
|
|
||||||
return Err(AppError::not_found(format!(
|
|
||||||
"Parent folder not found: {}",
|
|
||||||
parent_path
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
Err(_) => {
|
||||||
} else {
|
|
||||||
// Legacy fallback — ownership check
|
|
||||||
match folder_service.get_folder_by_path(parent_path).await {
|
|
||||||
Ok(parent) => {
|
|
||||||
assert_owner(parent.owner_id.as_deref(), &user.id, parent_path)?;
|
|
||||||
Some(parent.id)
|
|
||||||
}
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create folder (user_id is inherited from the parent in the DB layer)
|
|
||||||
let create_dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
let create_dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
||||||
name: folder_name.to_string(),
|
name: segment.to_string(),
|
||||||
parent_id,
|
parent_id: parent_id.clone(),
|
||||||
};
|
};
|
||||||
|
let created = folder_service
|
||||||
folder_service
|
|
||||||
.create_folder(create_dto)
|
.create_folder(create_dto)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to create folder: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
AppError::internal_error(format!(
|
||||||
|
"Failed to create folder '{}': {}",
|
||||||
|
accumulated_path, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
parent_id = Some(created.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::CREATED)
|
.status(StatusCode::CREATED)
|
||||||
|
|||||||
Reference in New Issue
Block a user