adding several features
This commit is contained in:
@@ -32,14 +32,33 @@ pub struct FileDto {
|
||||
impl From<File> for FileDto {
|
||||
fn from(file: File) -> Self {
|
||||
Self {
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
path: file.path.to_string_lossy().to_string(),
|
||||
size: file.size,
|
||||
mime_type: file.mime_type,
|
||||
folder_id: file.folder_id,
|
||||
created_at: file.created_at,
|
||||
modified_at: file.modified_at,
|
||||
id: file.id().to_string(),
|
||||
name: file.name().to_string(),
|
||||
path: file.path_string().to_string(),
|
||||
size: file.size(),
|
||||
mime_type: file.mime_type().to_string(),
|
||||
folder_id: file.folder_id().map(String::from),
|
||||
created_at: file.created_at(),
|
||||
modified_at: file.modified_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Para convertir de FileDto a File para los batch handlers
|
||||
impl From<FileDto> for File {
|
||||
fn from(dto: FileDto) -> Self {
|
||||
// Usar constructor para crear una entidad desde DTO
|
||||
// Nota: esto debe simplificarse si File tiene un constructor adecuado
|
||||
// Si no, deberías hacer la conversión de la mejor manera posible
|
||||
File::from_dto(
|
||||
dto.id,
|
||||
dto.name,
|
||||
dto.path,
|
||||
dto.size,
|
||||
dto.mime_type,
|
||||
dto.folder_id,
|
||||
dto.created_at,
|
||||
dto.modified_at
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -52,15 +52,32 @@ pub struct FolderDto {
|
||||
|
||||
impl From<Folder> for FolderDto {
|
||||
fn from(folder: Folder) -> Self {
|
||||
let is_root = folder.parent_id.is_none();
|
||||
let is_root = folder.parent_id().is_none();
|
||||
|
||||
Self {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
path: folder.path.to_string_lossy().to_string(),
|
||||
parent_id: folder.parent_id,
|
||||
created_at: folder.created_at,
|
||||
modified_at: folder.modified_at,
|
||||
id: folder.id().to_string(),
|
||||
name: folder.name().to_string(),
|
||||
path: folder.path_string().to_string(),
|
||||
parent_id: folder.parent_id().map(String::from),
|
||||
created_at: folder.created_at(),
|
||||
modified_at: folder.modified_at(),
|
||||
is_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Para convertir de FolderDto a Folder para los batch handlers
|
||||
impl From<FolderDto> for Folder {
|
||||
fn from(dto: FolderDto) -> Self {
|
||||
// Usar constructor para crear una entidad desde DTO
|
||||
// Nota: esto debe simplificarse si Folder tiene un constructor adecuado
|
||||
Folder::from_dto(
|
||||
dto.id,
|
||||
dto.name,
|
||||
dto.path,
|
||||
dto.parent_id,
|
||||
dto.created_at,
|
||||
dto.modified_at
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod file_dto;
|
||||
pub mod folder_dto;
|
||||
pub mod i18n_dto;
|
||||
pub mod pagination;
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// Un DTO para representar información de paginación
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaginationDto {
|
||||
/// Página actual (comienza en 0)
|
||||
pub page: usize,
|
||||
/// Tamaño de página
|
||||
pub page_size: usize,
|
||||
/// Número total de elementos
|
||||
pub total_items: usize,
|
||||
/// Número total de páginas
|
||||
pub total_pages: usize,
|
||||
/// Indica si hay una página siguiente
|
||||
pub has_next: bool,
|
||||
/// Indica si hay una página anterior
|
||||
pub has_prev: bool,
|
||||
}
|
||||
|
||||
/// Un DTO para representar una solicitud de paginación
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaginationRequestDto {
|
||||
/// Página solicitada (comienza en 0)
|
||||
#[serde(default)]
|
||||
pub page: usize,
|
||||
/// Tamaño de página solicitado
|
||||
#[serde(default = "default_page_size")]
|
||||
pub page_size: usize,
|
||||
}
|
||||
|
||||
/// Un DTO para representar una respuesta paginada
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaginatedResponseDto<T> {
|
||||
/// Datos en la página actual
|
||||
pub items: Vec<T>,
|
||||
/// Información de paginación
|
||||
pub pagination: PaginationDto,
|
||||
}
|
||||
|
||||
impl Default for PaginationRequestDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
page: 0,
|
||||
page_size: default_page_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Función para establecer el tamaño de página por defecto
|
||||
fn default_page_size() -> usize {
|
||||
100 // Por defecto, 100 items por página
|
||||
}
|
||||
|
||||
impl PaginationRequestDto {
|
||||
/// Calcula el offset para consultas paginadas
|
||||
pub fn offset(&self) -> usize {
|
||||
self.page * self.page_size
|
||||
}
|
||||
|
||||
/// Calcula el límite para consultas paginadas
|
||||
pub fn limit(&self) -> usize {
|
||||
self.page_size
|
||||
}
|
||||
|
||||
/// Valida y ajusta los parámetros de paginación
|
||||
pub fn validate_and_adjust(&self) -> Self {
|
||||
let mut page = self.page;
|
||||
let mut page_size = self.page_size;
|
||||
|
||||
// Asegurar que la página sea al menos 0
|
||||
if page < 1 {
|
||||
page = 0;
|
||||
}
|
||||
|
||||
// Asegurar que el tamaño de página esté entre 10 y 500
|
||||
if page_size < 10 {
|
||||
page_size = 10;
|
||||
} else if page_size > 500 {
|
||||
page_size = 500;
|
||||
}
|
||||
|
||||
Self {
|
||||
page,
|
||||
page_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PaginatedResponseDto<T> {
|
||||
/// Crea una nueva respuesta paginada a partir de los datos y la información de paginación
|
||||
pub fn new(
|
||||
items: Vec<T>,
|
||||
page: usize,
|
||||
page_size: usize,
|
||||
total_items: usize,
|
||||
) -> Self {
|
||||
let total_pages = if total_items == 0 {
|
||||
0
|
||||
} else {
|
||||
(total_items + page_size - 1) / page_size
|
||||
};
|
||||
|
||||
let pagination = PaginationDto {
|
||||
page,
|
||||
page_size,
|
||||
total_items,
|
||||
total_pages,
|
||||
has_next: page < total_pages - 1,
|
||||
has_prev: page > 0,
|
||||
};
|
||||
|
||||
Self {
|
||||
items,
|
||||
pagination,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod dtos;
|
||||
pub mod ports;
|
||||
pub mod services;
|
||||
pub mod transactions;
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto};
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Puerto primario para operaciones de archivos
|
||||
#[async_trait]
|
||||
pub trait FileUseCase: Send + Sync + 'static {
|
||||
/// Sube un nuevo archivo desde bytes
|
||||
async fn upload_file(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Obtiene un archivo por su ID
|
||||
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Lista archivos en una carpeta
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
|
||||
|
||||
/// Elimina un archivo
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como bytes (para archivos pequeños)
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como stream (para archivos grandes)
|
||||
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Mueve un archivo a otra carpeta
|
||||
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
|
||||
}
|
||||
|
||||
/// Puerto primario para operaciones de carpetas
|
||||
#[async_trait]
|
||||
pub trait FolderUseCase: Send + Sync + 'static {
|
||||
/// Crea una nueva carpeta
|
||||
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Obtiene una carpeta por su ID
|
||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Obtiene una carpeta por su ruta
|
||||
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Lista carpetas dentro de una carpeta padre
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError>;
|
||||
|
||||
/// Lista carpetas con paginación
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
||||
|
||||
/// Renombra una carpeta
|
||||
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Mueve una carpeta a otro padre
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Elimina una carpeta
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// Factory para crear implementaciones de casos de uso
|
||||
pub trait UseCaseFactory {
|
||||
fn create_file_use_case(&self) -> Arc<dyn FileUseCase>;
|
||||
fn create_folder_use_case(&self) -> Arc<dyn FolderUseCase>;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod inbound;
|
||||
pub mod outbound;
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Puerto secundario para operaciones de almacenamiento
|
||||
#[async_trait]
|
||||
pub trait StoragePort: Send + Sync + 'static {
|
||||
/// Resuelve una ruta de dominio a una ruta física
|
||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
|
||||
|
||||
/// Crea directorios si no existen
|
||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
|
||||
|
||||
/// Verifica si existe un archivo en la ruta dada
|
||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
|
||||
/// Verifica si existe un directorio en la ruta dada
|
||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
}
|
||||
|
||||
/// Puerto secundario para persistencia de archivos
|
||||
#[async_trait]
|
||||
pub trait FileStoragePort: Send + Sync + 'static {
|
||||
/// Guarda un nuevo archivo desde bytes
|
||||
async fn save_file(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Obtiene un archivo por su ID
|
||||
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
||||
|
||||
/// Lista archivos en una carpeta
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||
|
||||
/// Elimina un archivo
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como bytes
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como stream
|
||||
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Mueve un archivo a otra carpeta
|
||||
async fn move_file(&self, file_id: &str, target_folder_id: Option<String>) -> Result<File, DomainError>;
|
||||
|
||||
/// Obtiene la ruta de almacenamiento de un archivo
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
}
|
||||
|
||||
/// Puerto secundario para persistencia de carpetas
|
||||
#[async_trait]
|
||||
pub trait FolderStoragePort: Send + Sync + 'static {
|
||||
/// Crea una nueva carpeta
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Obtiene una carpeta por su ID
|
||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Obtiene una carpeta por su ruta
|
||||
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Lista carpetas dentro de una carpeta padre
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
|
||||
|
||||
/// Lista carpetas con paginación
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||
|
||||
/// Renombra una carpeta
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Mueve una carpeta a otro padre
|
||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Elimina una carpeta
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Verifica si existe una carpeta en la ruta dada
|
||||
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
|
||||
/// Obtiene la ruta de una carpeta
|
||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
}
|
||||
|
||||
/// Puerto secundario para mapeo de IDs
|
||||
#[async_trait]
|
||||
pub trait IdMappingPort: Send + Sync + 'static {
|
||||
/// Obtiene o crea un ID para una ruta
|
||||
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError>;
|
||||
|
||||
/// Obtiene una ruta por su ID
|
||||
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
|
||||
/// Actualiza la ruta para un ID existente
|
||||
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError>;
|
||||
|
||||
/// Elimina un ID del mapeo
|
||||
async fn remove_id(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Guarda cambios pendientes
|
||||
async fn save_changes(&self) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -0,0 +1,832 @@
|
||||
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<T> {
|
||||
/// Resultados exitosos
|
||||
pub successful: Vec<T>,
|
||||
/// 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<FileService>,
|
||||
folder_service: Arc<FolderService>,
|
||||
config: AppConfig,
|
||||
semaphore: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl BatchOperationService {
|
||||
/// Crea una nueva instancia del servicio de operaciones por lotes
|
||||
pub fn new(
|
||||
file_service: Arc<FileService>,
|
||||
folder_service: Arc<FolderService>,
|
||||
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<FileService>,
|
||||
folder_service: Arc<FolderService>
|
||||
) -> Self {
|
||||
Self::new(file_service, folder_service, AppConfig::default())
|
||||
}
|
||||
|
||||
/// Copia múltiples archivos en paralelo
|
||||
pub async fn copy_files(
|
||||
&self,
|
||||
file_ids: Vec<String>,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<BatchResult<FileDto>, 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<String>,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<BatchResult<FileDto>, 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<String>,
|
||||
) -> Result<BatchResult<String>, 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<String>,
|
||||
) -> Result<BatchResult<FileDto>, 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<String>,
|
||||
_recursive: bool,
|
||||
) -> Result<BatchResult<String>, 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<T, F, Fut>(
|
||||
&self,
|
||||
items: Vec<T>,
|
||||
operation: F,
|
||||
) -> Result<BatchResult<T>, BatchOperationError>
|
||||
where
|
||||
T: Clone + Send + 'static + std::fmt::Debug,
|
||||
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<T, DomainError>> + 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<String>)>, // (nombre, padre_id)
|
||||
) -> Result<BatchResult<FolderDto>, 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<String>,
|
||||
) -> Result<BatchResult<FolderDto>, 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<String>, content_type: String, content: Vec<u8>) -> Result<File, DomainError>;
|
||||
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
async fn move_file(&self, id: &str, target_folder_id: Option<String>) -> Result<File, DomainError>;
|
||||
async fn copy_file(&self, id: &str, target_folder_id: Option<String>) -> Result<File, DomainError>;
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
}
|
||||
}
|
||||
|
||||
mock! {
|
||||
FolderSvcMock {}
|
||||
|
||||
#[async_trait]
|
||||
impl FolderService for FolderSvcMock {
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
|
||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
||||
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<Vec<Folder>, DomainError>;
|
||||
async fn move_folder(&self, id: &str, target_parent_id: Option<String>) -> Result<Folder, DomainError>;
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Semaphore>| 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);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,81 @@
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult};
|
||||
use crate::domain::repositories::file_repository::FileRepositoryError;
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::ports::inbound::FileUseCase;
|
||||
use crate::application::ports::outbound::FileStoragePort;
|
||||
use crate::common::errors::DomainError;
|
||||
use futures::Stream;
|
||||
use bytes::Bytes;
|
||||
|
||||
/// Errores específicos del servicio de archivos
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FileServiceError {
|
||||
#[error("Archivo no encontrado: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Archivo ya existe: {0}")]
|
||||
Conflict(String),
|
||||
|
||||
#[error("Error de acceso al archivo: {0}")]
|
||||
AccessError(String),
|
||||
|
||||
#[error("Ruta de archivo inválida: {0}")]
|
||||
InvalidPath(String),
|
||||
|
||||
#[error("Error interno: {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl From<FileRepositoryError> for FileServiceError {
|
||||
fn from(err: FileRepositoryError) -> Self {
|
||||
match err {
|
||||
FileRepositoryError::NotFound(id) => FileServiceError::NotFound(id),
|
||||
FileRepositoryError::AlreadyExists(path) => FileServiceError::Conflict(path),
|
||||
FileRepositoryError::InvalidPath(path) => FileServiceError::InvalidPath(path),
|
||||
FileRepositoryError::IoError(e) => FileServiceError::AccessError(e.to_string()),
|
||||
FileRepositoryError::Timeout(msg) => FileServiceError::AccessError(format!("Operación expiró: {}", msg)),
|
||||
_ => FileServiceError::InternalError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomainError> for FileServiceError {
|
||||
fn from(err: DomainError) -> Self {
|
||||
match err.kind {
|
||||
crate::common::errors::ErrorKind::NotFound => FileServiceError::NotFound(err.to_string()),
|
||||
crate::common::errors::ErrorKind::AlreadyExists => FileServiceError::Conflict(err.to_string()),
|
||||
crate::common::errors::ErrorKind::InvalidInput => FileServiceError::InvalidPath(err.to_string()),
|
||||
crate::common::errors::ErrorKind::AccessDenied => FileServiceError::AccessError(err.to_string()),
|
||||
_ => FileServiceError::InternalError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileServiceError> for DomainError {
|
||||
fn from(err: FileServiceError) -> Self {
|
||||
match err {
|
||||
FileServiceError::NotFound(id) => DomainError::not_found("File", id),
|
||||
FileServiceError::Conflict(path) => DomainError::already_exists("File", path),
|
||||
FileServiceError::InvalidPath(path) => DomainError::validation_error("File", format!("Invalid path: {}", path)),
|
||||
FileServiceError::AccessError(msg) => DomainError::access_denied("File", msg),
|
||||
FileServiceError::InternalError(msg) => DomainError::internal_error("File", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type FileServiceResult<T> = Result<T, FileServiceError>;
|
||||
|
||||
/// Service for file operations
|
||||
pub struct FileService {
|
||||
file_repository: Arc<dyn FileRepository>,
|
||||
file_repository: Arc<dyn FileStoragePort>,
|
||||
}
|
||||
|
||||
impl FileService {
|
||||
/// Creates a new file service
|
||||
pub fn new(file_repository: Arc<dyn FileRepository>) -> Self {
|
||||
pub fn new(file_repository: Arc<dyn FileStoragePort>) -> Self {
|
||||
Self { file_repository }
|
||||
}
|
||||
|
||||
@@ -21,105 +86,103 @@ impl FileService {
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> FileRepositoryResult<FileDto>
|
||||
) -> FileServiceResult<FileDto>
|
||||
{
|
||||
let file = self.file_repository.save_file_from_bytes(name, folder_id, content_type, content).await?;
|
||||
let file = self.file_repository.save_file(name, folder_id, content_type, content).await
|
||||
.map_err(FileServiceError::from)?;
|
||||
Ok(FileDto::from(file))
|
||||
}
|
||||
|
||||
/// Gets a file by ID
|
||||
pub async fn get_file(&self, id: &str) -> FileRepositoryResult<FileDto> {
|
||||
let file = self.file_repository.get_file_by_id(id).await?;
|
||||
pub async fn get_file(&self, id: &str) -> FileServiceResult<FileDto> {
|
||||
let file = self.file_repository.get_file(id).await
|
||||
.map_err(FileServiceError::from)?;
|
||||
Ok(FileDto::from(file))
|
||||
}
|
||||
|
||||
/// Lists files in a folder
|
||||
pub async fn list_files(&self, folder_id: Option<&str>) -> FileRepositoryResult<Vec<FileDto>> {
|
||||
let files = self.file_repository.list_files(folder_id).await?;
|
||||
pub async fn list_files(&self, folder_id: Option<&str>) -> FileServiceResult<Vec<FileDto>> {
|
||||
let files = self.file_repository.list_files(folder_id).await
|
||||
.map_err(FileServiceError::from)?;
|
||||
Ok(files.into_iter().map(FileDto::from).collect())
|
||||
}
|
||||
|
||||
/// Deletes a file
|
||||
pub async fn delete_file(&self, id: &str) -> FileRepositoryResult<()> {
|
||||
pub async fn delete_file(&self, id: &str) -> FileServiceResult<()> {
|
||||
self.file_repository.delete_file(id).await
|
||||
.map_err(FileServiceError::from)
|
||||
}
|
||||
|
||||
/// Gets file content
|
||||
pub async fn get_file_content(&self, id: &str) -> FileRepositoryResult<Vec<u8>> {
|
||||
/// Gets file content as bytes - use for small files only
|
||||
pub async fn get_file_content(&self, id: &str) -> FileServiceResult<Vec<u8>> {
|
||||
self.file_repository.get_file_content(id).await
|
||||
.map_err(FileServiceError::from)
|
||||
}
|
||||
|
||||
/// Moves a file to a new folder implementing direct save with new location without deleting first
|
||||
pub async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> FileRepositoryResult<FileDto> {
|
||||
// Get the current file complete info
|
||||
let source_file = match self.file_repository.get_file_by_id(file_id).await {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
tracing::error!("Error al obtener archivo (ID: {}): {}", file_id, e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
/// Gets file content as stream - better for large files
|
||||
pub async fn get_file_stream(&self, id: &str) -> FileServiceResult<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>> {
|
||||
self.file_repository.get_file_stream(id).await
|
||||
.map_err(FileServiceError::from)
|
||||
}
|
||||
|
||||
/// Moves a file to a new folder using filesystem operations directly
|
||||
pub async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> FileServiceResult<FileDto> {
|
||||
tracing::info!("Moviendo archivo con ID: {} a carpeta: {:?}", file_id, folder_id);
|
||||
|
||||
tracing::info!("Moviendo archivo: {} (ID: {}) de carpeta: {:?} a carpeta: {:?}",
|
||||
source_file.name, file_id, source_file.folder_id, folder_id);
|
||||
|
||||
// Special handling for PDF files
|
||||
let is_pdf = source_file.name.to_lowercase().ends_with(".pdf");
|
||||
if is_pdf {
|
||||
tracing::info!("Moviendo un archivo PDF: {}", source_file.name);
|
||||
}
|
||||
|
||||
// No hacer nada si ya estamos en la carpeta de destino
|
||||
if source_file.folder_id == folder_id {
|
||||
tracing::info!("El archivo ya está en la carpeta de destino, no es necesario moverlo");
|
||||
return Ok(FileDto::from(source_file));
|
||||
}
|
||||
|
||||
// Step 1: Get file content
|
||||
tracing::info!("Leyendo contenido del archivo: {}", source_file.name);
|
||||
let content = match self.file_repository.get_file_content(file_id).await {
|
||||
Ok(content) => {
|
||||
tracing::info!("Contenido del archivo leído correctamente: {} bytes", content.len());
|
||||
content
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Error al leer el contenido del archivo {}: {}", file_id, e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 2: Save the file to the new location with a new ID
|
||||
tracing::info!("Guardando archivo en nueva ubicación: {} en carpeta: {:?}", source_file.name, folder_id);
|
||||
let new_file = match self.file_repository.save_file_from_bytes(
|
||||
source_file.name.clone(),
|
||||
folder_id.clone(),
|
||||
source_file.mime_type.clone(),
|
||||
content
|
||||
).await {
|
||||
Ok(file) => {
|
||||
tracing::info!("Archivo guardado en nueva ubicación con ID: {}", file.id);
|
||||
file
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Error al guardar archivo en nueva ubicación: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3: Only after ensuring new file is saved, try to delete the old file
|
||||
// If this fails, it's not critical - we already have the file in the new location
|
||||
tracing::info!("Eliminando archivo original con ID: {}", file_id);
|
||||
match self.file_repository.delete_file(file_id).await {
|
||||
Ok(_) => tracing::info!("Archivo original eliminado correctamente"),
|
||||
Err(e) => {
|
||||
tracing::warn!("Error al eliminar archivo original (ID: {}): {} - archivo duplicado posible", file_id, e);
|
||||
// Continue even if delete fails - at worst we'll have duplicate files
|
||||
}
|
||||
}
|
||||
// Usar la implementación eficiente del repositorio que utiliza rename
|
||||
let moved_file = self.file_repository.move_file(file_id, folder_id).await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error al mover archivo (ID: {}): {}", file_id, e);
|
||||
FileServiceError::from(e)
|
||||
})?;
|
||||
|
||||
tracing::info!("Archivo movido exitosamente: {} (ID: {}) a carpeta: {:?}",
|
||||
new_file.name, new_file.id, folder_id);
|
||||
moved_file.name(), moved_file.id(), moved_file.folder_id());
|
||||
|
||||
Ok(FileDto::from(new_file))
|
||||
Ok(FileDto::from(moved_file))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FileUseCase for FileService {
|
||||
async fn upload_file(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<FileDto, DomainError> {
|
||||
FileService::upload_file_from_bytes(self, name, folder_id, content_type, content).await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError> {
|
||||
FileService::get_file(self, id).await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError> {
|
||||
FileService::list_files(self, folder_id).await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
||||
FileService::delete_file(self, id).await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError> {
|
||||
FileService::get_file_content(self, id).await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
FileService::get_file_stream(self, id).await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError> {
|
||||
FileService::move_file(self, file_id, folder_id).await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,266 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult};
|
||||
use async_trait::async_trait;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto, FolderDto};
|
||||
use crate::application::ports::inbound::FolderUseCase;
|
||||
use crate::application::ports::outbound::FolderStoragePort;
|
||||
use crate::application::transactions::storage_transaction::StorageTransaction;
|
||||
use crate::common::errors::{DomainError, ErrorKind, ErrorContext};
|
||||
|
||||
/// Service for folder operations
|
||||
/// Implementación del caso de uso para operaciones de carpetas
|
||||
pub struct FolderService {
|
||||
folder_repository: Arc<dyn FolderRepository>,
|
||||
folder_storage: Arc<dyn FolderStoragePort>,
|
||||
}
|
||||
|
||||
impl FolderService {
|
||||
/// Creates a new folder service
|
||||
pub fn new(folder_repository: Arc<dyn FolderRepository>) -> Self {
|
||||
Self { folder_repository }
|
||||
/// Crea un nuevo servicio de carpetas
|
||||
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
|
||||
Self { folder_storage }
|
||||
}
|
||||
|
||||
/// Creates a new folder
|
||||
pub async fn create_folder(&self, dto: CreateFolderDto) -> FolderRepositoryResult<FolderDto> {
|
||||
let parent_path = match &dto.parent_id {
|
||||
Some(parent_id) => {
|
||||
let parent = self.folder_repository.get_folder_by_id(parent_id).await?;
|
||||
Some(parent.path)
|
||||
},
|
||||
None => None
|
||||
};
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FolderUseCase for FolderService {
|
||||
/// Crea una nueva carpeta
|
||||
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
|
||||
// Validación de entrada
|
||||
if dto.name.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Folder",
|
||||
"Folder name cannot be empty"
|
||||
));
|
||||
}
|
||||
|
||||
let folder = self.folder_repository.create_folder(dto.name, parent_path).await?;
|
||||
// Si se proporciona un parent_id, verificar que existe
|
||||
if let Some(parent_id) = &dto.parent_id {
|
||||
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
|
||||
if !parent_exists {
|
||||
return Err(DomainError::not_found("Folder", parent_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Crear la carpeta
|
||||
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
|
||||
.await
|
||||
.with_context(|| "Failed to create folder")?;
|
||||
|
||||
// Convertir a DTO
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Gets a folder by ID
|
||||
pub async fn get_folder(&self, id: &str) -> FolderRepositoryResult<FolderDto> {
|
||||
let folder = self.folder_repository.get_folder_by_id(id).await?;
|
||||
/// Obtiene una carpeta por su ID
|
||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {}", id))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Gets a folder by path
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_folder_by_path(&self, path: &str) -> FolderRepositoryResult<FolderDto> {
|
||||
let path_buf = PathBuf::from(path);
|
||||
let folder = self.folder_repository.get_folder_by_path(&path_buf).await?;
|
||||
/// Obtiene una carpeta por su ruta
|
||||
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
|
||||
// Convertir la ruta de string a StoragePath
|
||||
let storage_path = StoragePath::from_string(path);
|
||||
|
||||
let folder = self.folder_storage.get_folder_by_path(&storage_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder at path: {}", path))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Lists folders in a parent folder
|
||||
pub async fn list_folders(&self, parent_id: Option<&str>) -> FolderRepositoryResult<Vec<FolderDto>> {
|
||||
let folders = self.folder_repository.list_folders(parent_id).await?;
|
||||
/// Lista carpetas dentro de una carpeta padre
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
|
||||
let folders = self.folder_storage.list_folders(parent_id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to list folders in parent: {:?}", parent_id))?;
|
||||
|
||||
// Convertir a DTOs
|
||||
Ok(folders.into_iter().map(FolderDto::from).collect())
|
||||
}
|
||||
|
||||
/// Renames a folder
|
||||
pub async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> FolderRepositoryResult<FolderDto> {
|
||||
let folder = self.folder_repository.rename_folder(id, dto.name).await?;
|
||||
/// Lista carpetas con paginación
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
|
||||
// Validar y ajustar la paginación
|
||||
let pagination = pagination.validate_and_adjust();
|
||||
|
||||
// Obtener carpetas paginadas y conteo total
|
||||
let (folders, total_items) = self.folder_storage.list_folders_paginated(
|
||||
parent_id,
|
||||
pagination.offset(),
|
||||
pagination.limit(),
|
||||
true // Siempre incluir total para mejor UX
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to list folders with pagination in parent: {:?}", parent_id))?;
|
||||
|
||||
// El total es necesario para calcular la paginación
|
||||
let total = total_items.unwrap_or(folders.len());
|
||||
|
||||
// Convertir a PaginatedResponseDto
|
||||
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
|
||||
folders.into_iter().map(FolderDto::from).collect(),
|
||||
pagination.page,
|
||||
pagination.page_size,
|
||||
total
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Renombra una carpeta
|
||||
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> {
|
||||
// Validación de entrada
|
||||
if dto.name.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Folder",
|
||||
"New folder name cannot be empty"
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que la carpeta existe
|
||||
let existing_folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {} for renaming", id))?;
|
||||
|
||||
// Crear transacción para renombrar
|
||||
let mut transaction = StorageTransaction::new("rename_folder");
|
||||
|
||||
// Operación principal: renombrar carpeta
|
||||
// Clone all values to avoid lifetime issues
|
||||
let folder_storage = self.folder_storage.clone();
|
||||
let id_owned = id.to_string();
|
||||
let name_owned = dto.name.clone();
|
||||
|
||||
// Create future with owned values
|
||||
let rename_op = async move {
|
||||
folder_storage.rename_folder(&id_owned, name_owned).await?;
|
||||
Ok(())
|
||||
};
|
||||
let rollback_op = {
|
||||
let original_name = existing_folder.name().to_string();
|
||||
let storage = self.folder_storage.clone();
|
||||
let id_clone = id.to_string();
|
||||
|
||||
async move {
|
||||
// En caso de fallo, restaurar el nombre original
|
||||
storage.rename_folder(&id_clone, original_name).await
|
||||
.map(|_| ())
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Failed to rollback folder rename: {}", e)
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Añadir a la transacción
|
||||
transaction.add_operation(rename_op, rollback_op);
|
||||
|
||||
// Ejecutar transacción
|
||||
transaction.commit().await?;
|
||||
|
||||
// Obtener la carpeta renombrada
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get renamed folder with ID: {}", id))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Moves a folder to a new parent
|
||||
pub async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> FolderRepositoryResult<FolderDto> {
|
||||
let folder = self.folder_repository.move_folder(id, dto.parent_id.as_deref()).await?;
|
||||
/// Mueve una carpeta a un nuevo padre
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
|
||||
// Verificar que la carpeta origen existe
|
||||
let source_folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {} for moving", id))?;
|
||||
|
||||
// Si se especifica un parent_id, verificar que existe
|
||||
if let Some(parent_id) = &dto.parent_id {
|
||||
// Verificar que no estamos intentando mover la carpeta a sí misma o a uno de sus descendientes
|
||||
if parent_id == id {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Folder",
|
||||
"Cannot move a folder into itself"
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que el destino existe
|
||||
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
|
||||
if !parent_exists {
|
||||
return Err(DomainError::not_found("Folder", parent_id));
|
||||
}
|
||||
|
||||
// TODO: Idealmente deberíamos verificar toda la jerarquía para evitar ciclos
|
||||
}
|
||||
|
||||
// Crear transacción para mover
|
||||
let mut transaction = StorageTransaction::new("move_folder");
|
||||
|
||||
// Operación principal: mover carpeta
|
||||
// Clone all values to avoid lifetime issues
|
||||
let folder_storage = self.folder_storage.clone();
|
||||
let id_owned = id.to_string();
|
||||
// Get parent ID as owned string or None
|
||||
let parent_id_owned = dto.parent_id.as_ref().map(|p| p.to_string());
|
||||
|
||||
// Create future with owned values
|
||||
let move_op = async move {
|
||||
// Convert Option<String> to Option<&str>
|
||||
let parent_ref = parent_id_owned.as_deref();
|
||||
folder_storage.move_folder(&id_owned, parent_ref).await?;
|
||||
Ok(())
|
||||
};
|
||||
let rollback_op = {
|
||||
let original_parent_id = source_folder.parent_id().map(String::from);
|
||||
let storage = self.folder_storage.clone();
|
||||
let id_clone = id.to_string();
|
||||
|
||||
async move {
|
||||
// En caso de fallo, restaurar la ubicación original
|
||||
storage.move_folder(&id_clone, original_parent_id.as_deref()).await
|
||||
.map(|_| ())
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Failed to rollback folder move: {}", e)
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Añadir a la transacción
|
||||
transaction.add_operation(move_op, rollback_op);
|
||||
|
||||
// Ejecutar transacción
|
||||
transaction.commit().await?;
|
||||
|
||||
// Obtener la carpeta movida
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get moved folder with ID: {}", id))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Deletes a folder
|
||||
pub async fn delete_folder(&self, id: &str) -> FolderRepositoryResult<()> {
|
||||
self.folder_repository.delete_folder(id).await
|
||||
/// Elimina una carpeta
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
||||
// Verificar que la carpeta existe
|
||||
let _folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {} for deletion", id))?;
|
||||
|
||||
// En una implementación real, podríamos verificar permisos, dependencias, etc.
|
||||
|
||||
// Eliminar la carpeta
|
||||
self.folder_storage.delete_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete folder with ID: {}", id))
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod file_service;
|
||||
pub mod folder_service;
|
||||
pub mod i18n_application_service;
|
||||
pub mod storage_mediator;
|
||||
pub mod batch_operations;
|
||||
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryError};
|
||||
use crate::domain::repositories::file_repository::FileRepositoryError;
|
||||
use crate::domain::services::path_service::{PathService, StoragePath};
|
||||
use crate::application::ports::outbound::IdMappingPort;
|
||||
|
||||
/// Errores específicos del mediador de almacenamiento
|
||||
#[derive(Debug, Error)]
|
||||
pub enum StorageMediatorError {
|
||||
#[error("Entidad no encontrada: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Entidad ya existe: {0}")]
|
||||
AlreadyExists(String),
|
||||
|
||||
#[error("Ruta inválida: {0}")]
|
||||
InvalidPath(String),
|
||||
|
||||
#[error("Error de acceso: {0}")]
|
||||
AccessError(String),
|
||||
|
||||
#[error("Error interno: {0}")]
|
||||
InternalError(String),
|
||||
|
||||
#[error("Error de dominio: {0}")]
|
||||
DomainError(#[from] crate::common::errors::DomainError),
|
||||
}
|
||||
|
||||
impl From<FolderRepositoryError> for StorageMediatorError {
|
||||
fn from(err: FolderRepositoryError) -> Self {
|
||||
match err {
|
||||
FolderRepositoryError::NotFound(id) => StorageMediatorError::NotFound(id),
|
||||
FolderRepositoryError::AlreadyExists(path) => StorageMediatorError::AlreadyExists(path),
|
||||
FolderRepositoryError::InvalidPath(path) => StorageMediatorError::InvalidPath(path),
|
||||
FolderRepositoryError::IoError(e) => StorageMediatorError::AccessError(e.to_string()),
|
||||
_ => StorageMediatorError::InternalError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileRepositoryError> for StorageMediatorError {
|
||||
fn from(err: FileRepositoryError) -> Self {
|
||||
match err {
|
||||
FileRepositoryError::NotFound(id) => StorageMediatorError::NotFound(id),
|
||||
FileRepositoryError::AlreadyExists(path) => StorageMediatorError::AlreadyExists(path),
|
||||
FileRepositoryError::InvalidPath(path) => StorageMediatorError::InvalidPath(path),
|
||||
FileRepositoryError::IoError(e) => StorageMediatorError::AccessError(e.to_string()),
|
||||
_ => StorageMediatorError::InternalError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tipo de resultado para las operaciones del mediador
|
||||
pub type StorageMediatorResult<T> = Result<T, StorageMediatorError>;
|
||||
|
||||
/// Interfaz del servicio mediador entre repositorios de archivos y carpetas
|
||||
#[async_trait]
|
||||
pub trait StorageMediator: Send + Sync + 'static {
|
||||
/// Obtiene la ruta de una carpeta por su ID
|
||||
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf>;
|
||||
|
||||
/// Obtiene la ruta de dominio de una carpeta por su ID
|
||||
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>;
|
||||
|
||||
/// Obtiene todos los detalles de una carpeta por su ID
|
||||
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>;
|
||||
|
||||
/// Verifica si existe un archivo en una ruta específica
|
||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
||||
|
||||
/// Verifica si existe un archivo en una ruta de dominio específica
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
|
||||
|
||||
/// Verifica si existe una carpeta en una ruta específica
|
||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
||||
|
||||
/// Verifica si existe una carpeta en una ruta de dominio específica
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
|
||||
|
||||
/// Resuelve una ruta relativa a absoluta (legacy)
|
||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf;
|
||||
|
||||
/// Resuelve una ruta de dominio a una ruta física absoluta
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf;
|
||||
|
||||
/// Crea un directorio si no existe (legacy)
|
||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
|
||||
|
||||
/// Crea un directorio si no existe
|
||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>;
|
||||
}
|
||||
|
||||
/// Implementación concreta del mediador de almacenamiento
|
||||
pub struct FileSystemStorageMediator {
|
||||
folder_repository: Arc<dyn FolderRepository>,
|
||||
path_service: Arc<PathService>,
|
||||
id_mapping: Arc<dyn IdMappingPort>,
|
||||
}
|
||||
|
||||
impl FileSystemStorageMediator {
|
||||
pub fn new(folder_repository: Arc<dyn FolderRepository>, path_service: Arc<PathService>, id_mapping: Arc<dyn IdMappingPort>) -> Self {
|
||||
Self { folder_repository, path_service, id_mapping }
|
||||
}
|
||||
|
||||
/// Creates a stub implementation for initialization bootstrapping
|
||||
pub fn new_stub() -> StubStorageMediator {
|
||||
StubStorageMediator::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub implementation for initialization dependency issues
|
||||
pub struct StubStorageMediator {
|
||||
#[allow(dead_code)]
|
||||
_path_service: Arc<PathService>,
|
||||
}
|
||||
|
||||
impl StubStorageMediator {
|
||||
pub fn new() -> Self {
|
||||
let root_path = PathBuf::from("/tmp");
|
||||
let path_service = Arc::new(PathService::new(root_path));
|
||||
Self { _path_service: path_service }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageMediator for StubStorageMediator {
|
||||
async fn get_folder_path(&self, _folder_id: &str) -> StorageMediatorResult<PathBuf> {
|
||||
// Return a stub path
|
||||
Ok(PathBuf::from("/tmp"))
|
||||
}
|
||||
|
||||
async fn get_folder_storage_path(&self, _folder_id: &str) -> StorageMediatorResult<StoragePath> {
|
||||
// Return a stub storage path
|
||||
Ok(StoragePath::root())
|
||||
}
|
||||
|
||||
async fn get_folder(&self, _folder_id: &str) -> StorageMediatorResult<Folder> {
|
||||
// This is a stub that should never be called during initialization
|
||||
Err(StorageMediatorError::NotFound("Stub not implemented".to_string()))
|
||||
}
|
||||
|
||||
async fn file_exists_at_path(&self, _path: &Path) -> StorageMediatorResult<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn file_exists_at_storage_path(&self, _storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn folder_exists_at_path(&self, _path: &Path) -> StorageMediatorResult<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn folder_exists_at_storage_path(&self, _storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn resolve_path(&self, _relative_path: &Path) -> PathBuf {
|
||||
PathBuf::from("/tmp")
|
||||
}
|
||||
|
||||
fn resolve_storage_path(&self, _storage_path: &StoragePath) -> PathBuf {
|
||||
PathBuf::from("/tmp")
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, _path: &Path) -> StorageMediatorResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_storage_directory(&self, _storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageMediator for FileSystemStorageMediator {
|
||||
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf> {
|
||||
let folder = self.folder_repository.get_folder_by_id(folder_id).await
|
||||
.map_err(StorageMediatorError::from)?;
|
||||
|
||||
// Need to get the path from folder ID
|
||||
let storage_path = self.id_mapping.get_path_by_id(folder.id()).await
|
||||
.map_err(StorageMediatorError::from)?;
|
||||
|
||||
// Convert StoragePath to PathBuf
|
||||
let path_buf = self.path_service.resolve_path(&storage_path);
|
||||
Ok(path_buf)
|
||||
}
|
||||
|
||||
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath> {
|
||||
let folder = self.folder_repository.get_folder_by_id(folder_id).await
|
||||
.map_err(StorageMediatorError::from)?;
|
||||
|
||||
// Get path by folder ID - will already be a StoragePath
|
||||
let storage_path = self.id_mapping.get_path_by_id(folder.id()).await
|
||||
.map_err(StorageMediatorError::from)?;
|
||||
|
||||
Ok(storage_path)
|
||||
}
|
||||
|
||||
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder> {
|
||||
let folder = self.folder_repository.get_folder_by_id(folder_id).await
|
||||
.map_err(StorageMediatorError::from)?;
|
||||
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(path);
|
||||
|
||||
// Verificar si existe como archivo (no como directorio)
|
||||
let exists = abs_path.exists() && abs_path.is_file();
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_storage_path(storage_path);
|
||||
|
||||
// Verificar si existe como archivo (no como directorio)
|
||||
let exists = abs_path.exists() && abs_path.is_file();
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(path);
|
||||
|
||||
// Verificar si existe como directorio
|
||||
let exists = abs_path.exists() && abs_path.is_dir();
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_storage_path(storage_path);
|
||||
|
||||
// Verificar si existe como directorio
|
||||
let exists = abs_path.exists() && abs_path.is_dir();
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf {
|
||||
// Legacy method using PathBuf
|
||||
let path_str = relative_path.to_string_lossy().to_string();
|
||||
let storage_path = StoragePath::from_string(&path_str);
|
||||
self.path_service.resolve_path(&storage_path)
|
||||
}
|
||||
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.path_service.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
|
||||
let abs_path = self.resolve_path(path);
|
||||
|
||||
// Crear directorios si no existen
|
||||
if !abs_path.exists() {
|
||||
tokio::fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||
let abs_path = self.resolve_storage_path(storage_path);
|
||||
|
||||
// Crear directorios si no existen
|
||||
if !abs_path.exists() {
|
||||
tokio::fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod storage_transaction;
|
||||
@@ -0,0 +1,131 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Tipo para operaciones y rollbacks asíncronos
|
||||
type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>;
|
||||
|
||||
/// Transacción para operaciones de almacenamiento
|
||||
/// Permite definir un conjunto de operaciones y sus rollbacks correspondientes
|
||||
pub struct StorageTransaction {
|
||||
/// Operaciones a ejecutar
|
||||
operations: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
|
||||
/// Operaciones de rollback para revertir cambios en caso de error
|
||||
rollbacks: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
|
||||
/// Nombre de la transacción para logging
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl StorageTransaction {
|
||||
/// Crea una nueva transacción
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
operations: Vec::new(),
|
||||
rollbacks: Vec::new(),
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Añade una operación a la transacción con su correspondiente rollback
|
||||
pub fn add_operation<F, R>(&mut self, operation: F, rollback: R)
|
||||
where
|
||||
F: Future<Output = Result<(), DomainError>> + Send + 'static,
|
||||
R: Future<Output = Result<(), DomainError>> + Send + 'static,
|
||||
{
|
||||
self.operations.push(Box::new(move || Box::pin(operation)));
|
||||
self.rollbacks.push(Box::new(move || Box::pin(rollback)));
|
||||
}
|
||||
|
||||
/// Añade una operación sin rollback (para limpieza o logging)
|
||||
#[allow(dead_code)]
|
||||
pub fn add_finalizer<F>(&mut self, finalizer: F)
|
||||
where
|
||||
F: Future<Output = Result<(), DomainError>> + Send + 'static,
|
||||
{
|
||||
// El rollback es una operación nula
|
||||
let noop = async { Ok(()) };
|
||||
|
||||
self.operations.push(Box::new(move || Box::pin(finalizer)));
|
||||
self.rollbacks.push(Box::new(move || Box::pin(noop)));
|
||||
}
|
||||
|
||||
/// Ejecuta la transacción aplicando todas las operaciones en orden
|
||||
/// Si alguna falla, ejecuta los rollbacks en orden inverso
|
||||
pub async fn commit(mut self) -> Result<(), DomainError> {
|
||||
tracing::debug!("Iniciando transacción: {}", self.name);
|
||||
|
||||
let mut completed_ops = Vec::new();
|
||||
|
||||
// Extraer operaciones para evitar problemas de propiedad
|
||||
let operations = std::mem::take(&mut self.operations);
|
||||
let transaction_name = self.name.clone();
|
||||
|
||||
// Ejecutar operaciones
|
||||
for (i, op) in operations.into_iter().enumerate() {
|
||||
match op().await {
|
||||
Ok(()) => {
|
||||
completed_ops.push(i);
|
||||
tracing::trace!("Operación {} completada en transacción: {}", i, transaction_name);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error en operación {} de transacción {}: {}", i, transaction_name, e);
|
||||
|
||||
// Ejecutar rollbacks para las operaciones completadas en orden inverso
|
||||
self.rollback(completed_ops).await?;
|
||||
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Transaction",
|
||||
format!("Falló la transacción '{}': {}", transaction_name, e)
|
||||
).with_source(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Transacción completada exitosamente: {}", transaction_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ejecuta rollbacks para las operaciones completadas
|
||||
async fn rollback(mut self, completed_ops: Vec<usize>) -> Result<(), DomainError> {
|
||||
tracing::warn!("Iniciando rollback para transacción: {}", self.name);
|
||||
|
||||
let mut rollback_errors = Vec::new();
|
||||
|
||||
// Extraer rollbacks para evitar problemas de propiedad
|
||||
let mut rollbacks = Vec::new();
|
||||
std::mem::swap(&mut rollbacks, &mut self.rollbacks);
|
||||
|
||||
// Ejecutar rollbacks en orden inverso
|
||||
for i in completed_ops.into_iter().rev() {
|
||||
if i < rollbacks.len() {
|
||||
// Tomar propiedad del rollback (obtener una referencia mutable)
|
||||
if let Some(rb) = rollbacks.get_mut(i) {
|
||||
// Intercambiar con una función vacía
|
||||
let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) })));
|
||||
if let Err(e) = rollback().await {
|
||||
tracing::error!("Error en rollback de operación {} en transacción {}: {}",
|
||||
i, self.name, e);
|
||||
rollback_errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si hubo errores en el rollback, reportarlos
|
||||
if !rollback_errors.is_empty() {
|
||||
tracing::error!("Errores durante rollback de transacción {}: {} errores",
|
||||
self.name, rollback_errors.len());
|
||||
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Transaction",
|
||||
format!("Errores durante rollback de transacción '{}': {} errores",
|
||||
self.name, rollback_errors.len())
|
||||
));
|
||||
}
|
||||
|
||||
tracing::info!("Rollback de transacción completado: {}", self.name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user