2025-04-04 01:48:55 +02:00
|
|
|
/**
|
|
|
|
|
* WebDAV Handler Module
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* This module implements the WebDAV protocol (RFC 4918) endpoints for OxiCloud.
|
|
|
|
|
* It provides a complete WebDAV server implementation that allows clients to
|
|
|
|
|
* perform file operations over HTTP, including reading, writing, and manipulating
|
|
|
|
|
* files and directories.
|
|
|
|
|
*/
|
|
|
|
|
use axum::{
|
2025-04-09 00:21:20 +02:00
|
|
|
Router,
|
2026-02-14 01:29:34 +01:00
|
|
|
body::{self, Body},
|
|
|
|
|
http::{HeaderName, Request, StatusCode, header},
|
2025-04-04 01:48:55 +02:00
|
|
|
response::Response,
|
|
|
|
|
};
|
2025-04-04 21:31:41 +02:00
|
|
|
use bytes::Buf;
|
2026-02-14 01:29:34 +01:00
|
|
|
use chrono::Utc;
|
|
|
|
|
use uuid::Uuid;
|
2025-04-04 01:48:55 +02:00
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::application::adapters::webdav_adapter::{
|
|
|
|
|
LockInfo, LockScope, LockType, PropFindRequest, WebDavAdapter,
|
|
|
|
|
};
|
2025-04-04 21:31:41 +02:00
|
|
|
use crate::application::dtos::folder_dto::FolderDto;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::common::di::AppState;
|
2026-02-02 23:56:40 +01:00
|
|
|
use crate::interfaces::errors::AppError;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::interfaces::middleware::auth::CurrentUser;
|
2025-04-04 21:31:41 +02:00
|
|
|
|
|
|
|
|
// Create a custom DAV header since it's not in the standard headers
|
|
|
|
|
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
|
|
|
|
const HEADER_LOCK_TOKEN: HeaderName = HeaderName::from_static("lock-token");
|
2025-04-10 01:43:25 +02:00
|
|
|
// const HEADER_IF: HeaderName = HeaderName::from_static("if");
|
2025-04-04 01:48:55 +02:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
/// Maximum body size for XML-based WebDAV requests (PROPFIND, PROPPATCH, LOCK).
|
|
|
|
|
/// 1 MB is generous — a typical PROPFIND body is < 1 KB.
|
|
|
|
|
const MAX_XML_BODY: usize = 1_048_576;
|
|
|
|
|
|
|
|
|
|
/// Maximum body size for MKCOL requests (RFC 4918: body must be empty).
|
|
|
|
|
const MAX_MKCOL_BODY: usize = 4096;
|
|
|
|
|
|
2025-04-04 01:48:55 +02:00
|
|
|
/**
|
|
|
|
|
* Creates and returns the WebDAV router with all required endpoints.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* This function sets up all WebDAV method handlers following RFC 4918,
|
|
|
|
|
* mapping HTTP methods to appropriate WebDAV operations.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* @return Router configured with WebDAV endpoints
|
|
|
|
|
*/
|
2025-04-04 21:31:41 +02:00
|
|
|
pub fn webdav_routes() -> Router<AppState> {
|
2026-02-10 19:26:28 +01:00
|
|
|
// Three explicit routes to avoid Axum trailing-slash gaps
|
|
|
|
|
// (same pattern used for CalDAV/CardDAV)
|
2025-04-04 01:48:55 +02:00
|
|
|
Router::new()
|
2025-04-04 22:00:29 +02:00
|
|
|
.route("/webdav/{*path}", axum::routing::any(handle_webdav_methods))
|
2026-02-10 19:26:28 +01:00
|
|
|
.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
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_webdav_methods(
|
2026-02-10 19:26:28 +01:00
|
|
|
axum::extract::State(state): axum::extract::State<AppState>,
|
|
|
|
|
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,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let method = req.method().clone();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
match method.as_str() {
|
2026-02-10 19:26:28 +01:00
|
|
|
"OPTIONS" => handle_options(path).await,
|
|
|
|
|
"GET" => handle_get(state, req, path).await,
|
|
|
|
|
"HEAD" => handle_head(state, req, path).await,
|
|
|
|
|
"PUT" => handle_put(state, req, path).await,
|
|
|
|
|
"MKCOL" => handle_mkcol(state, req, path).await,
|
|
|
|
|
"DELETE" => handle_delete(state, req, path).await,
|
|
|
|
|
"MOVE" => handle_move(state, req, path).await,
|
|
|
|
|
"COPY" => handle_copy(state, req, path).await,
|
|
|
|
|
"PROPFIND" => handle_propfind(state, req, path).await,
|
|
|
|
|
"PROPPATCH" => handle_proppatch(state, req, path).await,
|
|
|
|
|
"LOCK" => handle_lock(state, req, path).await,
|
|
|
|
|
"UNLOCK" => handle_unlock(state, req, path).await,
|
2026-02-14 01:29:34 +01:00
|
|
|
_ => Err(AppError::method_not_allowed(format!(
|
|
|
|
|
"Method not allowed: {}",
|
|
|
|
|
method
|
|
|
|
|
))),
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles OPTIONS requests to advertise WebDAV capabilities.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* This handler responds with the DAV header indicating WebDAV compliance
|
|
|
|
|
* level and the methods supported by this WebDAV server.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @return HTTP response with appropriate WebDAV headers
|
|
|
|
|
*/
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn handle_options(_path: String) -> Result<Response<Body>, AppError> {
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
2025-04-04 01:48:55 +02:00
|
|
|
.status(StatusCode::OK)
|
2025-04-04 21:31:41 +02:00
|
|
|
.header(HEADER_DAV, "1, 2") // Class 1 and 2 WebDAV support
|
2026-02-14 01:29:34 +01:00
|
|
|
.header(
|
|
|
|
|
header::ALLOW,
|
|
|
|
|
"OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK",
|
|
|
|
|
)
|
2025-04-04 21:31:41 +02:00
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles PROPFIND requests to retrieve resource properties.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler processes WebDAV PROPFIND requests according to RFC 4918,
|
|
|
|
|
* retrieving properties of files and folders in the specified path.
|
|
|
|
|
* It supports the Depth header to control recursion depth.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* @param state The application state containing service dependencies
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param user The authenticated user information
|
2025-04-04 01:48:55 +02:00
|
|
|
* @param path The requested resource path
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param req The HTTP request containing the PROPFIND XML body
|
|
|
|
|
* @return XML response with resource properties
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_propfind(
|
2026-02-10 19:26:28 +01:00
|
|
|
state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
// Extract depth header (cloning to avoid borrowing issues)
|
2026-02-14 01:29:34 +01:00
|
|
|
let depth = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("Depth")
|
2025-04-04 01:48:55 +02:00
|
|
|
.and_then(|v| v.to_str().ok())
|
2025-04-04 21:31:41 +02:00
|
|
|
.unwrap_or("infinity")
|
|
|
|
|
.to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
let _user = {
|
2026-02-14 01:29:34 +01:00
|
|
|
let user_ref = req
|
|
|
|
|
.extensions()
|
|
|
|
|
.get::<CurrentUser>()
|
|
|
|
|
.ok_or_else(|| AppError::unauthorized("Authentication required"))?;
|
2025-04-04 21:31:41 +02:00
|
|
|
user_ref.clone()
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Extract the body separately to avoid borrow issues
|
|
|
|
|
let body_bytes = {
|
|
|
|
|
// Convert the request into a body
|
|
|
|
|
let body = req.into_body();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
// Read request body (PROPFIND is XML, 1 MB is more than enough)
|
|
|
|
|
body::to_bytes(body, MAX_XML_BODY)
|
2025-04-04 21:31:41 +02:00
|
|
|
.await
|
2026-02-14 01:29:34 +01:00
|
|
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?
|
2025-04-04 01:48:55 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Parse PROPFIND request
|
|
|
|
|
let propfind_request = if body_bytes.is_empty() {
|
|
|
|
|
// Empty body means get all properties
|
|
|
|
|
PropFindRequest {
|
|
|
|
|
prop_find_type: crate::application::adapters::webdav_adapter::PropFindType::AllProp,
|
|
|
|
|
}
|
2025-04-04 01:48:55 +02:00
|
|
|
} else {
|
2025-04-04 21:31:41 +02:00
|
|
|
// Parse XML body
|
|
|
|
|
WebDavAdapter::parse_propfind(body_bytes.reader()).map_err(|e| {
|
|
|
|
|
AppError::bad_request(format!("Failed to parse PROPFIND request: {}", e))
|
|
|
|
|
})?
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get folder service from state
|
|
|
|
|
let folder_service = &state.applications.folder_service;
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Determine base HREF
|
|
|
|
|
let base_href = format!("/webdav/{}/", path);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if path exists as a file or folder
|
|
|
|
|
if path.is_empty() || path == "/" {
|
|
|
|
|
// Root folder
|
2026-02-14 01:29:34 +01:00
|
|
|
let subfolders = folder_service
|
|
|
|
|
.list_folders(None)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to get subfolders: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
let files = file_retrieval_service
|
|
|
|
|
.list_files(None)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to get files: {}", e)))?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Create root folder DTO for response
|
|
|
|
|
let root_folder = FolderDto {
|
|
|
|
|
id: "root".to_string(),
|
|
|
|
|
name: "".to_string(),
|
|
|
|
|
path: "".to_string(),
|
|
|
|
|
parent_id: None,
|
2026-02-15 23:45:11 +01:00
|
|
|
owner_id: None,
|
2025-04-04 21:31:41 +02:00
|
|
|
created_at: Utc::now().timestamp() as u64,
|
|
|
|
|
modified_at: Utc::now().timestamp() as u64,
|
|
|
|
|
is_root: true,
|
2026-02-16 01:09:28 +01:00
|
|
|
icon_class: "fas fa-folder".to_string(),
|
|
|
|
|
icon_special_class: "folder-icon".to_string(),
|
|
|
|
|
category: "Folder".to_string(),
|
2025-04-04 01:48:55 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response
|
|
|
|
|
let mut response_body = Vec::new();
|
2025-04-04 01:48:55 +02:00
|
|
|
WebDavAdapter::generate_propfind_response(
|
2025-04-04 21:31:41 +02:00
|
|
|
&mut response_body,
|
|
|
|
|
Some(&root_folder),
|
2025-04-04 01:48:55 +02:00
|
|
|
&files,
|
2025-04-04 21:31:41 +02:00
|
|
|
&subfolders,
|
|
|
|
|
&propfind_request,
|
|
|
|
|
&depth,
|
|
|
|
|
&base_href,
|
2026-02-14 01:29:34 +01:00
|
|
|
)
|
|
|
|
|
.map_err(|e| {
|
2025-04-04 21:31:41 +02:00
|
|
|
AppError::internal_error(format!("Failed to generate PROPFIND response: {}", e))
|
|
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 01:48:55 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
2025-04-04 21:31:41 +02:00
|
|
|
.body(Body::from(response_body))
|
2025-04-04 01:48:55 +02:00
|
|
|
.unwrap())
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
// Check if path is a folder
|
|
|
|
|
let folder_result = folder_service.get_folder_by_path(&path).await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if let Ok(folder) = folder_result {
|
|
|
|
|
// Path is a folder
|
|
|
|
|
let files = if depth != "0" {
|
2026-02-14 01:29:34 +01:00
|
|
|
file_retrieval_service
|
|
|
|
|
.list_files(Some(&folder.id))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to get files: {}", e)))?
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
vec![]
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
let subfolders = if depth != "0" {
|
2026-02-14 01:29:34 +01:00
|
|
|
folder_service
|
|
|
|
|
.list_folders(Some(&folder.id))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to get subfolders: {}", e))
|
|
|
|
|
})?
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
vec![]
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response
|
|
|
|
|
let mut response_body = Vec::new();
|
|
|
|
|
WebDavAdapter::generate_propfind_response(
|
|
|
|
|
&mut response_body,
|
|
|
|
|
Some(&folder),
|
|
|
|
|
&files,
|
|
|
|
|
&subfolders,
|
|
|
|
|
&propfind_request,
|
|
|
|
|
&depth,
|
|
|
|
|
&base_href,
|
2026-02-14 01:29:34 +01:00
|
|
|
)
|
|
|
|
|
.map_err(|e| {
|
2025-04-04 21:31:41 +02:00
|
|
|
AppError::internal_error(format!("Failed to generate PROPFIND response: {}", e))
|
|
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(response_body))
|
|
|
|
|
.unwrap())
|
|
|
|
|
} else {
|
|
|
|
|
// Check if path is a file
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_result = file_retrieval_service.get_file_by_path(&path).await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if let Ok(file) = file_result {
|
|
|
|
|
// Path is a file
|
|
|
|
|
let mut response_body = Vec::new();
|
|
|
|
|
WebDavAdapter::generate_propfind_response_for_file(
|
|
|
|
|
&mut response_body,
|
|
|
|
|
&file,
|
|
|
|
|
&propfind_request,
|
|
|
|
|
&depth,
|
|
|
|
|
&base_href,
|
2026-02-14 01:29:34 +01:00
|
|
|
)
|
|
|
|
|
.map_err(|e| {
|
2025-04-04 21:31:41 +02:00
|
|
|
AppError::internal_error(format!("Failed to generate PROPFIND response: {}", e))
|
|
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(response_body))
|
|
|
|
|
.unwrap())
|
|
|
|
|
} else {
|
|
|
|
|
// Path does not exist
|
|
|
|
|
Err(AppError::not_found(format!("Resource not found: {}", path)))
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
/**
|
|
|
|
|
* Handles PROPPATCH requests to set or remove resource properties.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler processes WebDAV PROPPATCH requests according to RFC 4918,
|
|
|
|
|
* modifying properties of files and folders in the specified path.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @param req The HTTP request containing the PROPPATCH XML body
|
|
|
|
|
* @return XML response with property modification results
|
|
|
|
|
*/
|
|
|
|
|
async fn handle_proppatch(
|
2026-02-10 19:26:28 +01:00
|
|
|
_state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
2026-02-14 01:29:34 +01:00
|
|
|
let _user = req
|
|
|
|
|
.extensions()
|
|
|
|
|
.get::<CurrentUser>()
|
|
|
|
|
.ok_or_else(|| AppError::unauthorized("Authentication required"))?;
|
|
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
// Read request body (XML — bounded to 1 MB)
|
|
|
|
|
let body_bytes = body::to_bytes(req.into_body(), MAX_XML_BODY)
|
2025-04-04 21:31:41 +02:00
|
|
|
.await
|
2026-02-22 23:28:03 +01:00
|
|
|
.map_err(|e| AppError::payload_too_large(format!("PROPPATCH body too large or unreadable: {}", e)))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
let (props_to_set, props_to_remove) = WebDavAdapter::parse_proppatch(body_bytes.reader())
|
|
|
|
|
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPPATCH request: {}", e)))?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// For now, we don't actually persist custom properties, but we respond as if we did
|
|
|
|
|
// In a full implementation, we would store these properties in a database
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response - we'll pretend all operations succeeded
|
|
|
|
|
let mut results = Vec::new();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// For each property to set, indicate success
|
|
|
|
|
for prop in &props_to_set {
|
|
|
|
|
results.push((&prop.name, true));
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// For each property to remove, indicate success
|
|
|
|
|
for prop in &props_to_remove {
|
|
|
|
|
results.push((prop, true));
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response
|
|
|
|
|
let href = format!("/webdav/{}", path);
|
|
|
|
|
let mut response_body = Vec::new();
|
2026-02-14 01:29:34 +01:00
|
|
|
WebDavAdapter::generate_proppatch_response(&mut response_body, &href, &results).map_err(
|
|
|
|
|
|e| AppError::internal_error(format!("Failed to generate PROPPATCH response: {}", e)),
|
|
|
|
|
)?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(response_body))
|
|
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 01:48:55 +02:00
|
|
|
/**
|
|
|
|
|
* Handles GET requests to retrieve file contents.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler retrieves the contents of a file at the specified path.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* @param state The application state containing service dependencies
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @return HTTP response with file contents
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_get(
|
2026-02-10 19:26:28 +01:00
|
|
|
state: AppState,
|
|
|
|
|
_req: Request<Body>,
|
|
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
// Get file service from state
|
|
|
|
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if path is empty (root folder)
|
|
|
|
|
if path.is_empty() || path == "/" {
|
2025-04-04 01:48:55 +02:00
|
|
|
return Err(AppError::bad_request("Cannot GET a directory"));
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 01:48:55 +02:00
|
|
|
// Get file metadata
|
2026-02-14 01:29:34 +01:00
|
|
|
let file = file_retrieval_service
|
|
|
|
|
.get_file_by_path(&path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_e| AppError::not_found(format!("File not found: {}", path)))?;
|
|
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
// Stream file content — constant ~64 KB memory regardless of file size
|
|
|
|
|
let stream = file_retrieval_service
|
|
|
|
|
.get_file_stream(&file.id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
2026-02-15 17:53:25 +01:00
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to stream file: {}", e)))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
// Build streaming response using Content-Length from metadata
|
2025-04-04 01:48:55 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, file.mime_type)
|
2026-02-15 17:53:25 +01:00
|
|
|
.header(header::CONTENT_LENGTH, file.size)
|
2025-04-04 01:48:55 +02:00
|
|
|
.header(header::ETAG, format!("\"{}\"", file.id))
|
2026-02-14 01:29:34 +01:00
|
|
|
.header(
|
|
|
|
|
header::LAST_MODIFIED,
|
|
|
|
|
chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
|
|
|
|
.unwrap_or_else(Utc::now)
|
|
|
|
|
.to_rfc2822(),
|
|
|
|
|
)
|
2026-02-15 17:53:25 +01:00
|
|
|
.body(Body::from_stream(Box::into_pin(stream)))
|
2025-04-04 01:48:55 +02:00
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
/**
|
|
|
|
|
* 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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
// Try as file — use metadata only, never load content for HEAD
|
2026-02-14 01:29:34 +01:00
|
|
|
let file = file_retrieval_service
|
|
|
|
|
.get_file_by_path(&path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
|
2026-02-10 19:26:28 +01:00
|
|
|
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, &file.mime_type)
|
2026-02-15 17:53:25 +01:00
|
|
|
.header(header::CONTENT_LENGTH, file.size)
|
2026-02-10 19:26:28 +01:00
|
|
|
.header(header::ETAG, format!("\"{}\"", file.id))
|
2026-02-14 01:29:34 +01:00
|
|
|
.header(
|
|
|
|
|
header::LAST_MODIFIED,
|
|
|
|
|
chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
|
|
|
|
.unwrap_or_else(Utc::now)
|
|
|
|
|
.to_rfc2822(),
|
|
|
|
|
)
|
2026-02-10 19:26:28 +01:00
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 01:48:55 +02:00
|
|
|
/**
|
|
|
|
|
* Handles PUT requests to create or update files.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2026-02-22 23:28:03 +01:00
|
|
|
* **Streaming implementation**: the request body is spooled to a temp file
|
|
|
|
|
* with incremental SHA-256 hashing. Peak RAM usage is ~256 KB regardless
|
|
|
|
|
* of file size. The temp file is then atomically moved into blob storage
|
|
|
|
|
* via `update_file_streaming`.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @param req The HTTP request containing the file contents
|
|
|
|
|
* @return HTTP response indicating success
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_put(
|
2026-02-10 19:26:28 +01:00
|
|
|
state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
2026-02-22 23:28:03 +01:00
|
|
|
use http_body_util::BodyStream;
|
|
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
|
use tokio_stream::StreamExt;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get file service from state
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_upload_service = &state.applications.file_upload_service;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if path is empty (root folder)
|
2026-02-22 23:28:03 +01:00
|
|
|
if path.is_empty() || path == \"/\" {
|
|
|
|
|
return Err(AppError::bad_request(\"Cannot PUT to root folder\"));
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
// Hard upload size limit from config
|
|
|
|
|
let max_upload = state.core.config.storage.max_upload_size;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Extract content type before consuming the request
|
2026-02-22 23:28:03 +01:00
|
|
|
let content_type = req
|
2026-02-14 01:29:34 +01:00
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get(header::CONTENT_TYPE)
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
2026-02-22 23:28:03 +01:00
|
|
|
.unwrap_or(\"application/octet-stream\")
|
2025-04-04 21:31:41 +02:00
|
|
|
.to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
// ── Streaming spool: body → temp file + incremental hash ──
|
|
|
|
|
let temp_file = tempfile::NamedTempFile::new()
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!(\"Failed to create temp file: {}\", e)))?;
|
|
|
|
|
let temp_path = temp_file.path().to_path_buf();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
let mut file = tokio::fs::File::create(&temp_path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!(\"Failed to open temp file: {}\", e)))?;
|
|
|
|
|
|
|
|
|
|
let mut hasher = Sha256::new();
|
|
|
|
|
let mut total_bytes: usize = 0;
|
|
|
|
|
let mut stream = BodyStream::new(req.into_body());
|
|
|
|
|
|
|
|
|
|
while let Some(frame_result) = stream.next().await {
|
|
|
|
|
let frame = frame_result
|
|
|
|
|
.map_err(|e| AppError::bad_request(format!(\"Failed to read request body: {}\", e)))?;
|
|
|
|
|
if let Some(chunk) = frame.data_ref() {
|
|
|
|
|
total_bytes += chunk.len();
|
|
|
|
|
if total_bytes > max_upload {
|
|
|
|
|
// Abort early — stop reading, delete temp file
|
|
|
|
|
drop(file);
|
|
|
|
|
let _ = tokio::fs::remove_file(&temp_path).await;
|
|
|
|
|
return Err(AppError::payload_too_large(format!(
|
|
|
|
|
\"Upload exceeds maximum size of {} bytes\",
|
|
|
|
|
max_upload
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
hasher.update(chunk);
|
|
|
|
|
file.write_all(chunk)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!(\"Failed to write to temp file: {}\", e)))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
file.flush().await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!(\"Failed to flush temp file: {}\", e)))?;
|
|
|
|
|
drop(file);
|
|
|
|
|
|
|
|
|
|
let hash = hex::encode(hasher.finalize());
|
|
|
|
|
|
|
|
|
|
// ── Atomic store: temp file → dedup blob + DB metadata update ──
|
|
|
|
|
let result = file_upload_service
|
|
|
|
|
.update_file_streaming(
|
|
|
|
|
&path,
|
|
|
|
|
&temp_path,
|
|
|
|
|
total_bytes as u64,
|
|
|
|
|
&content_type,
|
|
|
|
|
Some(hash),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
// Clean up temp file (may already be moved by dedup, ignore error)
|
|
|
|
|
let _ = tokio::fs::remove_file(&temp_path).await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
match result {
|
|
|
|
|
Ok(_) => Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap()),
|
2026-02-14 01:29:34 +01:00
|
|
|
Err(e) => Err(AppError::internal_error(format!(
|
2026-02-22 23:28:03 +01:00
|
|
|
\"Failed to put file: {}\",
|
2026-02-14 01:29:34 +01:00
|
|
|
e
|
|
|
|
|
))),
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-04-04 21:31:41 +02:00
|
|
|
* Handles MKCOL requests to create folders.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler creates a new folder at the specified path.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @return HTTP response indicating success
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_mkcol(
|
2026-02-10 19:26:28 +01:00
|
|
|
state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
// Get folder service from state
|
|
|
|
|
let folder_service = &state.applications.folder_service;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if path is empty (root folder)
|
|
|
|
|
if path.is_empty() || path == "/" {
|
|
|
|
|
return Err(AppError::conflict("Root folder already exists"));
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Read request body - must be empty for MKCOL
|
|
|
|
|
let body_bytes = {
|
|
|
|
|
// Convert the request into a body
|
|
|
|
|
let body = req.into_body();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
// Read request body (MKCOL — must be empty per RFC 4918)
|
|
|
|
|
body::to_bytes(body, MAX_MKCOL_BODY)
|
2025-04-04 21:31:41 +02:00
|
|
|
.await
|
2026-02-22 23:28:03 +01:00
|
|
|
.map_err(|e| AppError::payload_too_large(format!("MKCOL body too large: {}", e)))?
|
2025-04-04 21:31:41 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if !body_bytes.is_empty() {
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(AppError::unsupported_media_type(
|
|
|
|
|
"MKCOL request body must be empty",
|
|
|
|
|
));
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Extract folder name from path
|
2026-02-14 01:26:02 +01:00
|
|
|
let folder_name = path.split('/').next_back().unwrap_or("unnamed");
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get parent folder path
|
|
|
|
|
let parent_path = if let Some(idx) = path.rfind('/') {
|
|
|
|
|
&path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Create folder
|
|
|
|
|
let create_dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
|
|
|
|
name: folder_name.to_string(),
|
2026-02-14 01:29:34 +01:00
|
|
|
parent_id: if parent_path.is_empty() {
|
|
|
|
|
None
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
// Try to get the parent folder ID from its path
|
|
|
|
|
match folder_service.get_folder_by_path(parent_path).await {
|
|
|
|
|
Ok(parent) => Some(parent.id),
|
2026-02-14 01:29:34 +01:00
|
|
|
Err(_) => None, // If not found, use root
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
},
|
2025-04-04 21:31:41 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
folder_service
|
|
|
|
|
.create_folder(create_dto)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to create folder: {}", e)))?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::CREATED)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-04-04 21:31:41 +02:00
|
|
|
* Handles DELETE requests to remove files or folders.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler deletes a file or folder at the specified path.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @return HTTP response indicating success
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_delete(
|
2026-02-10 19:26:28 +01:00
|
|
|
state: AppState,
|
|
|
|
|
_req: Request<Body>,
|
|
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
// Get services from state
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
|
|
|
|
let file_management_service = &state.applications.file_management_service;
|
2025-04-04 21:31:41 +02:00
|
|
|
let folder_service = &state.applications.folder_service;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if path is empty (root folder)
|
|
|
|
|
if path.is_empty() || path == "/" {
|
|
|
|
|
return Err(AppError::forbidden("Cannot delete root folder"));
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if path is a folder
|
|
|
|
|
let folder_result = folder_service.get_folder_by_path(&path).await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if let Ok(folder) = folder_result {
|
2026-02-16 00:22:42 +01:00
|
|
|
// Delete folder — use the folder's own owner as caller_id
|
|
|
|
|
let caller_id = folder.owner_id.as_deref().unwrap_or("webdav");
|
2026-02-14 01:29:34 +01:00
|
|
|
folder_service
|
2026-02-16 00:22:42 +01:00
|
|
|
.delete_folder(&folder.id, caller_id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
// Try to delete file
|
2026-02-14 01:29:34 +01:00
|
|
|
let file = file_retrieval_service
|
|
|
|
|
.get_file_by_path(&path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
|
|
|
|
|
|
|
|
|
|
file_management_service
|
|
|
|
|
.delete_file(&file.id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to delete file: {}", e)))?;
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-04-04 21:31:41 +02:00
|
|
|
* Handles MOVE requests to rename or relocate files or folders.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler moves a file or folder from one path to another.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The source resource path
|
|
|
|
|
* @param req The HTTP request containing the destination path
|
|
|
|
|
* @return HTTP response indicating success
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
2025-04-04 21:31:41 +02:00
|
|
|
async fn handle_move(
|
2026-02-10 19:26:28 +01:00
|
|
|
state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
2026-02-10 19:26:28 +01:00
|
|
|
let source_path = path;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get destination from Destination header
|
2026-02-14 01:29:34 +01:00
|
|
|
let destination = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("Destination")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
2026-02-10 19:26:28 +01:00
|
|
|
.ok_or_else(|| AppError::bad_request("Destination header required"))?
|
|
|
|
|
.to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// Overwrite header (RFC 4918 §9.8.4): T = overwrite, F = fail if exists
|
2026-02-14 01:29:34 +01:00
|
|
|
let overwrite = req
|
|
|
|
|
.headers()
|
2026-02-10 19:26:28 +01:00
|
|
|
.get("Overwrite")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
2026-02-14 01:29:34 +01:00
|
|
|
.unwrap_or("T")
|
|
|
|
|
!= "F";
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Extract destination path from URL
|
|
|
|
|
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
|
|
|
|
let after_prefix = &destination[webdav_prefix + 8..];
|
2026-02-10 19:26:28 +01:00
|
|
|
after_prefix.trim_end_matches('/').to_string()
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
return Err(AppError::bad_request("Invalid destination URL"));
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get services from state
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
|
|
|
|
let file_management_service = &state.applications.file_management_service;
|
2025-04-04 21:31:41 +02:00
|
|
|
let folder_service = &state.applications.folder_service;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// Check if destination already exists (for Overwrite header compliance)
|
|
|
|
|
if !overwrite {
|
2026-02-14 01:29:34 +01:00
|
|
|
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();
|
2026-02-10 19:26:28 +01:00
|
|
|
if dest_exists {
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(AppError::precondition_failed(
|
|
|
|
|
"Destination already exists and Overwrite is F",
|
|
|
|
|
));
|
2026-02-10 19:26:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if source is a folder
|
|
|
|
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if let Ok(folder) = folder_result {
|
|
|
|
|
// Move folder
|
2026-02-14 01:29:34 +01:00
|
|
|
let dest_folder_name = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
2025-04-04 21:31:41 +02:00
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Create DTOs for moving and renaming
|
|
|
|
|
let move_dto = crate::application::dtos::folder_dto::MoveFolderDto {
|
|
|
|
|
parent_id: if dest_parent_path.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
|
|
|
|
Ok(parent) => Some(parent.id),
|
2026-02-14 01:29:34 +01:00
|
|
|
Err(_) => None, // If not found, use root
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
},
|
2025-04-04 21:31:41 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
folder_service
|
2026-02-21 13:33:18 +01:00
|
|
|
.move_folder(
|
|
|
|
|
&folder.id,
|
|
|
|
|
move_dto,
|
|
|
|
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
|
|
|
|
)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if folder.name != dest_folder_name {
|
|
|
|
|
let rename_dto = crate::application::dtos::folder_dto::RenameFolderDto {
|
2026-02-14 01:29:34 +01:00
|
|
|
name: dest_folder_name.to_string(),
|
2025-04-04 21:31:41 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
folder_service
|
2026-02-21 13:33:18 +01:00
|
|
|
.rename_folder(
|
|
|
|
|
&folder.id,
|
|
|
|
|
rename_dto,
|
|
|
|
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
|
|
|
|
)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Try to move file
|
2026-02-14 01:29:34 +01:00
|
|
|
let file = file_retrieval_service
|
|
|
|
|
.get_file_by_path(&source_path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
|
|
|
|
|
|
|
|
|
|
let dest_filename = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
2025-04-04 21:31:41 +02:00
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// Determine source parent path for comparison
|
|
|
|
|
let source_parent_path = if let Some(idx) = source_path.rfind('/') {
|
|
|
|
|
&source_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// Only call move_file if the parent directory actually changes
|
|
|
|
|
if source_parent_path != dest_parent_path {
|
2026-02-14 01:29:34 +01:00
|
|
|
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)))?;
|
2026-02-10 19:26:28 +01:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// Rename the file if the name changed
|
|
|
|
|
if file.name != dest_filename {
|
2026-02-14 01:29:34 +01:00
|
|
|
file_management_service
|
|
|
|
|
.rename_file(&file.id, dest_filename)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to rename file: {}", e)))?;
|
2026-02-10 19:26:28 +01:00
|
|
|
}
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
2026-02-10 19:26:28 +01:00
|
|
|
.status(StatusCode::CREATED)
|
2025-04-04 21:31:41 +02:00
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-04-04 21:31:41 +02:00
|
|
|
* Handles COPY requests to duplicate files or folders.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler copies a file or folder from one path to another.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The source resource path
|
|
|
|
|
* @param req The HTTP request containing the destination path
|
|
|
|
|
* @return HTTP response indicating success
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
2025-04-04 21:31:41 +02:00
|
|
|
async fn handle_copy(
|
2026-02-10 19:26:28 +01:00
|
|
|
state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
2026-02-10 19:26:28 +01:00
|
|
|
let source_path = path;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get destination from Destination header
|
2026-02-14 01:29:34 +01:00
|
|
|
let destination = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("Destination")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
2026-02-10 19:26:28 +01:00
|
|
|
.ok_or_else(|| AppError::bad_request("Destination header required"))?
|
|
|
|
|
.to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// Overwrite header (RFC 4918 §9.8.4): T = overwrite, F = fail if exists
|
2026-02-14 01:29:34 +01:00
|
|
|
let overwrite = req
|
|
|
|
|
.headers()
|
2026-02-10 19:26:28 +01:00
|
|
|
.get("Overwrite")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
2026-02-14 01:29:34 +01:00
|
|
|
.unwrap_or("T")
|
|
|
|
|
!= "F";
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Extract destination path from URL
|
|
|
|
|
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
|
|
|
|
let after_prefix = &destination[webdav_prefix + 8..];
|
2026-02-10 19:26:28 +01:00
|
|
|
after_prefix.trim_end_matches('/').to_string()
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
return Err(AppError::bad_request("Invalid destination URL"));
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get depth from Depth header
|
2026-02-14 01:29:34 +01:00
|
|
|
let depth = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("Depth")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.unwrap_or("infinity");
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get services from state
|
|
|
|
|
let file_retrieval_service = &state.applications.file_retrieval_service;
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_upload_service = &state.applications.file_upload_service;
|
|
|
|
|
let folder_service = &state.applications.folder_service;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// Check if destination already exists (for Overwrite header compliance)
|
|
|
|
|
if !overwrite {
|
2026-02-14 01:29:34 +01:00
|
|
|
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();
|
2026-02-10 19:26:28 +01:00
|
|
|
if dest_exists {
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(AppError::precondition_failed(
|
|
|
|
|
"Destination already exists and Overwrite is F",
|
|
|
|
|
));
|
2026-02-10 19:26:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if source is a folder
|
|
|
|
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if let Ok(folder) = folder_result {
|
|
|
|
|
// Copy folder
|
|
|
|
|
let recursive = depth != "0";
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
let dest_folder_name = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
2025-04-04 21:31:41 +02:00
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// For now, just create a new folder and copy files individually
|
|
|
|
|
// In a real implementation, we would have a dedicated copy_folder service method
|
|
|
|
|
let create_dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
|
|
|
|
name: dest_folder_name.to_string(),
|
2026-02-14 01:29:34 +01:00
|
|
|
parent_id: if dest_parent_path.is_empty() {
|
|
|
|
|
None
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
|
|
|
|
// Try to get the parent folder ID from its path
|
|
|
|
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
|
|
|
|
Ok(parent) => Some(parent.id),
|
2026-02-14 01:29:34 +01:00
|
|
|
Err(_) => None, // If not found, use root
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
},
|
2025-04-04 21:31:41 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
let _new_folder = folder_service
|
|
|
|
|
.create_folder(create_dto)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to create destination folder: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
if recursive {
|
|
|
|
|
// Copy subfolders and files (simplified implementation)
|
2026-02-14 01:29:34 +01:00
|
|
|
let files = file_retrieval_service
|
|
|
|
|
.list_files(Some(&folder.id))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to list files: {}", e)))?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
for file in files {
|
|
|
|
|
// Get file content
|
2026-02-08 13:40:23 +01:00
|
|
|
if let Ok(content) = file_retrieval_service.get_file_content(&file.id).await {
|
|
|
|
|
// Create new file in destination
|
2026-02-14 01:29:34 +01:00
|
|
|
file_upload_service
|
|
|
|
|
.create_file(&destination_path, &file.name, &content, &file.mime_type)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!(
|
|
|
|
|
"Failed to copy file {}: {}",
|
|
|
|
|
file.name, e
|
|
|
|
|
))
|
|
|
|
|
})?;
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-15 17:53:25 +01:00
|
|
|
// Copy file — use zero-copy dedup (only increments blob ref_count, no content loaded)
|
2026-02-14 01:29:34 +01:00
|
|
|
let file = file_retrieval_service
|
|
|
|
|
.get_file_by_path(&source_path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
|
|
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
// Get destination parent folder ID
|
2025-04-04 21:31:41 +02:00
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
let target_folder_id = if dest_parent_path.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
|
|
|
|
Ok(parent) => Some(parent.id),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Zero-copy: only creates a new metadata row + increments blob reference count.
|
|
|
|
|
// No file content is ever loaded into memory.
|
|
|
|
|
let file_management_service = &state.applications.file_management_service;
|
|
|
|
|
file_management_service
|
|
|
|
|
.copy_file(&file.id, target_folder_id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to copy file: {}", e)))?;
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-04-04 21:31:41 +02:00
|
|
|
* Handles LOCK requests to lock resources.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler processes WebDAV LOCK requests according to RFC 4918,
|
|
|
|
|
* creating a lock on a file or folder.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @param req The HTTP request containing the LOCK XML body
|
|
|
|
|
* @return XML response with lock information
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_lock(
|
2026-02-10 19:26:28 +01:00
|
|
|
_state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let user = {
|
2026-02-14 01:29:34 +01:00
|
|
|
let user_ref = req
|
|
|
|
|
.extensions()
|
|
|
|
|
.get::<CurrentUser>()
|
|
|
|
|
.ok_or_else(|| AppError::unauthorized("Authentication required"))?;
|
2025-04-04 21:31:41 +02:00
|
|
|
user_ref.clone()
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get the headers that we need
|
2026-02-14 01:29:34 +01:00
|
|
|
let depth = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("Depth")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.unwrap_or("infinity")
|
|
|
|
|
.to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
let timeout = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("Timeout")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.map(|s| s.to_string());
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
let if_header_value = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("If")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.map(|s| s.to_string());
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Extract the body separately to avoid borrow issues
|
|
|
|
|
let body_bytes = {
|
|
|
|
|
// Convert the request into a body
|
|
|
|
|
let body = req.into_body();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-22 23:28:03 +01:00
|
|
|
// Read request body (LOCK is XML, 1 MB is more than enough)
|
|
|
|
|
body::to_bytes(body, MAX_XML_BODY)
|
2025-04-04 21:31:41 +02:00
|
|
|
.await
|
2026-02-14 01:29:34 +01:00
|
|
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?
|
2025-04-04 21:31:41 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Check if this is a lock refresh (If header with a lock token)
|
|
|
|
|
if let Some(if_header) = if_header_value {
|
|
|
|
|
// This is a lock refresh request
|
|
|
|
|
// Extract lock token from If header
|
|
|
|
|
let token = if_header
|
|
|
|
|
.trim()
|
|
|
|
|
.trim_start_matches("(<")
|
|
|
|
|
.trim_end_matches(">)")
|
|
|
|
|
.to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// In a full implementation, we would look up the lock in a database
|
|
|
|
|
// and refresh its timeout. For now, just respond as if we did.
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate lock token and owner (for a real implementation, we'd store these)
|
|
|
|
|
let lock_info = LockInfo {
|
|
|
|
|
token,
|
|
|
|
|
owner: Some(user.id.clone()),
|
|
|
|
|
depth: depth.to_string(),
|
|
|
|
|
timeout,
|
|
|
|
|
scope: LockScope::Exclusive, // Default to exclusive
|
|
|
|
|
type_: LockType::Write, // Default to write
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response
|
|
|
|
|
let href = format!("/webdav/{}", path);
|
|
|
|
|
let mut response_body = Vec::new();
|
2026-02-14 01:29:34 +01:00
|
|
|
WebDavAdapter::generate_lock_response(&mut response_body, &lock_info, &href).map_err(
|
|
|
|
|
|e| AppError::internal_error(format!("Failed to generate LOCK response: {}", e)),
|
|
|
|
|
)?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.header(HEADER_LOCK_TOKEN, format!("<{}>", lock_info.token))
|
|
|
|
|
.body(Body::from(response_body))
|
|
|
|
|
.unwrap())
|
|
|
|
|
} else if !body_bytes.is_empty() {
|
|
|
|
|
// Parse lock request
|
2026-02-14 01:29:34 +01:00
|
|
|
let (scope, type_, owner) = WebDavAdapter::parse_lockinfo(body_bytes.reader())
|
|
|
|
|
.map_err(|e| AppError::bad_request(format!("Failed to parse LOCK request: {}", e)))?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate lock token and owner (for a real implementation, we'd store these)
|
|
|
|
|
let token = format!("opaquelocktoken:{}", Uuid::new_v4());
|
|
|
|
|
let lock_info = LockInfo {
|
|
|
|
|
token,
|
|
|
|
|
owner: owner.or(Some(user.id.clone())),
|
|
|
|
|
depth: depth.to_string(),
|
|
|
|
|
timeout,
|
|
|
|
|
scope,
|
|
|
|
|
type_,
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response
|
|
|
|
|
let href = format!("/webdav/{}", path);
|
|
|
|
|
let mut response_body = Vec::new();
|
2026-02-14 01:29:34 +01:00
|
|
|
WebDavAdapter::generate_lock_response(&mut response_body, &lock_info, &href).map_err(
|
|
|
|
|
|e| AppError::internal_error(format!("Failed to generate LOCK response: {}", e)),
|
|
|
|
|
)?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.header(HEADER_LOCK_TOKEN, format!("<{}>", lock_info.token))
|
|
|
|
|
.body(Body::from(response_body))
|
|
|
|
|
.unwrap())
|
|
|
|
|
} else {
|
|
|
|
|
Err(AppError::bad_request("Invalid LOCK request"))
|
|
|
|
|
}
|
2025-04-04 01:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-04-04 21:31:41 +02:00
|
|
|
* Handles UNLOCK requests to remove locks from resources.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* This handler processes WebDAV UNLOCK requests according to RFC 4918,
|
|
|
|
|
* removing a lock from a file or folder.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 21:31:41 +02:00
|
|
|
* @param state The application state containing service dependencies
|
|
|
|
|
* @param user The authenticated user information
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @param req The HTTP request containing the lock token
|
|
|
|
|
* @return HTTP response indicating success
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_unlock(
|
2026-02-10 19:26:28 +01:00
|
|
|
_state: AppState,
|
2025-04-04 21:31:41 +02:00
|
|
|
req: Request<Body>,
|
2026-02-10 19:26:28 +01:00
|
|
|
_path: String,
|
2025-04-04 21:31:41 +02:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let _user = {
|
2026-02-14 01:29:34 +01:00
|
|
|
let user_ref = req
|
|
|
|
|
.extensions()
|
|
|
|
|
.get::<CurrentUser>()
|
|
|
|
|
.ok_or_else(|| AppError::unauthorized("Authentication required"))?;
|
2025-04-04 21:31:41 +02:00
|
|
|
user_ref.clone()
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Get lock token from Lock-Token header
|
2026-02-14 01:29:34 +01:00
|
|
|
let lock_token = req
|
|
|
|
|
.headers()
|
2025-04-04 21:31:41 +02:00
|
|
|
.get("Lock-Token")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.ok_or_else(|| AppError::bad_request("Lock-Token header required"))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Extract token from header value (format: <token>)
|
2025-04-10 01:43:25 +02:00
|
|
|
let _token = lock_token
|
2025-04-04 21:31:41 +02:00
|
|
|
.trim()
|
|
|
|
|
.trim_start_matches('<')
|
|
|
|
|
.trim_end_matches('>')
|
|
|
|
|
.to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// In a full implementation, we would look up the lock in a database
|
|
|
|
|
// and remove it. For now, just respond as if we did.
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|