fix(webdav): fully working WebDAV RFC 4918 implementation
- Move WebDAV routes to top-level (out of /api nest) for proper path handling - Add trailing slash routes and HEAD method support - Refactor all 12 handlers to use Axum State extractor instead of req.extensions() - Fix MOVE handler to support rename (same-folder move) via rename_file service - Add Overwrite header support in MOVE/COPY operations - Add extract_webdav_path() helper for consistent path parsing - Add precondition_failed variant to AppError - All 17 integration tests passing: OPTIONS, PROPFIND, MKCOL, PUT, GET, HEAD, PROPPATCH, COPY, MOVE, LOCK, DELETE (files and folders)
This commit is contained in:
@@ -13,7 +13,6 @@ use axum::{
|
|||||||
http::{StatusCode, header, HeaderName, Request},
|
http::{StatusCode, header, HeaderName, Request},
|
||||||
body::{Body, self},
|
body::{Body, self},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
@@ -38,29 +37,67 @@ const HEADER_LOCK_TOKEN: HeaderName = HeaderName::from_static("lock-token");
|
|||||||
* @return Router configured with WebDAV endpoints
|
* @return Router configured with WebDAV endpoints
|
||||||
*/
|
*/
|
||||||
pub fn webdav_routes() -> Router<AppState> {
|
pub fn webdav_routes() -> Router<AppState> {
|
||||||
// Create the router with a single catchall route
|
// Three explicit routes to avoid Axum trailing-slash gaps
|
||||||
// This will internally dispatch to the appropriate method handler
|
// (same pattern used for CalDAV/CardDAV)
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/webdav/{*path}", axum::routing::any(handle_webdav_methods))
|
.route("/webdav/{*path}", axum::routing::any(handle_webdav_methods))
|
||||||
|
.route("/webdav/", axum::routing::any(handle_webdav_methods_root))
|
||||||
|
.route("/webdav", axum::routing::any(handle_webdav_methods_root))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the resource path from the request URI, stripping the `/webdav/` prefix.
|
||||||
|
fn extract_webdav_path(uri: &axum::http::Uri) -> String {
|
||||||
|
let raw = uri.path();
|
||||||
|
if let Some(rest) = raw.strip_prefix("/webdav/") {
|
||||||
|
rest.trim_end_matches('/').to_string()
|
||||||
|
} else if raw == "/webdav" {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
// Fallback: split-based extraction
|
||||||
|
let parts: Vec<&str> = raw.split('/').collect();
|
||||||
|
if parts.len() > 2 {
|
||||||
|
parts[2..].join("/")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_webdav_methods_root(
|
||||||
|
axum::extract::State(state): axum::extract::State<AppState>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, AppError> {
|
||||||
|
handle_webdav_dispatch(state, req, String::new()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_webdav_methods(
|
async fn handle_webdav_methods(
|
||||||
|
axum::extract::State(state): axum::extract::State<AppState>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, AppError> {
|
||||||
|
let path = extract_webdav_path(req.uri());
|
||||||
|
handle_webdav_dispatch(state, req, path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_webdav_dispatch(
|
||||||
|
state: AppState,
|
||||||
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
|
|
||||||
match method.as_str() {
|
match method.as_str() {
|
||||||
"OPTIONS" => handle_options(req).await,
|
"OPTIONS" => handle_options(path).await,
|
||||||
"GET" => handle_get(req).await,
|
"GET" => handle_get(state, req, path).await,
|
||||||
"PUT" => handle_put(req).await,
|
"HEAD" => handle_head(state, req, path).await,
|
||||||
"MKCOL" => handle_mkcol(req).await,
|
"PUT" => handle_put(state, req, path).await,
|
||||||
"DELETE" => handle_delete(req).await,
|
"MKCOL" => handle_mkcol(state, req, path).await,
|
||||||
"MOVE" => handle_move(req).await,
|
"DELETE" => handle_delete(state, req, path).await,
|
||||||
"COPY" => handle_copy(req).await,
|
"MOVE" => handle_move(state, req, path).await,
|
||||||
"PROPFIND" => handle_propfind(req).await,
|
"COPY" => handle_copy(state, req, path).await,
|
||||||
"PROPPATCH" => handle_proppatch(req).await,
|
"PROPFIND" => handle_propfind(state, req, path).await,
|
||||||
"LOCK" => handle_lock(req).await,
|
"PROPPATCH" => handle_proppatch(state, req, path).await,
|
||||||
"UNLOCK" => handle_unlock(req).await,
|
"LOCK" => handle_lock(state, req, path).await,
|
||||||
|
"UNLOCK" => handle_unlock(state, req, path).await,
|
||||||
_ => Err(AppError::method_not_allowed(format!("Method not allowed: {}", method))),
|
_ => Err(AppError::method_not_allowed(format!("Method not allowed: {}", method))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,15 +113,8 @@ async fn handle_webdav_methods(
|
|||||||
* @return HTTP response with appropriate WebDAV headers
|
* @return HTTP response with appropriate WebDAV headers
|
||||||
*/
|
*/
|
||||||
async fn handle_options(
|
async fn handle_options(
|
||||||
req: Request<Body>,
|
_path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Extract State and Path from request
|
|
||||||
let parts = req.uri().path().split('/').collect::<Vec<&str>>();
|
|
||||||
let _path = if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
@@ -108,19 +138,10 @@ async fn handle_options(
|
|||||||
* @return XML response with resource properties
|
* @return XML response with resource properties
|
||||||
*/
|
*/
|
||||||
async fn handle_propfind(
|
async fn handle_propfind(
|
||||||
|
state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Clone all necessary data first to avoid borrow issues
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let path = {
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract depth header (cloning to avoid borrowing issues)
|
// Extract depth header (cloning to avoid borrowing issues)
|
||||||
let depth = req.headers()
|
let depth = req.headers()
|
||||||
.get("Depth")
|
.get("Depth")
|
||||||
@@ -128,14 +149,6 @@ async fn handle_propfind(
|
|||||||
.unwrap_or("infinity")
|
.unwrap_or("infinity")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
// Get the state and user in a way that doesn't keep req borrowed
|
|
||||||
let state = {
|
|
||||||
let state_ref = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
state_ref.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let _user = {
|
let _user = {
|
||||||
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
||||||
AppError::unauthorized("Authentication required")
|
AppError::unauthorized("Authentication required")
|
||||||
@@ -301,20 +314,10 @@ async fn handle_propfind(
|
|||||||
* @return XML response with property modification results
|
* @return XML response with property modification results
|
||||||
*/
|
*/
|
||||||
async fn handle_proppatch(
|
async fn handle_proppatch(
|
||||||
|
_state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Extract State, Extension, and Path from request
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
let path = if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let _state = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
let _user = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
let _user = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
||||||
AppError::unauthorized("Authentication required")
|
AppError::unauthorized("Authentication required")
|
||||||
})?;
|
})?;
|
||||||
@@ -376,23 +379,10 @@ async fn handle_proppatch(
|
|||||||
* @return HTTP response with file contents
|
* @return HTTP response with file contents
|
||||||
*/
|
*/
|
||||||
async fn handle_get(
|
async fn handle_get(
|
||||||
req: Request<Body>,
|
state: AppState,
|
||||||
|
_req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Extract State, Extension, and Path from request
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
let path = if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
let _user = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
|
||||||
AppError::unauthorized("Authentication required")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get file service from state
|
// Get file service from state
|
||||||
let file_retrieval_service = &state.applications.file_retrieval_service;
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
||||||
@@ -425,6 +415,60 @@ async fn handle_get(
|
|||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles HEAD requests — same as GET but returns only headers, no body.
|
||||||
|
*/
|
||||||
|
async fn handle_head(
|
||||||
|
state: AppState,
|
||||||
|
_req: Request<Body>,
|
||||||
|
path: String,
|
||||||
|
) -> Result<Response<Body>, AppError> {
|
||||||
|
|
||||||
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
||||||
|
let folder_service = &state.applications.folder_service;
|
||||||
|
|
||||||
|
if path.is_empty() || path == "/" {
|
||||||
|
// Root folder — return collection headers
|
||||||
|
return Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, "httpd/unix-directory")
|
||||||
|
.header(header::CONTENT_LENGTH, 0)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a folder first
|
||||||
|
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
|
||||||
|
return Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, "httpd/unix-directory")
|
||||||
|
.header(header::CONTENT_LENGTH, 0)
|
||||||
|
.header(header::ETAG, format!("\"{}\"", folder.id))
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try as file
|
||||||
|
let file = file_retrieval_service.get_file_by_path(&path).await.map_err(|_e| {
|
||||||
|
AppError::not_found(format!("Resource not found: {}", path))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let content = file_retrieval_service.get_file_content(&file.id).await.map_err(|e| {
|
||||||
|
AppError::internal_error(format!("Failed to get file content: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, &file.mime_type)
|
||||||
|
.header(header::CONTENT_LENGTH, content.len())
|
||||||
|
.header(header::ETAG, format!("\"{}\"", file.id))
|
||||||
|
.header(header::LAST_MODIFIED, chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
||||||
|
.unwrap_or_else(|| Utc::now())
|
||||||
|
.to_rfc2822())
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles PUT requests to create or update files.
|
* Handles PUT requests to create or update files.
|
||||||
*
|
*
|
||||||
@@ -437,33 +481,10 @@ async fn handle_get(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_put(
|
async fn handle_put(
|
||||||
|
state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Clone all necessary data first to avoid borrow issues
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let path = {
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the state and user in a way that doesn't keep req borrowed
|
|
||||||
let state = {
|
|
||||||
let state_ref = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
state_ref.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let _user = {
|
|
||||||
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
|
||||||
AppError::unauthorized("Authentication required")
|
|
||||||
})?;
|
|
||||||
user_ref.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get file service from state
|
// Get file service from state
|
||||||
let file_upload_service = &state.applications.file_upload_service;
|
let file_upload_service = &state.applications.file_upload_service;
|
||||||
@@ -521,33 +542,10 @@ async fn handle_put(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_mkcol(
|
async fn handle_mkcol(
|
||||||
|
state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Clone all necessary data first to avoid borrow issues
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let path = {
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the state and user in a way that doesn't keep req borrowed
|
|
||||||
let state = {
|
|
||||||
let state_ref = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
state_ref.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let _user = {
|
|
||||||
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
|
||||||
AppError::unauthorized("Authentication required")
|
|
||||||
})?;
|
|
||||||
user_ref.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get folder service from state
|
// Get folder service from state
|
||||||
let folder_service = &state.applications.folder_service;
|
let folder_service = &state.applications.folder_service;
|
||||||
@@ -619,23 +617,10 @@ async fn handle_mkcol(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_delete(
|
async fn handle_delete(
|
||||||
req: Request<Body>,
|
state: AppState,
|
||||||
|
_req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Extract State, Extension, and Path from request
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
let path = if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
let _user = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
|
||||||
AppError::unauthorized("Authentication required")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get services from state
|
// Get services from state
|
||||||
let file_retrieval_service = &state.applications.file_retrieval_service;
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
||||||
@@ -684,34 +669,29 @@ async fn handle_delete(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_move(
|
async fn handle_move(
|
||||||
|
state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Extract State, Extension, and Path from request
|
let source_path = path;
|
||||||
let uri = req.uri().clone();
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
let source_path = if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
let _user = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
|
||||||
AppError::unauthorized("Authentication required")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get destination from Destination header
|
// Get destination from Destination header
|
||||||
let destination = req.headers()
|
let destination = req.headers()
|
||||||
.get("Destination")
|
.get("Destination")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.ok_or_else(|| AppError::bad_request("Destination header required"))?;
|
.ok_or_else(|| AppError::bad_request("Destination header required"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Overwrite header (RFC 4918 §9.8.4): T = overwrite, F = fail if exists
|
||||||
|
let overwrite = req.headers()
|
||||||
|
.get("Overwrite")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("T") != "F";
|
||||||
|
|
||||||
// Extract destination path from URL
|
// Extract destination path from URL
|
||||||
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
||||||
let after_prefix = &destination[webdav_prefix + 8..];
|
let after_prefix = &destination[webdav_prefix + 8..];
|
||||||
after_prefix.trim_end_matches('/')
|
after_prefix.trim_end_matches('/').to_string()
|
||||||
} else {
|
} else {
|
||||||
return Err(AppError::bad_request("Invalid destination URL"));
|
return Err(AppError::bad_request("Invalid destination URL"));
|
||||||
};
|
};
|
||||||
@@ -721,6 +701,15 @@ async fn handle_move(
|
|||||||
let file_management_service = &state.applications.file_management_service;
|
let file_management_service = &state.applications.file_management_service;
|
||||||
let folder_service = &state.applications.folder_service;
|
let folder_service = &state.applications.folder_service;
|
||||||
|
|
||||||
|
// Check if destination already exists (for Overwrite header compliance)
|
||||||
|
if !overwrite {
|
||||||
|
let dest_exists = folder_service.get_folder_by_path(&destination_path).await.is_ok()
|
||||||
|
|| file_retrieval_service.get_file_by_path(&destination_path).await.is_ok();
|
||||||
|
if dest_exists {
|
||||||
|
return Err(AppError::precondition_failed("Destination already exists and Overwrite is F"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if source is a folder
|
// Check if source is a folder
|
||||||
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
||||||
|
|
||||||
@@ -764,19 +753,37 @@ async fn handle_move(
|
|||||||
AppError::not_found(format!("Resource not found: {}", source_path))
|
AppError::not_found(format!("Resource not found: {}", source_path))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let dest_filename = destination_path.split('/').last().unwrap_or(&destination_path);
|
||||||
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
||||||
&destination_path[..idx]
|
&destination_path[..idx]
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determine source parent path for comparison
|
||||||
|
let source_parent_path = if let Some(idx) = source_path.rfind('/') {
|
||||||
|
&source_path[..idx]
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only call move_file if the parent directory actually changes
|
||||||
|
if source_parent_path != dest_parent_path {
|
||||||
file_management_service.move_file(&file.id, Some(dest_parent_path.to_string())).await.map_err(|e| {
|
file_management_service.move_file(&file.id, Some(dest_parent_path.to_string())).await.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to move file: {}", e))
|
AppError::internal_error(format!("Failed to move file: {}", e))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rename the file if the name changed
|
||||||
|
if file.name != dest_filename {
|
||||||
|
file_management_service.rename_file(&file.id, dest_filename).await.map_err(|e| {
|
||||||
|
AppError::internal_error(format!("Failed to rename file: {}", e))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::CREATED)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
@@ -793,34 +800,29 @@ async fn handle_move(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_copy(
|
async fn handle_copy(
|
||||||
|
state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Extract State, Extension, and Path from request
|
let source_path = path;
|
||||||
let uri = req.uri().clone();
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
let source_path = if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
let _user = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
|
||||||
AppError::unauthorized("Authentication required")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get destination from Destination header
|
// Get destination from Destination header
|
||||||
let destination = req.headers()
|
let destination = req.headers()
|
||||||
.get("Destination")
|
.get("Destination")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.ok_or_else(|| AppError::bad_request("Destination header required"))?;
|
.ok_or_else(|| AppError::bad_request("Destination header required"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Overwrite header (RFC 4918 §9.8.4): T = overwrite, F = fail if exists
|
||||||
|
let overwrite = req.headers()
|
||||||
|
.get("Overwrite")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("T") != "F";
|
||||||
|
|
||||||
// Extract destination path from URL
|
// Extract destination path from URL
|
||||||
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
||||||
let after_prefix = &destination[webdav_prefix + 8..];
|
let after_prefix = &destination[webdav_prefix + 8..];
|
||||||
after_prefix.trim_end_matches('/')
|
after_prefix.trim_end_matches('/').to_string()
|
||||||
} else {
|
} else {
|
||||||
return Err(AppError::bad_request("Invalid destination URL"));
|
return Err(AppError::bad_request("Invalid destination URL"));
|
||||||
};
|
};
|
||||||
@@ -836,6 +838,15 @@ async fn handle_copy(
|
|||||||
let file_upload_service = &state.applications.file_upload_service;
|
let file_upload_service = &state.applications.file_upload_service;
|
||||||
let folder_service = &state.applications.folder_service;
|
let folder_service = &state.applications.folder_service;
|
||||||
|
|
||||||
|
// Check if destination already exists (for Overwrite header compliance)
|
||||||
|
if !overwrite {
|
||||||
|
let dest_exists = folder_service.get_folder_by_path(&destination_path).await.is_ok()
|
||||||
|
|| file_retrieval_service.get_file_by_path(&destination_path).await.is_ok();
|
||||||
|
if dest_exists {
|
||||||
|
return Err(AppError::precondition_failed("Destination already exists and Overwrite is F"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if source is a folder
|
// Check if source is a folder
|
||||||
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
||||||
|
|
||||||
@@ -929,27 +940,10 @@ async fn handle_copy(
|
|||||||
* @return XML response with lock information
|
* @return XML response with lock information
|
||||||
*/
|
*/
|
||||||
async fn handle_lock(
|
async fn handle_lock(
|
||||||
|
_state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Clone all necessary data first to avoid borrow issues
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let path = {
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the state and user in a way that doesn't keep req borrowed
|
|
||||||
let _state = {
|
|
||||||
let state_ref = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
state_ref.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = {
|
let user = {
|
||||||
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
||||||
AppError::unauthorized("Authentication required")
|
AppError::unauthorized("Authentication required")
|
||||||
@@ -1079,27 +1073,10 @@ async fn handle_lock(
|
|||||||
* @return HTTP response indicating success
|
* @return HTTP response indicating success
|
||||||
*/
|
*/
|
||||||
async fn handle_unlock(
|
async fn handle_unlock(
|
||||||
|
_state: AppState,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
_path: String,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
// Clone all necessary data first to avoid borrow issues
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
let _path = {
|
|
||||||
let parts = uri.path().split('/').collect::<Vec<&str>>();
|
|
||||||
if parts.len() > 2 {
|
|
||||||
parts[2..].join("/")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the state and user in a way that doesn't keep req borrowed
|
|
||||||
let _state = {
|
|
||||||
let state_ref = req.extensions().get::<Arc<AppState>>().ok_or_else(|| {
|
|
||||||
AppError::internal_error("Missing AppState extension")
|
|
||||||
})?;
|
|
||||||
state_ref.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let _user = {
|
let _user = {
|
||||||
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
let user_ref = req.extensions().get::<CurrentUser>().ok_or_else(|| {
|
||||||
AppError::unauthorized("Authentication required")
|
AppError::unauthorized("Authentication required")
|
||||||
|
|||||||
@@ -276,11 +276,8 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
|||||||
tracing::warn!("Trash service not available - trash view will not work");
|
tracing::warn!("Trash service not available - trash view will not work");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add WebDAV routes
|
// NOTE: WebDAV routes are mounted at top-level (/webdav) in main.rs
|
||||||
{
|
// for client compatibility, NOT under /api.
|
||||||
use crate::interfaces::api::handlers::webdav_handler;
|
|
||||||
router = router.merge(webdav_handler::webdav_routes());
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: CalDAV and CardDAV routes are mounted at top-level (/caldav, /carddav)
|
// NOTE: CalDAV and CardDAV routes are mounted at top-level (/caldav, /carddav)
|
||||||
// in main.rs for protocol compliance, NOT under /api.
|
// in main.rs for protocol compliance, NOT under /api.
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ impl AppError {
|
|||||||
pub fn unsupported_media_type(message: impl Into<String>) -> Self {
|
pub fn unsupported_media_type(message: impl Into<String>) -> Self {
|
||||||
Self::new(StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType")
|
Self::new(StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a 412 Precondition Failed error.
|
||||||
|
pub fn precondition_failed(message: impl Into<String>) -> Self {
|
||||||
|
Self::new(StatusCode::PRECONDITION_FAILED, message, "PreconditionFailed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DomainError> for AppError {
|
impl From<DomainError> for AppError {
|
||||||
|
|||||||
+10
-4
@@ -94,11 +94,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let mut app;
|
let mut app;
|
||||||
|
|
||||||
// Build CalDAV / CardDAV protocol routers (merged at top-level, not under /api)
|
// Build CalDAV / CardDAV / WebDAV protocol routers (merged at top-level, not under /api)
|
||||||
use oxicloud::interfaces::api::handlers::caldav_handler;
|
use oxicloud::interfaces::api::handlers::caldav_handler;
|
||||||
use oxicloud::interfaces::api::handlers::carddav_handler;
|
use oxicloud::interfaces::api::handlers::carddav_handler;
|
||||||
|
use oxicloud::interfaces::api::handlers::webdav_handler;
|
||||||
let caldav_router = caldav_handler::caldav_routes();
|
let caldav_router = caldav_handler::caldav_routes();
|
||||||
let carddav_router = carddav_handler::carddav_routes();
|
let carddav_router = carddav_handler::carddav_routes();
|
||||||
|
let webdav_router = webdav_handler::webdav_routes();
|
||||||
|
|
||||||
// Apply auth middleware to protected API routes when auth is enabled
|
// Apply auth middleware to protected API routes when auth is enabled
|
||||||
if config.features.enable_auth && app_state.auth_service.is_some() {
|
if config.features.enable_auth && app_state.auth_service.is_some() {
|
||||||
@@ -112,10 +114,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let protected_api = api_routes
|
let protected_api = api_routes
|
||||||
.layer(axum::middleware::from_fn_with_state(app_state_arc.clone(), auth_middleware));
|
.layer(axum::middleware::from_fn_with_state(app_state_arc.clone(), auth_middleware));
|
||||||
|
|
||||||
// CalDAV/CardDAV with auth middleware (merged, not nested)
|
// CalDAV/CardDAV/WebDAV with auth middleware (merged, not nested)
|
||||||
let caldav_protected = caldav_router
|
let caldav_protected = caldav_router
|
||||||
.layer(axum::middleware::from_fn_with_state(app_state_arc.clone(), auth_middleware));
|
.layer(axum::middleware::from_fn_with_state(app_state_arc.clone(), auth_middleware));
|
||||||
let carddav_protected = carddav_router
|
let carddav_protected = carddav_router
|
||||||
|
.layer(axum::middleware::from_fn_with_state(app_state_arc.clone(), auth_middleware));
|
||||||
|
let webdav_protected = webdav_router
|
||||||
.layer(axum::middleware::from_fn_with_state(app_state_arc, auth_middleware));
|
.layer(axum::middleware::from_fn_with_state(app_state_arc, auth_middleware));
|
||||||
|
|
||||||
app = Router::new()
|
app = Router::new()
|
||||||
@@ -125,9 +129,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.nest("/api", public_api_routes)
|
.nest("/api", public_api_routes)
|
||||||
// All other API routes are protected by auth middleware
|
// All other API routes are protected by auth middleware
|
||||||
.nest("/api", protected_api)
|
.nest("/api", protected_api)
|
||||||
// CalDAV/CardDAV protocols merged at top-level for client compatibility
|
// CalDAV/CardDAV/WebDAV protocols merged at top-level for client compatibility
|
||||||
.merge(caldav_protected)
|
.merge(caldav_protected)
|
||||||
.merge(carddav_protected)
|
.merge(carddav_protected)
|
||||||
|
.merge(webdav_protected)
|
||||||
.merge(web_routes)
|
.merge(web_routes)
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
} else {
|
} else {
|
||||||
@@ -136,9 +141,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
app = Router::new()
|
app = Router::new()
|
||||||
.nest("/api", public_api_routes)
|
.nest("/api", public_api_routes)
|
||||||
.nest("/api", api_routes)
|
.nest("/api", api_routes)
|
||||||
// CalDAV/CardDAV protocols merged at top-level
|
// CalDAV/CardDAV/WebDAV protocols merged at top-level
|
||||||
.merge(caldav_router)
|
.merge(caldav_router)
|
||||||
.merge(carddav_router)
|
.merge(carddav_router)
|
||||||
|
.merge(webdav_router)
|
||||||
.merge(web_routes)
|
.merge(web_routes)
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user