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
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user