perf(webdav): single UNION ALL query for path resolution

Replace the double-query pattern (get_folder_by_path + get_file_by_path)
across PROPFIND, HEAD, DELETE, MOVE, and COPY handlers with a single
UNION ALL query via PathResolverService.

PG Append node short-circuits on LIMIT 1: if the folder branch matches,
the file branch is never executed. Cuts WebDAV path resolution from
2 round-trips to 1 per request.

Also adds an exists() method using EXISTS subqueries for the Overwrite
header checks in MOVE/COPY (avoids constructing full DTOs).

Legacy double-query fallback retained when PathResolver is unavailable.
This commit is contained in:
Dionisio
2026-03-02 23:40:48 +01:00
parent b199968a6e
commit b9bf7ba288
4 changed files with 648 additions and 212 deletions
+10
View File
@@ -542,6 +542,7 @@ impl AppServiceFactory {
wopi_discovery_service: None,
device_auth_service: None,
app_password_service: None,
path_resolver: None,
};
// 9b. Wire admin settings service when auth is available
@@ -651,6 +652,13 @@ impl AppServiceFactory {
}
}
// 9e. Wire PathResolver for single-query WebDAV path resolution
{
use crate::infrastructure::services::path_resolver_service::PathResolverService;
app_state.path_resolver = Some(Arc::new(PathResolverService::new(pool.clone())));
tracing::info!("PathResolver service initialized");
}
// 10. Wire CalDAV/CardDAV services
{
// CalDAV
@@ -846,6 +854,8 @@ pub struct AppState {
Option<Arc<crate::application::services::device_auth_service::DeviceAuthService>>,
pub app_password_service:
Option<Arc<crate::application::services::app_password_service::AppPasswordService>>,
pub path_resolver:
Option<Arc<crate::infrastructure::services::path_resolver_service::PathResolverService>>,
}
// All AppState construction is done via struct literal in build_app_state().
+1
View File
@@ -7,6 +7,7 @@ pub mod image_transcode_service;
pub mod jwt_service;
pub mod oidc_service;
pub mod password_hasher;
pub mod path_resolver_service;
pub mod path_service;
pub mod thumbnail_service;
pub mod trash_cleanup_service;
@@ -0,0 +1,205 @@
//! Single-query WebDAV path resolver.
//!
//! Replaces the double-query pattern (`get_folder_by_path` + `get_file_by_path`)
//! with a single `UNION ALL` query that returns the first match. PostgreSQL's
//! `Append` node short-circuits on `LIMIT 1`, so if the folder branch matches
//! the file branch is never executed.
use sqlx::PgPool;
use std::sync::Arc;
use crate::application::dtos::display_helpers::{
category_for, format_file_size, icon_class_for, icon_special_class_for,
};
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
use crate::common::errors::DomainError;
/// Result of resolving a WebDAV path — either a folder or a file.
#[derive(Debug, Clone)]
pub enum ResolvedResource {
Folder(FolderDto),
File(FileDto),
}
/// Resolves a WebDAV path to a folder or file in a single SQL round-trip.
pub struct PathResolverService {
pool: Arc<PgPool>,
}
impl PathResolverService {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
/// Resolve `path` (without leading `/`) to either a folder or a file.
///
/// The query uses `UNION ALL … LIMIT 1`: the folder branch is evaluated
/// first, and PG short-circuits if it produces a row.
pub async fn resolve_path(&self, path: &str) -> Result<ResolvedResource, DomainError> {
let path = path.trim_start_matches('/').trim_end_matches('/');
if path.is_empty() {
return Err(DomainError::not_found("Resource", "empty path"));
}
// Split into folder_path + filename for the file branch
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let filename = segments[segments.len() - 1];
let folder_path = if segments.len() > 1 {
segments[..segments.len() - 1].join("/")
} else {
String::new()
};
// Single round-trip: folder branch ∪ file branch, LIMIT 1.
// Column order: resource_type, id, name, path, parent_id, user_id,
// created_at, modified_at, size, mime_type, folder_id
let row = sqlx::query_as::<_, (
String, // resource_type
String, // id
String, // name
String, // path
Option<String>, // parent_id (folder) / NULL (file)
Option<String>, // user_id
i64, // created_at epoch
i64, // modified_at epoch
Option<i64>, // size (NULL for folder)
Option<String>, // mime_type (NULL for folder)
Option<String>, // folder_id (NULL for folder)
)>(
r#"
SELECT resource_type, id, name, path, parent_id, user_id,
created_at, modified_at, size, mime_type, folder_id
FROM (
SELECT 'folder'::text AS resource_type,
fo.id::text,
fo.name,
fo.path,
fo.parent_id::text,
fo.user_id::text,
EXTRACT(EPOCH FROM fo.created_at)::bigint AS created_at,
EXTRACT(EPOCH FROM fo.updated_at)::bigint AS modified_at,
NULL::bigint AS size,
NULL::text AS mime_type,
NULL::text AS folder_id
FROM storage.folders fo
WHERE fo.path = $1 AND NOT fo.is_trashed
UNION ALL
SELECT 'file'::text AS resource_type,
fi.id::text,
fi.name,
CASE
WHEN fo.path IS NOT NULL AND fo.path != ''
THEN fo.path || '/' || fi.name
ELSE fi.name
END AS path,
NULL::text AS parent_id,
fi.user_id::text,
EXTRACT(EPOCH FROM fi.created_at)::bigint AS created_at,
EXTRACT(EPOCH FROM fi.updated_at)::bigint AS modified_at,
fi.size,
fi.mime_type,
fi.folder_id::text
FROM storage.files fi
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
WHERE fi.name = $2
AND (
($3 = '' AND fi.folder_id IS NULL)
OR fo.path = $3
)
AND NOT fi.is_trashed
) sub
LIMIT 1
"#,
)
.bind(path) // $1 — full path for folder lookup
.bind(filename) // $2 — filename for file lookup
.bind(&folder_path) // $3 — parent folder path for file lookup
.fetch_optional(self.pool.as_ref())
.await
.map_err(|e| DomainError::internal_error("PathResolver", format!("resolve: {e}")))?
.ok_or_else(|| DomainError::not_found("Resource", path))?;
let (resource_type, id, name, res_path, parent_id, user_id,
created_at, modified_at, size, mime_type, folder_id) = row;
match resource_type.as_str() {
"folder" => Ok(ResolvedResource::Folder(FolderDto {
id,
name: name.clone(),
path: res_path,
parent_id,
owner_id: user_id,
created_at: created_at as u64,
modified_at: modified_at as u64,
is_root: false,
icon_class: "fas fa-folder".to_string(),
icon_special_class: "folder-icon".to_string(),
category: "Folder".to_string(),
})),
_ => {
let mime = mime_type.unwrap_or_else(|| "application/octet-stream".to_string());
let sz = size.unwrap_or(0) as u64;
Ok(ResolvedResource::File(FileDto {
id,
name: name.clone(),
path: res_path,
size: sz,
mime_type: mime.clone(),
folder_id,
created_at: created_at as u64,
modified_at: modified_at as u64,
icon_class: icon_class_for(&name, &mime).to_string(),
icon_special_class: icon_special_class_for(&name, &mime).to_string(),
category: category_for(&name, &mime).to_string(),
size_formatted: format_file_size(sz),
owner_id: user_id,
}))
}
}
}
/// Check whether *any* resource (folder or file) exists at the given path.
///
/// Equivalent to `resolve_path(…).is_ok()` but avoids constructing the DTO.
pub async fn exists(&self, path: &str) -> Result<bool, DomainError> {
let path = path.trim_start_matches('/').trim_end_matches('/');
if path.is_empty() {
return Ok(false);
}
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let filename = segments[segments.len() - 1];
let folder_path = if segments.len() > 1 {
segments[..segments.len() - 1].join("/")
} else {
String::new()
};
let exists = sqlx::query_scalar::<_, bool>(
r#"
SELECT EXISTS(
SELECT 1 FROM storage.folders
WHERE path = $1 AND NOT is_trashed
) OR EXISTS(
SELECT 1
FROM storage.files fi
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
WHERE fi.name = $2
AND (($3 = '' AND fi.folder_id IS NULL) OR fo.path = $3)
AND NOT fi.is_trashed
)
"#,
)
.bind(path)
.bind(filename)
.bind(&folder_path)
.fetch_one(self.pool.as_ref())
.await
.map_err(|e| DomainError::internal_error("PathResolver", format!("exists: {e}")))?;
Ok(exists)
}
}
+432 -212
View File
@@ -25,6 +25,7 @@ use crate::application::dtos::folder_dto::FolderDto;
use crate::application::ports::file_ports::FileRetrievalUseCase;
use crate::application::ports::inbound::FolderUseCase;
use crate::common::di::AppState;
use crate::infrastructure::services::path_resolver_service::ResolvedResource;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::CurrentUser;
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC, AsciiSet};
@@ -301,38 +302,73 @@ async fn handle_propfind(
.await;
}
// Try folder first
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;
}
// Try file
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)))?;
WebDavAdapter::write_file_entry(&mut xml_writer, &file, &propfind_request, &base_href)
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
WebDavAdapter::write_multistatus_end(&mut xml_writer)
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
// 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)))?;
WebDavAdapter::write_file_entry(&mut xml_writer, &file, &propfind_request, &base_href)
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
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)))?;
WebDavAdapter::write_file_entry(&mut xml_writer, &file, &propfind_request, &base_href)
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
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());
}
return Ok(Response::builder()
.status(StatusCode::MULTI_STATUS)
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
.body(Body::from(buf))
.unwrap());
}
Err(AppError::not_found(format!("Resource not found: {}", path)))
@@ -596,7 +632,38 @@ async fn handle_head(
.unwrap());
}
// Check if it's a folder first
// 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
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
return Ok(Response::builder()
.status(StatusCode::OK)
@@ -835,27 +902,45 @@ async fn handle_delete(
return Err(AppError::forbidden("Cannot delete root folder"));
}
// Check if path is a folder
let folder_result = folder_service.get_folder_by_path(&path).await;
if let Ok(folder) = folder_result {
// Delete folder — use the folder's own owner as caller_id
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)))?;
// 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
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
}
Ok(ResolvedResource::File(file)) => {
file_management_service
.delete_file(&file.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete file: {}", e)))?;
}
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", path))),
}
} else {
// Try to delete file
let file = file_retrieval_service
.get_file_by_path(&path)
.await
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
// Fallback: legacy double-query path
let folder_result = folder_service.get_folder_by_path(&path).await;
file_management_service
.delete_file(&file.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete file: {}", e)))?;
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)))?;
}
}
Ok(Response::builder()
@@ -914,14 +999,12 @@ async fn handle_move(
// Check if destination already exists (for Overwrite header compliance)
if !overwrite {
let dest_exists = folder_service
.get_folder_by_path(&destination_path)
.await
.is_ok()
|| file_retrieval_service
.get_file_by_path(&destination_path)
.await
.is_ok();
let dest_exists = if let Some(resolver) = &state.path_resolver {
resolver.exists(&destination_path).await.unwrap_or(false)
} else {
folder_service.get_folder_by_path(&destination_path).await.is_ok()
|| file_retrieval_service.get_file_by_path(&destination_path).await.is_ok()
};
if dest_exists {
return Err(AppError::precondition_failed(
"Destination already exists and Overwrite is F",
@@ -929,94 +1012,166 @@ async fn handle_move(
}
}
// Check if source is a folder
let folder_result = folder_service.get_folder_by_path(&source_path).await;
// 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 {
""
};
if let Ok(folder) = folder_result {
// Move 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 {
""
};
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,
}
},
};
// Create DTOs for moving and renaming
let move_dto = crate::application::dtos::folder_dto::MoveFolderDto {
parent_id: if dest_parent_path.is_empty() {
None
} else {
match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => Some(parent.id),
Err(_) => None, // If not found, use root
folder_service
.move_folder(
&folder.id,
move_dto,
folder.owner_id.as_deref().unwrap_or("webdav"),
)
.await
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
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
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
}
},
};
}
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 {
""
};
folder_service
.move_folder(
&folder.id,
move_dto,
folder.owner_id.as_deref().unwrap_or("webdav"),
)
.await
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
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
.map_err(|e| AppError::internal_error(format!("Failed to rename file: {}", e)))?;
}
}
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", source_path))),
}
} else {
// Fallback: legacy double-query path
let folder_result = folder_service.get_folder_by_path(&source_path).await;
if folder.name != dest_folder_name {
let rename_dto = crate::application::dtos::folder_dto::RenameFolderDto {
name: dest_folder_name.to_string(),
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 {
""
};
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
.rename_folder(
.move_folder(
&folder.id,
rename_dto,
move_dto,
folder.owner_id.as_deref().unwrap_or("webdav"),
)
.await
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
}
} else {
// Try to move file
let file = file_retrieval_service
.get_file_by_path(&source_path)
.await
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
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]
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
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
}
} else {
""
};
// Determine source parent path for comparison
let source_parent_path = if let Some(idx) = source_path.rfind('/') {
&source_path[..idx]
} else {
""
};
// Only call move_file if the parent directory actually changes
if source_parent_path != dest_parent_path {
file_management_service
.move_file(&file.id, Some(dest_parent_path.to_string()))
let file = file_retrieval_service
.get_file_by_path(&source_path)
.await
.map_err(|e| AppError::internal_error(format!("Failed to move file: {}", e)))?;
}
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
// Rename the file if the name changed
if file.name != dest_filename {
file_management_service
.rename_file(&file.id, dest_filename)
.await
.map_err(|e| AppError::internal_error(format!("Failed to rename file: {}", e)))?;
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 {
""
};
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
.map_err(|e| AppError::internal_error(format!("Failed to rename file: {}", e)))?;
}
}
}
@@ -1082,14 +1237,12 @@ async fn handle_copy(
// Check if destination already exists (for Overwrite header compliance)
if !overwrite {
let dest_exists = folder_service
.get_folder_by_path(&destination_path)
.await
.is_ok()
|| file_retrieval_service
.get_file_by_path(&destination_path)
.await
.is_ok();
let dest_exists = if let Some(resolver) = &state.path_resolver {
resolver.exists(&destination_path).await.unwrap_or(false)
} else {
folder_service.get_folder_by_path(&destination_path).await.is_ok()
|| file_retrieval_service.get_file_by_path(&destination_path).await.is_ok()
};
if dest_exists {
return Err(AppError::precondition_failed(
"Destination already exists and Overwrite is F",
@@ -1097,90 +1250,157 @@ async fn handle_copy(
}
}
// Check if source is a folder
let folder_result = folder_service.get_folder_by_path(&source_path).await;
// 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";
if let Ok(folder) = folder_result {
// Copy 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 {
""
};
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 {
""
};
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,
}
};
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,
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| {
AppError::internal_error(format!("Failed to create destination folder: {}", e))
})?;
}
}
};
Ok(ResolvedResource::File(file)) => {
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
&destination_path[..idx]
} else {
""
};
if recursive {
// Atomic recursive copy: single SQL function call copies the entire
// folder tree (all sub-folders + all files) with zero-copy dedup.
// O(depth) folder INSERTs + 1 batch file INSERT + 1 batch ref_count UPDATE.
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 {
// Depth: 0 — create empty folder only (no sub-folder or file copy)
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| {
AppError::internal_error(format!("Failed to create destination folder: {}", e))
})?;
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)))?;
}
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", source_path))),
}
} else {
// Copy file — use zero-copy dedup (only increments blob ref_count, no content loaded)
let file = file_retrieval_service
.get_file_by_path(&source_path)
.await
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
// Fallback: legacy double-query path
let folder_result = folder_service.get_folder_by_path(&source_path).await;
// Get destination parent folder ID
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
&destination_path[..idx]
} else {
""
};
if let Ok(folder) = folder_result {
let recursive = depth != "0";
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 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 {
""
};
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,
}
};
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| {
AppError::internal_error(format!("Failed to create destination folder: {}", e))
})?;
}
};
} else {
let file = file_retrieval_service
.get_file_by_path(&source_path)
.await
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
// Zero-copy: only creates a new metadata row + increments blob reference count.
// No file content is ever loaded into memory.
let file_management_service = &state.applications.file_management_service;
file_management_service
.copy_file(&file.id, target_folder_id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to copy file: {}", e)))?;
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)))?;
}
}
Ok(Response::builder()