adding several features

This commit is contained in:
DioCrafts
2025-03-19 00:44:27 +01:00
parent 7157762540
commit d9bbd575d2
66 changed files with 12137 additions and 1055 deletions
+27 -8
View File
@@ -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
)
}
}
+24 -7
View File
@@ -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
View File
@@ -1,4 +1,5 @@
pub mod file_dto;
pub mod folder_dto;
pub mod i18n_dto;
pub mod pagination;
+117
View File
@@ -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,
}
}
}
+2
View File
@@ -1,3 +1,5 @@
pub mod dtos;
pub mod ports;
pub mod services;
pub mod transactions;
+77
View File
@@ -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>;
}
+2
View File
@@ -0,0 +1,2 @@
pub mod inbound;
pub mod outbound;
+118
View File
@@ -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);
}
}
+143 -80
View File
@@ -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)
}
}
+237 -39
View File
@@ -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))
}
}
+2
View File
@@ -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(())
}
}
+1
View File
@@ -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(())
}
}