use std::sync::Arc; use thiserror::Error; use futures::{future::join_all, Future}; use tokio::sync::Semaphore; use tracing::{info, error}; use crate::application::services::file_service::FileService; use crate::application::services::folder_service::FolderService; use crate::domain::services::path_service::StoragePath; use crate::common::errors::DomainError; use crate::common::config::AppConfig; use crate::application::ports::inbound::FolderUseCase; use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::folder_dto::FolderDto; /// Errores específicos para operaciones por lotes #[derive(Debug, Error)] #[allow(dead_code)] pub enum BatchOperationError { #[error("Error de dominio: {0}")] Domain(#[from] DomainError), #[error("Operación cancelada: {0}")] Cancelled(String), #[error("Límite de concurrencia excedido: {0}")] ConcurrencyLimit(String), #[error("Error en operación del lote: {0} ({1} de {2} completadas)")] PartialFailure(String, usize, usize), #[error("Error interno: {0}")] Internal(String), } /// Resultado de una operación por lotes con estadísticas #[derive(Debug, Clone)] pub struct BatchResult { /// Resultados exitosos pub successful: Vec, /// Operaciones fallidas con sus errores pub failed: Vec<(String, String)>, /// Estadísticas de la operación pub stats: BatchStats, } /// Estadísticas de una operación por lotes #[derive(Debug, Clone, Default)] pub struct BatchStats { /// Número total de operaciones pub total: usize, /// Número de operaciones exitosas pub successful: usize, /// Número de operaciones fallidas pub failed: usize, /// Tiempo total de ejecución en milisegundos pub execution_time_ms: u128, /// Concurrencia máxima alcanzada pub max_concurrency: usize, } /// Tipo de entidad para operaciones por lotes #[derive(Debug, Clone, PartialEq, Eq)] #[allow(dead_code)] pub enum EntityType { File, Folder, } /// Tipo de operación por lotes #[derive(Debug, Clone, PartialEq, Eq)] #[allow(dead_code)] pub enum BatchOperationType { Create, Read, Update, Delete, Copy, Move, } /// Identificador para una entidad (ID o ruta) #[derive(Debug, Clone)] #[allow(dead_code)] pub enum EntityIdentifier { Id(String), Path(StoragePath), } impl EntityIdentifier { #[allow(dead_code)] pub fn as_id(&self) -> Option<&str> { match self { EntityIdentifier::Id(id) => Some(id), _ => None, } } #[allow(dead_code)] pub fn as_path(&self) -> Option<&StoragePath> { match self { EntityIdentifier::Path(path) => Some(path), _ => None, } } } /// Servicio de operaciones por lotes pub struct BatchOperationService { file_service: Arc, folder_service: Arc, config: AppConfig, semaphore: Arc, } impl BatchOperationService { /// Crea una nueva instancia del servicio de operaciones por lotes pub fn new( file_service: Arc, folder_service: Arc, config: AppConfig ) -> Self { // Limitar la concurrencia basada en la configuración let max_concurrency = config.concurrency.max_concurrent_files; Self { file_service, folder_service, config, semaphore: Arc::new(Semaphore::new(max_concurrency)), } } /// Crea una nueva instancia con la configuración por defecto pub fn default( file_service: Arc, folder_service: Arc ) -> Self { Self::new(file_service, folder_service, AppConfig::default()) } /// Copia múltiples archivos en paralelo pub async fn copy_files( &self, file_ids: Vec, target_folder_id: Option, ) -> Result, BatchOperationError> { info!("Iniciando copia en lote de {} archivos", file_ids.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: file_ids.len(), ..Default::default() }, }; // Definir la operación a realizar para cada archivo let operations = file_ids.into_iter().map(|file_id| { let file_service = self.file_service.clone(); let target_folder = target_folder_id.clone(); let semaphore = self.semaphore.clone(); async move { // Adquirir permiso del semáforo let permit = semaphore.acquire().await.unwrap(); let copy_result = file_service.move_file(&file_id, target_folder.clone()).await; // Liberar el permiso explícitamente (también se libera al hacer drop) drop(permit); // Devolver el resultado junto con el ID para identificar éxitos/fallos (file_id, copy_result) } }); // Ejecutar todas las operaciones en paralelo con control de concurrencia let operation_results = join_all(operations).await; // Procesar los resultados for (file_id, operation_result) in operation_results { match operation_result { Ok(file) => { result.successful.push(file); result.stats.successful += 1; } Err(e) => { result.failed.push((file_id, e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Copia en lote completada: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } /// Mueve múltiples archivos en paralelo pub async fn move_files( &self, file_ids: Vec, target_folder_id: Option, ) -> Result, BatchOperationError> { info!("Iniciando movimiento en lote de {} archivos", file_ids.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: file_ids.len(), ..Default::default() }, }; // Definir la operación a realizar para cada archivo let operations = file_ids.into_iter().map(|file_id| { let file_service = self.file_service.clone(); let target_folder = target_folder_id.clone(); let semaphore = self.semaphore.clone(); async move { // Adquirir permiso del semáforo let permit = semaphore.acquire().await.unwrap(); let move_result = file_service.move_file(&file_id, target_folder.clone()).await; // Liberar el permiso explícitamente drop(permit); // Devolver el resultado junto con el ID para identificar éxitos/fallos (file_id, move_result) } }); // Ejecutar todas las operaciones en paralelo con control de concurrencia let operation_results = join_all(operations).await; // Procesar los resultados for (file_id, operation_result) in operation_results { match operation_result { Ok(file) => { result.successful.push(file); result.stats.successful += 1; } Err(e) => { result.failed.push((file_id, e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Movimiento en lote completado: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } /// Elimina múltiples archivos en paralelo pub async fn delete_files( &self, file_ids: Vec, ) -> Result, BatchOperationError> { info!("Iniciando eliminación en lote de {} archivos", file_ids.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: file_ids.len(), ..Default::default() }, }; // Definir la operación a realizar para cada archivo let operations = file_ids.into_iter().map(|file_id| { let file_service = self.file_service.clone(); let semaphore = self.semaphore.clone(); let id_clone = file_id.clone(); async move { // Adquirir permiso del semáforo let permit = semaphore.acquire().await.unwrap(); let delete_result = file_service.delete_file(&file_id).await; // Liberar el permiso explícitamente drop(permit); // Devolver el resultado junto con el ID (id_clone.clone(), delete_result.map(|_| id_clone)) } }); // Ejecutar todas las operaciones en paralelo con control de concurrencia let operation_results = join_all(operations).await; // Procesar los resultados for (file_id, operation_result) in operation_results { match operation_result { Ok(id) => { result.successful.push(id); result.stats.successful += 1; } Err(e) => { result.failed.push((file_id, e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Eliminación en lote completada: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } /// Carga múltiples archivos en paralelo (datos en memoria) pub async fn get_multiple_files( &self, file_ids: Vec, ) -> Result, BatchOperationError> { info!("Iniciando carga en lote de {} archivos", file_ids.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: file_ids.len(), ..Default::default() }, }; // Definir la operación a realizar para cada archivo let operations = file_ids.into_iter().map(|file_id| { let file_service = self.file_service.clone(); let semaphore = self.semaphore.clone(); async move { // Adquirir permiso del semáforo let permit = semaphore.acquire().await.unwrap(); let get_result = file_service.get_file(&file_id).await; // Liberar el permiso explícitamente drop(permit); // Devolver el resultado junto con el ID (file_id, get_result) } }); // Ejecutar todas las operaciones en paralelo con control de concurrencia let operation_results = join_all(operations).await; // Procesar los resultados for (file_id, operation_result) in operation_results { match operation_result { Ok(file) => { result.successful.push(file); result.stats.successful += 1; } Err(e) => { result.failed.push((file_id, e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Carga en lote completada: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } /// Elimina múltiples carpetas en paralelo pub async fn delete_folders( &self, folder_ids: Vec, _recursive: bool, ) -> Result, BatchOperationError> { info!("Iniciando eliminación en lote de {} carpetas", folder_ids.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: folder_ids.len(), ..Default::default() }, }; // Definir la operación a realizar para cada carpeta let operations = folder_ids.into_iter().map(|folder_id| { let folder_service = self.folder_service.clone(); let semaphore = self.semaphore.clone(); let id_clone = folder_id.clone(); async move { // Adquirir permiso del semáforo let permit = semaphore.acquire().await.unwrap(); // For both recursive and non-recursive, use the standard delete_folder method // since FolderUseCase only has a single delete_folder method let delete_result = folder_service.delete_folder(&folder_id).await; // Liberar el permiso explícitamente drop(permit); // Devolver el resultado junto con el ID (id_clone.clone(), delete_result.map(|_| id_clone)) } }); // Ejecutar todas las operaciones en paralelo con control de concurrencia let operation_results = join_all(operations).await; // Procesar los resultados for (folder_id, operation_result) in operation_results { match operation_result { Ok(id) => { result.successful.push(id); result.stats.successful += 1; } Err(e) => { result.failed.push((folder_id, e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Eliminación en lote de carpetas completada: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } /// Operación genérica de lote para cualquier tipo de función asíncrona #[allow(dead_code)] pub async fn generic_batch_operation( &self, items: Vec, operation: F, ) -> Result, BatchOperationError> where T: Clone + Send + 'static + std::fmt::Debug, F: Fn(T, Arc) -> Fut + Clone + Send + Sync + 'static, Fut: Future> + Send + 'static, { info!("Iniciando operación genérica en lote con {} items", items.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: items.len(), ..Default::default() }, }; // Convertir cada item a una tarea let tasks = items.iter().map(|item| { let item_clone = item.clone(); let op = operation.clone(); let semaphore = self.semaphore.clone(); async move { // La función proporcionada debe manejar la adquisición del semáforo let op_result = op(item_clone.clone(), semaphore).await; // Devolver el resultado junto con el item original para identificación (item_clone, op_result) } }); // Ejecutar todas las tareas en paralelo let operation_results = join_all(tasks).await; // Procesar resultados for (item, operation_result) in operation_results { match operation_result { Ok(result_item) => { result.successful.push(result_item); result.stats.successful += 1; } Err(e) => { // Convertir el item a string para el reporte de error result.failed.push((format!("{:?}", item), e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Operación genérica en lote completada: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } /// Crear múltiples carpetas en paralelo pub async fn create_folders( &self, folders: Vec<(String, Option)>, // (nombre, padre_id) ) -> Result, BatchOperationError> { info!("Iniciando creación en lote de {} carpetas", folders.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: folders.len(), ..Default::default() }, }; // Definir la operación para cada carpeta let operations = folders.into_iter().map(|(name, parent_id)| { let folder_service = self.folder_service.clone(); let semaphore = self.semaphore.clone(); async move { // Adquirir permiso del semáforo let permit = semaphore.acquire().await.unwrap(); let dto = crate::application::dtos::folder_dto::CreateFolderDto { name: name.clone(), parent_id: parent_id.clone() }; let create_result = folder_service.create_folder(dto).await; // Liberar el permiso explícitamente drop(permit); // Devolver el resultado con un identificador para los errores let id = format!("{}:{}", name, parent_id.unwrap_or_default()); (id, create_result) } }); // Ejecutar todas las operaciones en paralelo let operation_results = join_all(operations).await; // Procesar los resultados for (id, operation_result) in operation_results { match operation_result { Ok(folder) => { result.successful.push(folder); result.stats.successful += 1; } Err(e) => { result.failed.push((id, e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Creación en lote de carpetas completada: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } /// Obtener metadatos de múltiples carpetas en paralelo pub async fn get_multiple_folders( &self, folder_ids: Vec, ) -> Result, BatchOperationError> { info!("Iniciando carga en lote de {} carpetas", folder_ids.len()); let start_time = std::time::Instant::now(); // Crear estructura para el resultado let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), stats: BatchStats { total: folder_ids.len(), ..Default::default() }, }; // Definir la operación para cada carpeta let operations = folder_ids.into_iter().map(|folder_id| { let folder_service = self.folder_service.clone(); let semaphore = self.semaphore.clone(); async move { // Adquirir permiso del semáforo let permit = semaphore.acquire().await.unwrap(); let get_result = folder_service.get_folder(&folder_id).await; // Liberar el permiso explícitamente drop(permit); // Devolver el resultado con su ID (folder_id, get_result) } }); // Ejecutar todas las operaciones en paralelo let operation_results = join_all(operations).await; // Procesar los resultados for (folder_id, operation_result) in operation_results { match operation_result { Ok(folder) => { result.successful.push(folder); result.stats.successful += 1; } Err(e) => { result.failed.push((folder_id, e.to_string())); result.stats.failed += 1; } } } // Completar estadísticas 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!( "Carga en lote de carpetas completada: {}/{} exitosas en {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms ); Ok(result) } } #[cfg(test)] mod tests { use super::*; use std::sync::Arc; use tokio::sync::Mutex; use mockall::predicate::*; use mockall::mock; // Crear mocks para los servicios mock! { FileSvcMock {} #[async_trait] impl FileService for FileSvcMock { async fn create_file(&self, name: String, folder_id: Option, content_type: String, content: Vec) -> Result; async fn get_file(&self, id: &str) -> Result; async fn delete_file(&self, id: &str) -> Result<(), DomainError>; async fn move_file(&self, id: &str, target_folder_id: Option) -> Result; async fn copy_file(&self, id: &str, target_folder_id: Option) -> Result; async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError>; async fn get_file_content(&self, id: &str) -> Result, DomainError>; } } mock! { FolderSvcMock {} #[async_trait] impl FolderService for FolderSvcMock { async fn create_folder(&self, name: String, parent_id: Option) -> Result; async fn get_folder(&self, id: &str) -> Result; async fn delete_folder(&self, id: &str) -> Result<(), DomainError>; async fn delete_folder_recursive(&self, id: &str) -> Result<(), DomainError>; async fn list_folders(&self, parent_id: Option<&str>) -> Result, DomainError>; async fn move_folder(&self, id: &str, target_parent_id: Option) -> Result; } } #[tokio::test] async fn test_batch_delete_files() { // Crear mocks let mut file_service = MockFileSvcMock::new(); // Configurar comportamiento esperado file_service.expect_delete_file() .times(3) .returning(|id| { if id == "error-id" { Err(DomainError::not_found("FileService", "File not found")) } else { Ok(()) } }); // Crear el servicio de batch con los mocks let batch_service = BatchOperationService::new( Arc::new(file_service), Arc::new(MockFolderSvcMock::new()), AppConfig::default() ); // Ejecutar la operación de batch let file_ids = vec![ "id1".to_string(), "id2".to_string(), "error-id".to_string() ]; let result = batch_service.delete_files(file_ids).await.unwrap(); // Verificar los resultados assert_eq!(result.stats.total, 3); assert_eq!(result.stats.successful, 2); assert_eq!(result.stats.failed, 1); assert_eq!(result.successful.len(), 2); assert_eq!(result.failed.len(), 1); assert_eq!(result.failed[0].0, "error-id"); } #[tokio::test] async fn test_generic_batch_operation() { // Crear el servicio de batch let batch_service = BatchOperationService::new( Arc::new(MockFileSvcMock::new()), Arc::new(MockFolderSvcMock::new()), AppConfig::default() ); // Definir una operación genérica de prueba let operation = |item: i32, semaphore: Arc| async move { // Adquirir y liberar el semáforo let _permit = semaphore.acquire().await.unwrap(); if item % 2 == 0 { // Simular éxito para números pares Ok(item * 2) } else { // Simular error para números impares Err(DomainError::invalid_input("Test", "Odd number not allowed")) } }; // Ejecutar la operación de batch let items = vec![1, 2, 3, 4, 5]; let result = batch_service.generic_batch_operation(items, operation).await.unwrap(); // Verificar los resultados assert_eq!(result.stats.total, 5); assert_eq!(result.stats.successful, 2); assert_eq!(result.stats.failed, 3); // Los números pares deberían estar en los éxitos, duplicados assert!(result.successful.contains(&4)); // 2*2 assert!(result.successful.contains(&8)); // 4*2 // Los impares deberían estar en los fallos assert_eq!(result.failed.len(), 3); } }