From f730f576e724836d8758891a893f626de610a278 Mon Sep 17 00:00:00 2001 From: Jared Wolff Date: Thu, 5 Mar 2026 00:19:14 -0500 Subject: [PATCH] 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 --- .../services/file_upload_service.rs | 8 +- src/interfaces/api/handlers/webdav_handler.rs | 121 +++++++++++------- 2 files changed, 78 insertions(+), 51 deletions(-) diff --git a/src/application/services/file_upload_service.rs b/src/application/services/file_upload_service.rs index 45a3d187..b42eaab8 100755 --- a/src/application/services/file_upload_service.rs +++ b/src/application/services/file_upload_service.rs @@ -279,15 +279,17 @@ impl FileUploadUseCase for FileUploadService { // File doesn't exist — create it via streaming upload 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..]) } else { ("", 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 { - file_read.get_parent_folder_id(parent_path).await.ok() + file_read.get_parent_folder_id(path_normalized).await.ok() } else { None } diff --git a/src/interfaces/api/handlers/webdav_handler.rs b/src/interfaces/api/handlers/webdav_handler.rs index de29d6af..d59d756d 100755 --- a/src/interfaces/api/handlers/webdav_handler.rs +++ b/src/interfaces/api/handlers/webdav_handler.rs @@ -180,6 +180,28 @@ async fn handle_webdav_methods( 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, + user_id: &str, + path: &str, +) -> Option { + 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( state: Arc, req: Request, @@ -187,6 +209,25 @@ async fn handle_webdav_dispatch( ) -> Result, AppError> { 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::() + .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() { "OPTIONS" => handle_options(path).await, "GET" => handle_get(state, req, path).await, @@ -927,22 +968,15 @@ async fn handle_mkcol( req: Request, path: String, ) -> Result, AppError> { - let user = extract_user(&req)?; - - // Get folder service from state let folder_service = &state.applications.folder_service; - // Check if path is empty (root folder) if path.is_empty() || path == "/" { 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 = { - // Convert the request into a body let body = req.into_body(); - - // Read request body (MKCOL — must be empty per RFC 4918) body::to_bytes(body, MAX_MKCOL_BODY) .await .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 - let folder_name = path.split('/').next_back().unwrap_or("unnamed"); + // Path is already translated by dispatch (e.g. "My Folder - jared/03/01"). + // 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 = None; + let mut accumulated_path = String::new(); - // Get parent folder path - let parent_path = if let Some(idx) = path.rfind('/') { - &path[..idx] - } else { - "" - }; + for segment in &segments { + if !accumulated_path.is_empty() { + accumulated_path.push('/'); + } + accumulated_path.push_str(segment); - // ── Resolve parent folder (user-scoped) ──────────────────── - let parent_id = if parent_path.is_empty() { - None - } 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 - ))); + match folder_service.get_folder_by_path(&accumulated_path).await { + Ok(existing) => { + parent_id = Some(existing.id); + } + Err(_) => { + let create_dto = crate::application::dtos::folder_dto::CreateFolderDto { + name: segment.to_string(), + parent_id: parent_id.clone(), + }; + let created = folder_service + .create_folder(create_dto) + .await + .map_err(|e| { + AppError::internal_error(format!( + "Failed to create folder '{}': {}", + accumulated_path, e + )) + })?; + parent_id = Some(created.id); } } - } 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 { - name: folder_name.to_string(), - parent_id, - }; - - folder_service - .create_folder(create_dto) - .await - .map_err(|e| AppError::internal_error(format!("Failed to create folder: {}", e)))?; + } Ok(Response::builder() .status(StatusCode::CREATED)