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,
|
|
|
|
|
};
|
2026-02-24 15:11:56 +01:00
|
|
|
use bytes::{Buf, Bytes};
|
2026-02-14 01:29:34 +01:00
|
|
|
use chrono::Utc;
|
2026-02-24 15:11:56 +01:00
|
|
|
use quick_xml::Writer;
|
2026-02-14 01:29:34 +01:00
|
|
|
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::{
|
2026-03-03 11:49:52 +01:00
|
|
|
LockInfo, PropFindRequest, WebDavAdapter,
|
2026-02-14 01:29:34 +01:00
|
|
|
};
|
2026-02-24 15:11:56 +01:00
|
|
|
use crate::application::dtos::file_dto::FileDto;
|
2025-04-04 21:31:41 +02:00
|
|
|
use crate::application::dtos::folder_dto::FolderDto;
|
2026-02-24 15:11:56 +01:00
|
|
|
use crate::application::ports::file_ports::FileRetrievalUseCase;
|
|
|
|
|
use crate::application::ports::inbound::FolderUseCase;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::common::di::AppState;
|
2026-03-02 23:40:48 +01:00
|
|
|
use crate::infrastructure::services::path_resolver_service::ResolvedResource;
|
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;
|
2026-03-03 15:36:42 +00:00
|
|
|
use crate::application::services::file_retrieval_service::FileRetrievalService;
|
|
|
|
|
use crate::application::services::folder_service::FolderService;
|
2026-03-03 01:49:18 +01:00
|
|
|
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
|
2026-02-24 15:11:56 +01:00
|
|
|
use std::sync::Arc;
|
2026-03-03 15:36:42 +00:00
|
|
|
use crate::application::ports::file_ports::{FileManagementUseCase, FileUploadUseCase};
|
2025-04-04 21:31:41 +02:00
|
|
|
|
2026-03-01 20:34:12 +01:00
|
|
|
/// Characters that MUST NOT be percent-encoded inside a URI path segment.
|
|
|
|
|
/// RFC 3986 §3.3 pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
|
|
|
|
/// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
|
|
|
|
/// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
|
|
|
|
|
const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
|
|
|
|
|
.remove(b'-')
|
|
|
|
|
.remove(b'.')
|
|
|
|
|
.remove(b'_')
|
|
|
|
|
.remove(b'~')
|
|
|
|
|
.remove(b'!')
|
|
|
|
|
.remove(b'$')
|
|
|
|
|
.remove(b'&')
|
|
|
|
|
.remove(b'\'')
|
|
|
|
|
.remove(b'(')
|
|
|
|
|
.remove(b')')
|
|
|
|
|
.remove(b'*')
|
|
|
|
|
.remove(b'+')
|
|
|
|
|
.remove(b',')
|
|
|
|
|
.remove(b';')
|
|
|
|
|
.remove(b'=')
|
|
|
|
|
.remove(b':')
|
|
|
|
|
.remove(b'@');
|
|
|
|
|
|
|
|
|
|
/// Percent-encode a single URI path segment (folder/file name).
|
|
|
|
|
fn encode_path_segment(segment: &str) -> String {
|
|
|
|
|
utf8_percent_encode(segment, PATH_SEGMENT_ENCODE_SET).to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Percent-encode a full slash-separated path, encoding each segment individually.
|
|
|
|
|
pub(crate) fn encode_uri_path(path: &str) -> String {
|
|
|
|
|
path.split('/')
|
|
|
|
|
.map(|seg| encode_path_segment(seg))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("/")
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
/// Batch size for streaming PROPFIND — files and folders are fetched in pages
|
|
|
|
|
/// of this size to keep memory constant regardless of folder contents.
|
|
|
|
|
const PROPFIND_BATCH_SIZE: i64 = 500;
|
|
|
|
|
|
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
|
|
|
|
|
*/
|
2026-02-24 15:11:56 +01:00
|
|
|
pub fn webdav_routes() -> Router<Arc<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))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 20:34:12 +01:00
|
|
|
/// Extract the resource path from the request URI, stripping the `/webdav/` prefix
|
|
|
|
|
/// and percent-decoding the result so that folder/file names with spaces and
|
|
|
|
|
/// special characters match the values stored in the database.
|
2026-02-10 19:26:28 +01:00
|
|
|
fn extract_webdav_path(uri: &axum::http::Uri) -> String {
|
|
|
|
|
let raw = uri.path();
|
2026-03-01 20:34:12 +01:00
|
|
|
let encoded = if let Some(rest) = raw.strip_prefix("/webdav/") {
|
|
|
|
|
rest.trim_end_matches('/')
|
2026-02-10 19:26:28 +01:00
|
|
|
} else if raw == "/webdav" {
|
2026-03-01 20:34:12 +01:00
|
|
|
""
|
2026-02-10 19:26:28 +01:00
|
|
|
} else {
|
|
|
|
|
// Fallback: split-based extraction
|
2026-03-01 20:34:12 +01:00
|
|
|
let trimmed = raw.strip_prefix('/').unwrap_or(raw);
|
|
|
|
|
trimmed.trim_end_matches('/')
|
|
|
|
|
};
|
|
|
|
|
// Decode percent-encoded characters (e.g. %20 → space)
|
2026-03-03 01:49:18 +01:00
|
|
|
percent_decode_str(encoded).decode_utf8_lossy().into_owned()
|
2026-02-10 19:26:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_webdav_methods_root(
|
2026-02-24 15:11:56 +01:00
|
|
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
2026-02-10 19:26:28 +01:00
|
|
|
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-24 15:11:56 +01:00
|
|
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
2026-02-10 19:26:28 +01:00
|
|
|
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(
|
2026-02-24 15:11:56 +01:00
|
|
|
state: Arc<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.
|
2026-02-24 15:11:56 +01:00
|
|
|
*
|
|
|
|
|
* **Security hardening (Sol.2):** `Depth: infinity` is rejected with
|
|
|
|
|
* `403 Forbidden` and the RFC 4918 `propfind-finite-depth` precondition
|
|
|
|
|
* error body. The default depth when the header is absent is `1`.
|
|
|
|
|
*
|
|
|
|
|
* **Streaming response (Sol.3):** For `Depth: 1`, files and sub-folders
|
|
|
|
|
* are fetched in batches of `PROPFIND_BATCH_SIZE` and the XML response
|
|
|
|
|
* is written incrementally to a streaming body. Memory usage is O(batch)
|
|
|
|
|
* regardless of how many children the folder contains.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-04-04 01:48:55 +02:00
|
|
|
* @param state The application state containing service dependencies
|
2026-02-24 15:11:56 +01:00
|
|
|
* @param req The HTTP request containing the PROPFIND XML body
|
|
|
|
|
* @param path The requested resource path
|
|
|
|
|
* @return 207 Multi-Status XML response with resource properties
|
2025-04-04 01:48:55 +02:00
|
|
|
*/
|
|
|
|
|
async fn handle_propfind(
|
2026-02-24 15:11:56 +01:00
|
|
|
state: Arc<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-24 15:11:56 +01:00
|
|
|
// ── 1. Extract and validate Depth header ─────────────────────
|
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())
|
2026-02-24 15:11:56 +01:00
|
|
|
.unwrap_or("1");
|
|
|
|
|
|
|
|
|
|
// RFC 4918 §9.1: servers MAY reject Depth:infinity with 403
|
|
|
|
|
if depth == "infinity" {
|
|
|
|
|
let body = r#"<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
|
<D:error xmlns:D="DAV:">
|
|
|
|
|
<D:propfind-finite-depth/>
|
|
|
|
|
</D:error>"#;
|
|
|
|
|
return Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::FORBIDDEN)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(body))
|
|
|
|
|
.unwrap());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
// Normalize: anything other than "0" or "1" is treated as "0"
|
|
|
|
|
let depth = match depth {
|
|
|
|
|
"0" | "1" => depth,
|
|
|
|
|
_ => "0",
|
|
|
|
|
};
|
|
|
|
|
let depth_owned = depth.to_string();
|
|
|
|
|
|
|
|
|
|
// ── 2. Authenticate ──────────────────────────────────────────
|
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
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
// ── 3. Parse PROPFIND XML body ───────────────────────────────
|
2025-04-04 21:31:41 +02:00
|
|
|
let body_bytes = {
|
|
|
|
|
let body = req.into_body();
|
2026-02-22 23:28:03 +01:00
|
|
|
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
|
|
|
let propfind_request = if body_bytes.is_empty() {
|
|
|
|
|
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
|
|
|
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
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
// ── 4. Services ──────────────────────────────────────────────
|
|
|
|
|
let folder_service = state.applications.folder_service.clone();
|
|
|
|
|
let file_retrieval_service = state.applications.file_retrieval_service.clone();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
let base_href = if path.is_empty() || path == "/" {
|
|
|
|
|
"/webdav/".to_string()
|
|
|
|
|
} else {
|
2026-03-01 20:34:12 +01:00
|
|
|
format!("/webdav/{}/", encode_uri_path(&path))
|
2026-02-24 15:11:56 +01:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
// ── 5. Determine target resource ─────────────────────────────
|
2025-04-04 21:31:41 +02:00
|
|
|
if path.is_empty() || path == "/" {
|
2026-02-24 15:11:56 +01:00
|
|
|
// Root folder
|
2025-04-04 21:31:41 +02:00
|
|
|
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
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
return build_streaming_propfind_response(
|
|
|
|
|
root_folder,
|
|
|
|
|
None, // folder_id = None → root children
|
|
|
|
|
&depth_owned,
|
2025-04-04 21:31:41 +02:00
|
|
|
&base_href,
|
2026-02-24 15:11:56 +01:00
|
|
|
propfind_request,
|
|
|
|
|
folder_service,
|
|
|
|
|
file_retrieval_service,
|
2026-02-14 01:29:34 +01:00
|
|
|
)
|
2026-02-24 15:11:56 +01:00
|
|
|
.await;
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
// Single-query path resolution: folder OR file in one DB round-trip
|
|
|
|
|
if let Some(resolver) = &state.path_resolver {
|
|
|
|
|
match resolver.resolve_path(&path).await {
|
|
|
|
|
Ok(ResolvedResource::Folder(folder)) => {
|
|
|
|
|
let folder_id = folder.id.clone();
|
|
|
|
|
return build_streaming_propfind_response(
|
|
|
|
|
folder,
|
|
|
|
|
Some(folder_id),
|
|
|
|
|
&depth_owned,
|
|
|
|
|
&base_href,
|
|
|
|
|
propfind_request,
|
|
|
|
|
folder_service,
|
|
|
|
|
file_retrieval_service,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
Ok(ResolvedResource::File(file)) => {
|
|
|
|
|
let mut buf = Vec::with_capacity(1024);
|
|
|
|
|
{
|
|
|
|
|
let mut xml_writer = Writer::new(&mut buf);
|
|
|
|
|
WebDavAdapter::write_multistatus_start(&mut xml_writer)
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
2026-03-03 01:49:18 +01:00
|
|
|
WebDavAdapter::write_file_entry(
|
|
|
|
|
&mut xml_writer,
|
|
|
|
|
&file,
|
|
|
|
|
&propfind_request,
|
|
|
|
|
&base_href,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
2026-03-02 23:40:48 +01:00
|
|
|
WebDavAdapter::write_multistatus_end(&mut xml_writer)
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
|
|
|
|
}
|
|
|
|
|
return Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(buf))
|
|
|
|
|
.unwrap());
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: legacy double-query path when PathResolver is unavailable
|
|
|
|
|
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
|
|
|
|
|
let folder_id = folder.id.clone();
|
|
|
|
|
return build_streaming_propfind_response(
|
|
|
|
|
folder,
|
|
|
|
|
Some(folder_id),
|
|
|
|
|
&depth_owned,
|
|
|
|
|
&base_href,
|
|
|
|
|
propfind_request,
|
|
|
|
|
folder_service,
|
|
|
|
|
file_retrieval_service,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
if let Ok(file) = file_retrieval_service.get_file_by_path(&path).await {
|
|
|
|
|
let mut buf = Vec::with_capacity(1024);
|
|
|
|
|
{
|
|
|
|
|
let mut xml_writer = Writer::new(&mut buf);
|
|
|
|
|
WebDavAdapter::write_multistatus_start(&mut xml_writer)
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
2026-03-03 01:49:18 +01:00
|
|
|
WebDavAdapter::write_file_entry(
|
|
|
|
|
&mut xml_writer,
|
|
|
|
|
&file,
|
|
|
|
|
&propfind_request,
|
|
|
|
|
&base_href,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
2026-03-02 23:40:48 +01:00
|
|
|
WebDavAdapter::write_multistatus_end(&mut xml_writer)
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
|
|
|
|
}
|
|
|
|
|
return Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(buf))
|
|
|
|
|
.unwrap());
|
2026-02-24 15:11:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-24 15:11:56 +01:00
|
|
|
Err(AppError::not_found(format!("Resource not found: {}", path)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Builds a streaming 207 Multi-Status PROPFIND response.
|
|
|
|
|
///
|
|
|
|
|
/// The XML is written incrementally: first the folder itself, then children
|
|
|
|
|
/// (sub-folders and files) are fetched in batches of `PROPFIND_BATCH_SIZE`.
|
|
|
|
|
/// Each batch is serialised to XML and sent as a chunk, so memory stays
|
|
|
|
|
/// constant at O(batch_size) regardless of the total number of children.
|
|
|
|
|
async fn build_streaming_propfind_response(
|
|
|
|
|
folder: FolderDto,
|
|
|
|
|
folder_id: Option<String>,
|
|
|
|
|
depth: &str,
|
|
|
|
|
base_href: &str,
|
|
|
|
|
propfind_request: PropFindRequest,
|
2026-03-03 15:36:42 +00:00
|
|
|
folder_service: std::sync::Arc<FolderService>,
|
|
|
|
|
file_retrieval_service: std::sync::Arc<FileRetrievalService>,
|
2026-02-24 15:11:56 +01:00
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let depth = depth.to_string();
|
|
|
|
|
let base_href = base_href.to_string();
|
|
|
|
|
let propfind_request = Arc::new(propfind_request);
|
|
|
|
|
|
|
|
|
|
let stream = async_stream::try_stream! {
|
|
|
|
|
// ── XML header + <D:multistatus> + folder entry ──────────
|
|
|
|
|
let mut buf = Vec::with_capacity(4096);
|
|
|
|
|
{
|
|
|
|
|
let mut w = Writer::new(&mut buf);
|
|
|
|
|
WebDavAdapter::write_multistatus_start(&mut w)
|
2026-02-25 10:28:34 +01:00
|
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
2026-02-24 15:11:56 +01:00
|
|
|
WebDavAdapter::write_folder_entry(&mut w, &folder, &propfind_request, &base_href)
|
2026-02-25 10:28:34 +01:00
|
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
2026-02-24 15:11:56 +01:00
|
|
|
}
|
|
|
|
|
yield Bytes::from(buf);
|
|
|
|
|
|
|
|
|
|
// ── Children (only if Depth == 1) ────────────────────────
|
|
|
|
|
if depth == "1" {
|
|
|
|
|
let pagination = crate::application::dtos::pagination::PaginationRequestDto {
|
|
|
|
|
page: 0,
|
|
|
|
|
page_size: PROPFIND_BATCH_SIZE as usize,
|
|
|
|
|
};
|
|
|
|
|
let fid_ref = folder_id.as_deref();
|
|
|
|
|
|
|
|
|
|
// Stream sub-folders in pages
|
|
|
|
|
let mut page = 0usize;
|
|
|
|
|
loop {
|
|
|
|
|
let pag = crate::application::dtos::pagination::PaginationRequestDto {
|
|
|
|
|
page,
|
|
|
|
|
page_size: pagination.page_size,
|
|
|
|
|
};
|
|
|
|
|
let result = folder_service
|
|
|
|
|
.list_folders_paginated(fid_ref, &pag)
|
|
|
|
|
.await
|
2026-02-25 10:28:34 +01:00
|
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
2026-02-24 15:11:56 +01:00
|
|
|
|
|
|
|
|
if result.items.is_empty() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut chunk = Vec::with_capacity(result.items.len() * 800);
|
|
|
|
|
{
|
|
|
|
|
let mut w = Writer::new(&mut chunk);
|
|
|
|
|
for subfolder in &result.items {
|
2026-03-01 20:34:12 +01:00
|
|
|
let href = format!("{}{}/", base_href, encode_path_segment(&subfolder.name));
|
2026-02-24 15:11:56 +01:00
|
|
|
WebDavAdapter::write_folder_entry(&mut w, subfolder, &propfind_request, &href)
|
2026-02-25 10:28:34 +01:00
|
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
2026-02-24 15:11:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let has_more = result.pagination.has_next;
|
|
|
|
|
yield Bytes::from(chunk);
|
|
|
|
|
|
|
|
|
|
if !has_more {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
page += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stream files in pages
|
|
|
|
|
let mut offset: i64 = 0;
|
|
|
|
|
loop {
|
|
|
|
|
let batch: Vec<FileDto> = file_retrieval_service
|
|
|
|
|
.list_files_batch(fid_ref, offset, PROPFIND_BATCH_SIZE)
|
|
|
|
|
.await
|
2026-02-25 10:28:34 +01:00
|
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
2026-02-24 15:11:56 +01:00
|
|
|
|
|
|
|
|
if batch.is_empty() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let batch_len = batch.len();
|
|
|
|
|
let mut chunk = Vec::with_capacity(batch_len * 800);
|
|
|
|
|
{
|
|
|
|
|
let mut w = Writer::new(&mut chunk);
|
|
|
|
|
for file in &batch {
|
2026-03-01 20:34:12 +01:00
|
|
|
let href = format!("{}{}", base_href, encode_path_segment(&file.name));
|
2026-02-24 15:11:56 +01:00
|
|
|
WebDavAdapter::write_file_entry(&mut w, file, &propfind_request, &href)
|
2026-02-25 10:28:34 +01:00
|
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
2026-02-24 15:11:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
yield Bytes::from(chunk);
|
|
|
|
|
|
|
|
|
|
if (batch_len as i64) < PROPFIND_BATCH_SIZE {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
offset += batch_len as i64;
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-24 15:11:56 +01:00
|
|
|
|
|
|
|
|
// ── Close </D:multistatus> ───────────────────────────────
|
|
|
|
|
let mut buf = Vec::with_capacity(32);
|
|
|
|
|
{
|
|
|
|
|
let mut w = Writer::new(&mut buf);
|
|
|
|
|
WebDavAdapter::write_multistatus_end(&mut w)
|
2026-02-25 10:28:34 +01:00
|
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
2026-02-24 15:11:56 +01:00
|
|
|
}
|
|
|
|
|
yield Bytes::from(buf);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use futures::TryStreamExt;
|
2026-02-25 10:28:34 +01:00
|
|
|
let stream = stream
|
|
|
|
|
.map_err(|e: std::io::Error| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) });
|
2026-02-24 15:11:56 +01:00
|
|
|
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from_stream(stream))
|
|
|
|
|
.unwrap())
|
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-24 15:11:56 +01:00
|
|
|
_state: Arc<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-25 10:28:34 +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
|
2026-03-01 20:34:12 +01:00
|
|
|
let href = format!("/webdav/{}", encode_uri_path(&path));
|
2025-04-04 21:31:41 +02:00
|
|
|
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-24 15:11:56 +01:00
|
|
|
state: Arc<AppState>,
|
2026-02-10 19:26:28 +01:00
|
|
|
_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(
|
2026-02-24 15:11:56 +01:00
|
|
|
state: Arc<AppState>,
|
2026-02-10 19:26:28 +01:00
|
|
|
_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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
// Single-query path resolution
|
|
|
|
|
if let Some(resolver) = &state.path_resolver {
|
|
|
|
|
match resolver.resolve_path(&path).await {
|
|
|
|
|
Ok(ResolvedResource::Folder(folder)) => {
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
Ok(ResolvedResource::File(file)) => {
|
|
|
|
|
return Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, &file.mime_type)
|
|
|
|
|
.header(header::CONTENT_LENGTH, file.size)
|
|
|
|
|
.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());
|
|
|
|
|
}
|
|
|
|
|
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", path))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: legacy double-query path
|
2026-02-10 19:26:28 +01:00
|
|
|
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
|
2026-03-02 02:13:08 +01:00
|
|
|
* with incremental BLAKE3 hashing. Peak RAM usage is ~256 KB regardless
|
2026-02-22 23:28:03 +01:00
|
|
|
* 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-24 15:11:56 +01:00
|
|
|
state: Arc<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 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-23 00:17:40 +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-23 00:17:40 +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()
|
2026-02-23 00:17:40 +01:00
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to create temp file: {}", e)))?;
|
2026-02-22 23:28:03 +01:00
|
|
|
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
|
2026-02-23 00:17:40 +01:00
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to open temp file: {}", e)))?;
|
2026-02-22 23:28:03 +01:00
|
|
|
|
2026-03-02 02:13:08 +01:00
|
|
|
let mut hasher = blake3::Hasher::new();
|
2026-02-22 23:28:03 +01:00
|
|
|
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
|
2026-02-23 00:17:40 +01:00
|
|
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
2026-02-22 23:28:03 +01:00
|
|
|
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!(
|
2026-02-23 00:17:40 +01:00
|
|
|
"Upload exceeds maximum size of {} bytes",
|
2026-02-22 23:28:03 +01:00
|
|
|
max_upload
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
hasher.update(chunk);
|
2026-02-25 10:28:34 +01:00
|
|
|
file.write_all(chunk).await.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to write to temp file: {}", e))
|
|
|
|
|
})?;
|
2026-02-22 23:28:03 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-25 10:28:34 +01:00
|
|
|
file.flush()
|
|
|
|
|
.await
|
2026-02-23 00:17:40 +01:00
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to flush temp file: {}", e)))?;
|
2026-02-22 23:28:03 +01:00
|
|
|
drop(file);
|
|
|
|
|
|
2026-03-02 02:13:08 +01:00
|
|
|
let hash = hasher.finalize().to_hex().to_string();
|
2026-02-22 23:28:03 +01:00
|
|
|
|
|
|
|
|
// ── 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-23 00:17:40 +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-24 15:11:56 +01:00
|
|
|
state: Arc<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-24 15:11:56 +01:00
|
|
|
state: Arc<AppState>,
|
2026-02-10 19:26:28 +01:00
|
|
|
_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
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
// Single-query path resolution
|
|
|
|
|
if let Some(resolver) = &state.path_resolver {
|
|
|
|
|
match resolver.resolve_path(&path).await {
|
|
|
|
|
Ok(ResolvedResource::Folder(folder)) => {
|
|
|
|
|
let caller_id = folder.owner_id.as_deref().unwrap_or("webdav");
|
|
|
|
|
folder_service
|
|
|
|
|
.delete_folder(&folder.id, caller_id)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to delete folder: {}", e))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
|
|
|
|
Ok(ResolvedResource::File(file)) => {
|
|
|
|
|
file_management_service
|
|
|
|
|
.delete_file(&file.id)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to delete file: {}", e))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
|
|
|
|
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", path))),
|
|
|
|
|
}
|
2025-04-04 21:31:41 +02:00
|
|
|
} else {
|
2026-03-02 23:40:48 +01:00
|
|
|
// Fallback: legacy double-query path
|
|
|
|
|
let folder_result = folder_service.get_folder_by_path(&path).await;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
if let Ok(folder) = folder_result {
|
|
|
|
|
let caller_id = folder.owner_id.as_deref().unwrap_or("webdav");
|
|
|
|
|
folder_service
|
|
|
|
|
.delete_folder(&folder.id, caller_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
|
|
|
|
} else {
|
|
|
|
|
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-24 15:11:56 +01:00
|
|
|
state: Arc<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";
|
|
|
|
|
|
2026-03-01 20:34:12 +01:00
|
|
|
// Extract destination path from URL and percent-decode it
|
2025-04-04 21:31:41 +02:00
|
|
|
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
|
|
|
|
let after_prefix = &destination[webdav_prefix + 8..];
|
2026-03-01 20:34:12 +01:00
|
|
|
let trimmed = after_prefix.trim_end_matches('/');
|
|
|
|
|
percent_decode_str(trimmed).decode_utf8_lossy().into_owned()
|
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-03-02 23:40:48 +01:00
|
|
|
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
|
|
|
|
resolver.exists(&destination_path).await.unwrap_or(false)
|
|
|
|
|
} else {
|
2026-03-03 01:49:18 +01:00
|
|
|
folder_service
|
|
|
|
|
.get_folder_by_path(&destination_path)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok()
|
|
|
|
|
|| file_retrieval_service
|
|
|
|
|
.get_file_by_path(&destination_path)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok()
|
2026-03-02 23:40:48 +01:00
|
|
|
};
|
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
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
// Resolve source: single-query when PathResolver is available
|
|
|
|
|
if let Some(resolver) = &state.path_resolver {
|
|
|
|
|
match resolver.resolve_path(&source_path).await {
|
|
|
|
|
Ok(ResolvedResource::Folder(folder)) => {
|
|
|
|
|
let dest_folder_name = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
|
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
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),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
folder_service
|
|
|
|
|
.move_folder(
|
|
|
|
|
&folder.id,
|
|
|
|
|
move_dto,
|
|
|
|
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to move folder: {}", e))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
|
|
|
|
|
if folder.name != dest_folder_name {
|
|
|
|
|
let rename_dto = crate::application::dtos::folder_dto::RenameFolderDto {
|
|
|
|
|
name: dest_folder_name.to_string(),
|
|
|
|
|
};
|
|
|
|
|
folder_service
|
|
|
|
|
.rename_folder(
|
|
|
|
|
&folder.id,
|
|
|
|
|
rename_dto,
|
|
|
|
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
|
|
|
|
})?;
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
|
|
|
|
Ok(ResolvedResource::File(file)) => {
|
|
|
|
|
let dest_filename = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
|
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
|
|
|
|
let source_parent_path = if let Some(idx) = source_path.rfind('/') {
|
|
|
|
|
&source_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
if source_parent_path != dest_parent_path {
|
|
|
|
|
file_management_service
|
|
|
|
|
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to move file: {}", e))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
|
|
|
|
if file.name != dest_filename {
|
|
|
|
|
file_management_service
|
|
|
|
|
.rename_file(&file.id, dest_filename)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to rename file: {}", e))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-03 01:49:18 +01:00
|
|
|
Err(_) => {
|
|
|
|
|
return Err(AppError::not_found(format!(
|
|
|
|
|
"Resource not found: {}",
|
|
|
|
|
source_path
|
|
|
|
|
)));
|
|
|
|
|
}
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: legacy double-query path
|
|
|
|
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
|
|
|
|
|
|
|
|
|
if let Ok(folder) = folder_result {
|
|
|
|
|
let dest_folder_name = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
|
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
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),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-04 21:31:41 +02:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
folder_service
|
2026-03-02 23:40:48 +01:00
|
|
|
.move_folder(
|
2026-02-21 13:33:18 +01:00
|
|
|
&folder.id,
|
2026-03-02 23:40:48 +01:00
|
|
|
move_dto,
|
2026-02-21 13:33:18 +01:00
|
|
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
|
|
|
|
)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
2026-03-02 23:40:48 +01:00
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
if folder.name != dest_folder_name {
|
|
|
|
|
let rename_dto = crate::application::dtos::folder_dto::RenameFolderDto {
|
|
|
|
|
name: dest_folder_name.to_string(),
|
|
|
|
|
};
|
|
|
|
|
folder_service
|
|
|
|
|
.rename_folder(
|
|
|
|
|
&folder.id,
|
|
|
|
|
rename_dto,
|
|
|
|
|
folder.owner_id.as_deref().unwrap_or("webdav"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
2026-02-10 19:26:28 +01:00
|
|
|
} else {
|
2026-03-02 23:40:48 +01:00
|
|
|
let file = file_retrieval_service
|
|
|
|
|
.get_file_by_path(&source_path)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|_e| {
|
|
|
|
|
AppError::not_found(format!("Resource not found: {}", source_path))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
|
|
|
|
|
let dest_filename = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
|
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
|
|
|
|
let source_parent_path = if let Some(idx) = source_path.rfind('/') {
|
|
|
|
|
&source_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
if source_parent_path != dest_parent_path {
|
|
|
|
|
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)))?;
|
|
|
|
|
}
|
|
|
|
|
if file.name != dest_filename {
|
|
|
|
|
file_management_service
|
|
|
|
|
.rename_file(&file.id, dest_filename)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to rename file: {}", e))
|
|
|
|
|
})?;
|
2026-03-02 23:40:48 +01:00
|
|
|
}
|
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-24 15:11:56 +01:00
|
|
|
state: Arc<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";
|
|
|
|
|
|
2026-03-01 20:34:12 +01:00
|
|
|
// Extract destination path from URL and percent-decode it
|
2025-04-04 21:31:41 +02:00
|
|
|
let destination_path = if let Some(webdav_prefix) = destination.find("/webdav/") {
|
|
|
|
|
let after_prefix = &destination[webdav_prefix + 8..];
|
2026-03-01 20:34:12 +01:00
|
|
|
let trimmed = after_prefix.trim_end_matches('/');
|
|
|
|
|
percent_decode_str(trimmed).decode_utf8_lossy().into_owned()
|
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 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-03-02 23:40:48 +01:00
|
|
|
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
|
|
|
|
resolver.exists(&destination_path).await.unwrap_or(false)
|
|
|
|
|
} else {
|
2026-03-03 01:49:18 +01:00
|
|
|
folder_service
|
|
|
|
|
.get_folder_by_path(&destination_path)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok()
|
|
|
|
|
|| file_retrieval_service
|
|
|
|
|
.get_file_by_path(&destination_path)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok()
|
2026-03-02 23:40:48 +01:00
|
|
|
};
|
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
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
// Resolve source: single-query when PathResolver is available
|
|
|
|
|
if let Some(resolver) = &state.path_resolver {
|
|
|
|
|
match resolver.resolve_path(&source_path).await {
|
|
|
|
|
Ok(ResolvedResource::Folder(folder)) => {
|
|
|
|
|
let recursive = depth != "0";
|
|
|
|
|
|
|
|
|
|
let dest_folder_name = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
|
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
let target_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),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
if recursive {
|
|
|
|
|
let file_management_service = &state.applications.file_management_service;
|
|
|
|
|
file_management_service
|
|
|
|
|
.copy_folder_tree(
|
|
|
|
|
&folder.id,
|
|
|
|
|
target_parent_id,
|
|
|
|
|
Some(dest_folder_name.to_string()),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to copy folder tree: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
} else {
|
|
|
|
|
let create_dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
|
|
|
|
name: dest_folder_name.to_string(),
|
|
|
|
|
parent_id: target_parent_id,
|
|
|
|
|
};
|
|
|
|
|
folder_service
|
|
|
|
|
.create_folder(create_dto)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
2026-03-03 01:49:18 +01:00
|
|
|
AppError::internal_error(format!(
|
|
|
|
|
"Failed to create destination folder: {}",
|
|
|
|
|
e
|
|
|
|
|
))
|
2026-03-02 23:40:48 +01:00
|
|
|
})?;
|
|
|
|
|
}
|
2026-02-24 19:28:00 +01:00
|
|
|
}
|
2026-03-02 23:40:48 +01:00
|
|
|
Ok(ResolvedResource::File(file)) => {
|
|
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +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,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let file_management_service = &state.applications.file_management_service;
|
|
|
|
|
file_management_service
|
|
|
|
|
.copy_file(&file.id, target_folder_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to copy file: {}", e)))?;
|
|
|
|
|
}
|
2026-03-03 01:49:18 +01:00
|
|
|
Err(_) => {
|
|
|
|
|
return Err(AppError::not_found(format!(
|
|
|
|
|
"Resource not found: {}",
|
|
|
|
|
source_path
|
|
|
|
|
)));
|
|
|
|
|
}
|
2025-04-04 21:31:41 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-03-02 23:40:48 +01:00
|
|
|
// Fallback: legacy double-query path
|
|
|
|
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
|
|
|
|
|
|
|
|
|
if let Ok(folder) = folder_result {
|
|
|
|
|
let recursive = depth != "0";
|
|
|
|
|
|
|
|
|
|
let dest_folder_name = destination_path
|
|
|
|
|
.split('/')
|
|
|
|
|
.next_back()
|
|
|
|
|
.unwrap_or(&destination_path);
|
|
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
let target_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),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
if recursive {
|
|
|
|
|
let file_management_service = &state.applications.file_management_service;
|
|
|
|
|
file_management_service
|
|
|
|
|
.copy_folder_tree(
|
|
|
|
|
&folder.id,
|
|
|
|
|
target_parent_id,
|
|
|
|
|
Some(dest_folder_name.to_string()),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to copy folder tree: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
} else {
|
|
|
|
|
let create_dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
|
|
|
|
name: dest_folder_name.to_string(),
|
|
|
|
|
parent_id: target_parent_id,
|
|
|
|
|
};
|
|
|
|
|
folder_service
|
|
|
|
|
.create_folder(create_dto)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
2026-03-03 01:49:18 +01:00
|
|
|
AppError::internal_error(format!(
|
|
|
|
|
"Failed to create destination folder: {}",
|
|
|
|
|
e
|
|
|
|
|
))
|
2026-03-02 23:40:48 +01:00
|
|
|
})?;
|
2026-02-15 17:53:25 +01:00
|
|
|
}
|
2026-03-02 23:40:48 +01:00
|
|
|
} else {
|
|
|
|
|
let file = file_retrieval_service
|
|
|
|
|
.get_file_by_path(&source_path)
|
|
|
|
|
.await
|
2026-03-03 01:49:18 +01:00
|
|
|
.map_err(|_e| {
|
|
|
|
|
AppError::not_found(format!("Resource not found: {}", source_path))
|
|
|
|
|
})?;
|
2026-02-15 17:53:25 +01:00
|
|
|
|
2026-03-02 23:40:48 +01:00
|
|
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
|
|
|
|
&destination_path[..idx]
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let file_management_service = &state.applications.file_management_service;
|
|
|
|
|
file_management_service
|
|
|
|
|
.copy_file(&file.id, target_folder_id)
|
|
|
|
|
.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-03-03 11:49:52 +01:00
|
|
|
state: Arc<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
|
|
|
|
2026-03-03 11:49:52 +01:00
|
|
|
let lock_store = &state.webdav_lock_store;
|
|
|
|
|
|
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 {
|
|
|
|
|
// 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
|
|
|
|
2026-03-03 11:49:52 +01:00
|
|
|
// Refresh the lock in the store (extends TTL)
|
|
|
|
|
let entry = lock_store
|
|
|
|
|
.refresh(&token, timeout.as_deref())
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
AppError::precondition_failed(format!("Lock token not found or expired: {}", token))
|
|
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response
|
2026-03-01 20:34:12 +01:00
|
|
|
let href = format!("/webdav/{}", encode_uri_path(&path));
|
2025-04-04 21:31:41 +02:00
|
|
|
let mut response_body = Vec::new();
|
2026-03-03 11:49:52 +01:00
|
|
|
WebDavAdapter::generate_lock_response(&mut response_body, &entry.info, &href).map_err(
|
2026-02-14 01:29:34 +01:00
|
|
|
|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")
|
2026-03-03 11:49:52 +01:00
|
|
|
.header(HEADER_LOCK_TOKEN, format!("<{}>", entry.info.token))
|
2025-04-04 21:31:41 +02:00
|
|
|
.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
|
|
|
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
|
|
|
|
2026-03-03 11:49:52 +01:00
|
|
|
// Try to acquire the lock (conflict detection via moka store)
|
|
|
|
|
let entry = lock_store.acquire(&path, lock_info).map_err(|existing| {
|
|
|
|
|
AppError::locked(format!(
|
|
|
|
|
"Resource already locked by token {}",
|
|
|
|
|
existing.info.token
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
|
2025-04-04 21:31:41 +02:00
|
|
|
// Generate response
|
2026-03-01 20:34:12 +01:00
|
|
|
let href = format!("/webdav/{}", encode_uri_path(&path));
|
2025-04-04 21:31:41 +02:00
|
|
|
let mut response_body = Vec::new();
|
2026-03-03 11:49:52 +01:00
|
|
|
WebDavAdapter::generate_lock_response(&mut response_body, &entry.info, &href).map_err(
|
2026-02-14 01:29:34 +01:00
|
|
|
|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")
|
2026-03-03 11:49:52 +01:00
|
|
|
.header(HEADER_LOCK_TOKEN, format!("<{}>", entry.info.token))
|
2025-04-04 21:31:41 +02:00
|
|
|
.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-03-03 11:49:52 +01:00
|
|
|
state: Arc<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>)
|
2026-03-03 11:49:52 +01: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
|
|
|
|
2026-03-03 11:49:52 +01:00
|
|
|
// Remove the lock from the store
|
|
|
|
|
if !state.webdav_lock_store.release(&token) {
|
|
|
|
|
// RFC 4918 §9.11.1: If the lock does not exist, return 409 Conflict
|
|
|
|
|
return Err(AppError::conflict(format!(
|
|
|
|
|
"Lock token not found or already expired: {}",
|
|
|
|
|
token
|
|
|
|
|
)));
|
|
|
|
|
}
|
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
|
|
|
}
|