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:
Jared Wolff
2026-03-05 00:19:14 -05:00
parent f20e8bf2dc
commit f730f576e7
2 changed files with 78 additions and 51 deletions
@@ -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
}
+70 -45
View File
@@ -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<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(
state: Arc<AppState>,
req: Request<Body>,
@@ -187,6 +209,25 @@ async fn handle_webdav_dispatch(
) -> Result<Response<Body>, 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::<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() {
"OPTIONS" => handle_options(path).await,
"GET" => handle_get(state, req, path).await,
@@ -927,22 +968,15 @@ async fn handle_mkcol(
req: Request<Body>,
path: String,
) -> Result<Response<Body>, 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<String> = 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);
}
}
} 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)
Err(_) => {
let create_dto = crate::application::dtos::folder_dto::CreateFolderDto {
name: folder_name.to_string(),
parent_id,
name: segment.to_string(),
parent_id: parent_id.clone(),
};
folder_service
let created = folder_service
.create_folder(create_dto)
.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()
.status(StatusCode::CREATED)