/** * WebDAV Handler Module * * 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. * * The WebDAV protocol extends HTTP to provide file system-like functionality, enabling: * - File/folder listing (PROPFIND) * - Creation of collections/directories (MKCOL) * - Retrieving and updating resources (GET, PUT) * - Moving and copying resources (MOVE, COPY) * - Resource locking for concurrency control (LOCK, UNLOCK) * * This implementation leverages OxiCloud's existing file and folder services * through the application's port interfaces. */ use std::sync::Arc; use axum::{ Router, routing::get, extract::{Path, State, Request, Extension}, http::StatusCode, response::Response, }; use http::{Method, header}; use crate::common::di::AppState; use crate::interfaces::middleware::auth::CurrentUser; use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseCase}; use crate::application::ports::folder_ports::FolderUseCase; use crate::common::errors::AppError; use crate::application::adapters::webdav_adapter::WebDavAdapter; /** * Creates and returns the WebDAV router with all required endpoints. * * This function sets up all WebDAV method handlers following RFC 4918, * mapping HTTP methods to appropriate WebDAV operations. * * @return Router configured with WebDAV endpoints */ pub fn webdav_routes() -> Router> { Router::new() // Standard HTTP methods used in WebDAV .route("/webdav/*path", get(handle_get)) .route("/webdav/*path", axum::routing::head(handle_head)) .route("/webdav/*path", axum::routing::put(handle_put)) .route("/webdav/*path", axum::routing::delete(handle_delete)) // WebDAV-specific methods .route_with_tsr("/webdav/*path", axum::routing::on( Method::OPTIONS, handle_options, Method::PROPFIND, handle_propfind, Method::PROPPATCH, handle_proppatch, Method::MKCOL, handle_mkcol, Method::COPY, handle_copy, Method::MOVE, handle_move, Method::LOCK, handle_lock, Method::UNLOCK, handle_unlock, )) } /** * Handles OPTIONS requests to advertise WebDAV capabilities. * * This handler responds with the DAV header indicating WebDAV compliance * level and the methods supported by this WebDAV server. * * @param state The application state containing service dependencies * @param path The requested resource path * @return HTTP response with appropriate WebDAV headers */ async fn handle_options( State(_state): State>, Path(_path): Path, ) -> Response { Response::builder() .status(StatusCode::OK) .header(header::DAV, "1, 2") // Class 1 and 2 WebDAV support .header(header::ALLOW, "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK") .body(axum::body::Body::empty()) .unwrap() } /** * Handles PROPFIND requests to retrieve resource properties. * * This handler processes WebDAV PROPFIND requests, which are used to retrieve * properties for one or more resources. It supports different depths (0, 1, infinity) * and can return all properties or a specified subset based on the request. * * @param state The application state containing service dependencies * @param user The authenticated user making the request * @param path The requested resource path * @param request The full HTTP request containing headers and body * @return XML response with requested properties */ async fn handle_propfind( State(state): State>, Extension(user): Extension, Path(path): Path, request: Request, ) -> Result { // Extract depth header (0, 1, or infinity) let depth = request .headers() .get(header::from_str("Depth").unwrap()) .and_then(|v| v.to_str().ok()) .unwrap_or("infinity"); // Read request body to determine which properties are requested let body_bytes = hyper::body::to_bytes(request.into_body()).await .map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?; // Use the adapter to parse the PROPFIND request let prop_find_request = WebDavAdapter::parse_propfind(&body_bytes[..]) .map_err(|e| AppError::bad_request(format!("Invalid PROPFIND request: {}", e)))?; // Determine if the path is a file or directory let is_file = if path.ends_with('/') { false } else { // Check if path exists as a file let file_service = state.file_service.as_ref() .ok_or_else(|| AppError::internal_error("File service not configured"))?; match file_service.file_retrieval_service.get_file_by_path(&path, &user.id).await { Ok(_) => true, Err(_) => false, } }; if is_file { // Handle PROPFIND for a file let file_service = state.file_service.as_ref() .ok_or_else(|| AppError::internal_error("File service not configured"))?; let file = file_service.file_retrieval_service.get_file_by_path(&path, &user.id).await?; // Generate XML response let mut xml_buffer = Vec::new(); WebDavAdapter::generate_propfind_response_for_file( &mut xml_buffer, &file, &prop_find_request, depth, &format!("/webdav/{}", path), ).map_err(|e| AppError::internal_error(format!("Failed to generate XML response: {}", e)))?; // Return response with appropriate headers Ok(Response::builder() .status(StatusCode::MULTI_STATUS) .header(header::CONTENT_TYPE, "application/xml; charset=utf-8") .body(axum::body::Body::from(xml_buffer)) .unwrap()) } else { // Handle PROPFIND for a directory let folder_service = state.folder_service.as_ref() .ok_or_else(|| AppError::internal_error("Folder service not configured"))?; let folder_path = if path.ends_with('/') { path.clone() } else { format!("{}/", path) }; let folder = folder_service.get_folder_by_path(&folder_path, &user.id).await?; // Fetch children if depth > 0 let (files, folders) = if depth == "0" { (Vec::new(), Vec::new()) } else { let files = file_service.file_retrieval_service.get_files_in_folder( Some(&folder.id.to_string()), &user.id, ).await?; let folders = folder_service.get_subfolders( Some(&folder.id.to_string()), &user.id, ).await?; (files, folders) }; // Generate XML response let mut xml_buffer = Vec::new(); WebDavAdapter::generate_propfind_response( &mut xml_buffer, Some(&folder), &files, &folders, &prop_find_request, depth, &format!("/webdav/{}", folder_path), ).map_err(|e| AppError::internal_error(format!("Failed to generate XML response: {}", e)))?; // Return response with appropriate headers Ok(Response::builder() .status(StatusCode::MULTI_STATUS) .header(header::CONTENT_TYPE, "application/xml; charset=utf-8") .body(axum::body::Body::from(xml_buffer)) .unwrap()) } } /** * Handles GET requests to retrieve file contents. * * This handler streams file contents to the client with appropriate * content type and other metadata headers. * * @param state The application state containing service dependencies * @param user The authenticated user making the request * @param path The requested file path * @return Streaming response with file contents */ async fn handle_get( State(state): State>, Extension(user): Extension, Path(path): Path, ) -> Result { // Ensure this is a file request (not a directory) if path.ends_with('/') { return Err(AppError::bad_request("Cannot GET a directory")); } let file_service = state.file_service.as_ref() .ok_or_else(|| AppError::internal_error("File service not configured"))?; // Get file metadata let file = file_service.file_retrieval_service.get_file_by_path(&path, &user.id).await?; // Stream file content let stream = file_service.file_retrieval_service.get_file_stream(&file.id, &user.id).await?; // Return streamed response with appropriate headers Ok(Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, file.mime_type) .header(header::CONTENT_LENGTH, file.size.to_string()) .header(header::ETAG, format!("\"{}\"", file.id)) .header(header::LAST_MODIFIED, file.updated_at.to_rfc2822()) .body(axum::body::Body::from_stream(stream)) .unwrap()) } // Implement remaining WebDAV method handlers... /** * Handles HEAD requests to retrieve file metadata without content. * Similar to GET but without returning the file body. */ async fn handle_head( State(state): State>, Extension(user): Extension, Path(path): Path, ) -> Result { // Implementation similar to handle_get but without body // ... todo!() } /** * Handles PUT requests to create or update files. * Streams the request body to create or replace a file. */ async fn handle_put( // ... ) -> Result { todo!() } /** * Handles PROPPATCH requests to update resource properties. * Processes property updates and removals. */ async fn handle_proppatch( // ... ) -> Result { todo!() } /** * Handles MKCOL requests to create directories. * Creates a new collection (directory) at the specified path. */ async fn handle_mkcol( // ... ) -> Result { todo!() } /** * Handles DELETE requests to remove resources. * Deletes the specified file or recursively deletes a directory. */ async fn handle_delete( // ... ) -> Result { todo!() } /** * Handles COPY requests to duplicate resources. * Copies a file or recursively copies a directory. */ async fn handle_copy( // ... ) -> Result { todo!() } /** * Handles MOVE requests to relocate resources. * Moves or renames a file or directory. */ async fn handle_move( // ... ) -> Result { todo!() } /** * Handles LOCK requests for concurrency control. * Locks a resource for exclusive access by a client. */ async fn handle_lock( // ... ) -> Result { todo!() } /** * Handles UNLOCK requests to release locks. * Releases a previously acquired lock on a resource. */ async fn handle_unlock( // ... ) -> Result { todo!() }