feat(folders): implement copy_folders taking care of ownership

This commit is contained in:
Edouard Vanbelle
2026-05-11 22:58:33 +02:00
parent 85c8c086ac
commit 2f49daa4ee
7 changed files with 233 additions and 12 deletions
+10
View File
@@ -334,6 +334,16 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
"copy_folder_tree not implemented",
))
}
/// Copies a folder tree, enforcing that `caller_id` owns both the source folder
/// and the target parent folder.
async fn copy_folder_tree_owned(
&self,
source_folder_id: &str,
caller_id: Uuid,
target_parent_id: Option<String>,
dest_name: Option<String>,
) -> Result<CopyFolderTreeResult, DomainError>;
}
/// Factory for creating file use case implementations
@@ -12,6 +12,7 @@ use tracing::info;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::{FolderDto, MoveFolderDto};
use crate::application::ports::file_ports::{FileManagementUseCase, FileRetrievalUseCase};
use crate::application::ports::storage_ports::CopyFolderTreeResult;
use crate::application::ports::inbound::FolderUseCase;
use crate::application::ports::trash_ports::TrashUseCase;
use crate::application::services::file_management_service::FileManagementService;
@@ -616,6 +617,73 @@ impl BatchOperationService {
Ok(result)
}
/// Copies multiple folder trees to a target parent in parallel
pub async fn copy_folders(
&self,
folder_ids: Vec<String>,
target_folder_id: Option<String>,
user_id: Uuid,
) -> Result<BatchResult<CopyFolderTreeResult>, BatchOperationError> {
info!("Starting batch copy of {} folders", folder_ids.len());
let start_time = std::time::Instant::now();
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
stats: BatchStats {
total: folder_ids.len(),
..Default::default()
},
};
let target: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
let file_management = self.file_management.clone();
let target = target.clone();
async move {
let copy_result = file_management
.copy_folder_tree_owned(
&folder_id,
user_id,
target.map(|s| s.to_string()),
None,
)
.await;
(folder_id, copy_result)
}
}))
.buffer_unordered(self.config.concurrency.max_concurrent_files);
while let Some((folder_id, operation_result)) = operation_stream.next().await {
match operation_result {
Ok(copy_result) => {
result.successful.push(copy_result);
result.stats.successful += 1;
}
Err(e) => {
result.failed.push((folder_id, e.to_string()));
result.stats.failed += 1;
}
}
}
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total);
info!(
"Batch folder copy completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.total, result.stats.execution_time_ms
);
Ok(result)
}
/// Downloads multiple files/folders as a single ZIP archive.
///
/// Writes the archive to a temporary file so RAM usage is O(buffer_size)
@@ -326,4 +326,31 @@ impl FileManagementUseCase for FileManagementService {
Ok(result)
}
async fn copy_folder_tree_owned(
&self,
source_folder_id: &str,
caller_id: Uuid,
target_parent_id: Option<String>,
dest_name: Option<String>,
) -> Result<CopyFolderTreeResult, DomainError> {
if let Some(folder_repo) = &self.folder_repo {
let owner = folder_repo.get_folder_user_id(source_folder_id).await?;
if owner != caller_id {
return Err(DomainError::not_found(
"Folder",
"Source folder not found or access denied",
));
}
} else {
return Err(DomainError::internal_error(
"FileManagement",
"Folder ownership verification unavailable",
));
}
self.verify_target_folder_owner(&target_parent_id, caller_id)
.await?;
self.copy_folder_tree(source_folder_id, target_parent_id, dest_name)
.await
}
}