340 lines
11 KiB
Rust
340 lines
11 KiB
Rust
/**
|
|
* 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<Arc<AppState>> {
|
|
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<Arc<AppState>>,
|
|
Path(_path): Path<String>,
|
|
) -> 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<Arc<AppState>>,
|
|
Extension(user): Extension<CurrentUser>,
|
|
Path(path): Path<String>,
|
|
request: Request<axum::body::Body>,
|
|
) -> Result<Response, AppError> {
|
|
// 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<Arc<AppState>>,
|
|
Extension(user): Extension<CurrentUser>,
|
|
Path(path): Path<String>,
|
|
) -> Result<Response, AppError> {
|
|
// 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<Arc<AppState>>,
|
|
Extension(user): Extension<CurrentUser>,
|
|
Path(path): Path<String>,
|
|
) -> Result<Response, AppError> {
|
|
// 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<Response, AppError> {
|
|
todo!()
|
|
}
|
|
|
|
/**
|
|
* Handles PROPPATCH requests to update resource properties.
|
|
* Processes property updates and removals.
|
|
*/
|
|
async fn handle_proppatch(
|
|
// ...
|
|
) -> Result<Response, AppError> {
|
|
todo!()
|
|
}
|
|
|
|
/**
|
|
* Handles MKCOL requests to create directories.
|
|
* Creates a new collection (directory) at the specified path.
|
|
*/
|
|
async fn handle_mkcol(
|
|
// ...
|
|
) -> Result<Response, AppError> {
|
|
todo!()
|
|
}
|
|
|
|
/**
|
|
* Handles DELETE requests to remove resources.
|
|
* Deletes the specified file or recursively deletes a directory.
|
|
*/
|
|
async fn handle_delete(
|
|
// ...
|
|
) -> Result<Response, AppError> {
|
|
todo!()
|
|
}
|
|
|
|
/**
|
|
* Handles COPY requests to duplicate resources.
|
|
* Copies a file or recursively copies a directory.
|
|
*/
|
|
async fn handle_copy(
|
|
// ...
|
|
) -> Result<Response, AppError> {
|
|
todo!()
|
|
}
|
|
|
|
/**
|
|
* Handles MOVE requests to relocate resources.
|
|
* Moves or renames a file or directory.
|
|
*/
|
|
async fn handle_move(
|
|
// ...
|
|
) -> Result<Response, AppError> {
|
|
todo!()
|
|
}
|
|
|
|
/**
|
|
* Handles LOCK requests for concurrency control.
|
|
* Locks a resource for exclusive access by a client.
|
|
*/
|
|
async fn handle_lock(
|
|
// ...
|
|
) -> Result<Response, AppError> {
|
|
todo!()
|
|
}
|
|
|
|
/**
|
|
* Handles UNLOCK requests to release locks.
|
|
* Releases a previously acquired lock on a resource.
|
|
*/
|
|
async fn handle_unlock(
|
|
// ...
|
|
) -> Result<Response, AppError> {
|
|
todo!()
|
|
} |