diff --git a/README.md b/README.md index 488a8c81..cef9ff48 100644 --- a/README.md +++ b/README.md @@ -24,21 +24,74 @@ OxiCloud incorporates multiple advanced performance optimizations: - **Parallel File Processing**: Automatically splits large files into chunks for parallel processing - **Asynchronous I/O**: Built on Tokio for non-blocking operations - **Worker Pools**: Smart thread management for optimal resource utilization +- **Timeout Management**: Strategic timeouts to prevent resource exhaustion ### Intelligent Caching - **File Metadata Cache**: Drastically reduces filesystem calls - **Smart Cache Invalidation**: Selectively invalidates cache entries - **Preloading**: Strategic preloading for frequently accessed directories +- **TTL-Based Cache**: Time-based expiration for optimal memory usage ### I/O Optimization -- **Buffer Pooling**: Reuses memory buffers to reduce GC pressure +- **Buffer Pooling**: Reuses memory buffers to reduce allocation pressure - **Adaptive Streaming**: Adjusts chunk sizes based on file size - **Size-Based Processing**: Different strategies for small, medium, and large files +- **Non-Blocking Filesystem Operations**: Prevents I/O bottlenecks ### Batch Processing - **ID Mapping Optimizer**: Groups mapping operations to reduce overhead - **Operation Batching**: Processes multiple file operations concurrently - **Debounced Saving**: Groups write operations for optimal I/O +- **Parallel Directory Scanning**: Efficient directory traversal + +## 馃 Advanced Technical Features + +### Clean Architecture Implementation +- **Hexagonal/Ports and Adapters Pattern**: Clear separation between domain, application, and infrastructure +- **Dependency Inversion**: Domain business rules are independent of external frameworks +- **Explicit Dependency Injection**: Manual, type-safe DI without heavy frameworks + +### Advanced Error Handling +- **Domain-Specific Error Types**: Granular error classification with context +- **Error Propagation Chain**: Preserves context through abstraction layers +- **Custom Error Context**: Enriches errors with additional information +- **Source Tracking**: Errors maintain their original source for debugging + +### Robust Repository Pattern +- **Persistence Abstraction**: Domain layer completely isolated from storage details +- **Repository Interfaces**: Defined in domain layer and implemented in infrastructure +- **Storage Mediator Pattern**: Coordinates interactions between repositories +- **ID Mapping Service**: Decouples domain identifiers from filesystem paths + +### Transaction Management +- **Atomic Operations**: Entity-level transaction support +- **Pending Changes System**: Batches persistence operations for efficiency +- **Rollback Capabilities**: Reverts state on failed operations +- **Optimistic Concurrency**: Protects against concurrent modifications + +### Advanced File System Handling +- **Parallel Processing for Large Files**: Chunked operations for efficient I/O +- **Specialized Strategies**: Different handlers for various file sizes +- **Timeout-Protected Operations**: Prevents hanging on problematic files +- **Background Processing**: Heavy operations offloaded to background tasks + +### Memory Efficiency +- **Buffer Pool Manager**: Reuses allocated memory to reduce fragmentation +- **Streaming I/O**: Processing large files without loading entirely into memory +- **Resource-Aware Processing**: Adapts resource usage based on file size +- **Lazy Loading**: Loads data only when needed + +### Defensive Programming +- **Extensive Input Validation**: Domain entities enforce business rules +- **Immutable Data Structures**: Prevents unexpected state mutations +- **Fail-Fast Operations**: Early validation to prevent cascading failures +- **Extensive Logging**: Structured logs with contextual information + +### Service Layer Optimizations +- **Application Services**: Orchestrate use cases with domain entities +- **Transaction Coordination**: Ensures data consistency across operations +- **Domain Service Specialization**: Services focused on specific domain concerns +- **Cross-Cutting Concerns**: Separated into dedicated middleware components ## 馃摳 Screenshots diff --git a/src/application/mod.rs b/src/application/mod.rs index f13be734..69d133bc 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -3,3 +3,8 @@ pub mod ports; pub mod services; pub mod transactions; +// Re-exportaciones para facilitar el acceso a los principales puertos +pub use ports::inbound::FolderUseCase; +pub use ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory}; +pub use ports::outbound::{FolderStoragePort, IdMappingPort}; +pub use ports::storage_ports::{FileReadPort, FileWritePort, FilePathResolutionPort, StorageVerificationPort, DirectoryManagementPort}; diff --git a/src/application/ports/file_ports.rs b/src/application/ports/file_ports.rs new file mode 100644 index 00000000..0052c01c --- /dev/null +++ b/src/application/ports/file_ports.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; +use async_trait::async_trait; +use bytes::Bytes; +use futures::Stream; + +use crate::application::dtos::file_dto::FileDto; +use crate::common::errors::DomainError; + +/// Puerto primario para operaciones de subida de archivos +#[async_trait] +pub trait FileUploadUseCase: Send + Sync + 'static { + /// Sube un nuevo archivo desde bytes + async fn upload_file( + &self, + name: String, + folder_id: Option, + content_type: String, + content: Vec, + ) -> Result; +} + +/// Puerto primario para operaciones de recuperaci贸n de archivos +#[async_trait] +pub trait FileRetrievalUseCase: Send + Sync + 'static { + /// Obtiene un archivo por su ID + async fn get_file(&self, id: &str) -> Result; + + /// Lista archivos en una carpeta + async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError>; + + /// Obtiene contenido de archivo como bytes (para archivos peque帽os) + async fn get_file_content(&self, id: &str) -> Result, DomainError>; + + /// Obtiene contenido de archivo como stream (para archivos grandes) + async fn get_file_stream(&self, id: &str) -> Result> + Send>, DomainError>; +} + +/// Puerto primario para operaciones de gesti贸n de archivos +#[async_trait] +pub trait FileManagementUseCase: Send + Sync + 'static { + /// Mueve un archivo a otra carpeta + async fn move_file(&self, file_id: &str, folder_id: Option) -> Result; + + /// Elimina un archivo + async fn delete_file(&self, id: &str) -> Result<(), DomainError>; +} + +/// Factory para crear implementaciones de casos de uso de archivos +pub trait FileUseCaseFactory { + fn create_file_upload_use_case(&self) -> Arc; + fn create_file_retrieval_use_case(&self) -> Arc; + fn create_file_management_use_case(&self) -> Arc; +} \ No newline at end of file diff --git a/src/application/ports/mod.rs b/src/application/ports/mod.rs index 216f0b5c..1ce9d878 100644 --- a/src/application/ports/mod.rs +++ b/src/application/ports/mod.rs @@ -1,2 +1,4 @@ pub mod inbound; -pub mod outbound; \ No newline at end of file +pub mod outbound; +pub mod file_ports; +pub mod storage_ports; \ No newline at end of file diff --git a/src/application/ports/storage_ports.rs b/src/application/ports/storage_ports.rs new file mode 100644 index 00000000..a0b0a26c --- /dev/null +++ b/src/application/ports/storage_ports.rs @@ -0,0 +1,70 @@ +use std::path::PathBuf; +use async_trait::async_trait; +use bytes::Bytes; +use futures::Stream; + +use crate::domain::entities::file::File; +use crate::domain::services::path_service::StoragePath; +use crate::common::errors::DomainError; + +/// Puerto secundario para lectura de archivos +#[async_trait] +pub trait FileReadPort: Send + Sync + 'static { + /// Obtiene un archivo por su ID + async fn get_file(&self, id: &str) -> Result; + + /// Lista archivos en una carpeta + async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError>; + + /// Obtiene contenido de archivo como bytes + async fn get_file_content(&self, id: &str) -> Result, DomainError>; + + /// Obtiene contenido de archivo como stream + async fn get_file_stream(&self, id: &str) -> Result> + Send>, DomainError>; +} + +/// Puerto secundario para escritura de archivos +#[async_trait] +pub trait FileWritePort: Send + Sync + 'static { + /// Guarda un nuevo archivo desde bytes + async fn save_file( + &self, + name: String, + folder_id: Option, + content_type: String, + content: Vec, + ) -> Result; + + /// Mueve un archivo a otra carpeta + async fn move_file(&self, file_id: &str, target_folder_id: Option) -> Result; + + /// Elimina un archivo + async fn delete_file(&self, id: &str) -> Result<(), DomainError>; +} + +/// Puerto secundario para resoluci贸n de rutas de archivos +#[async_trait] +pub trait FilePathResolutionPort: Send + Sync + 'static { + /// Obtiene la ruta de almacenamiento de un archivo + async fn get_file_path(&self, id: &str) -> Result; + + /// Resuelve una ruta de dominio a una ruta f铆sica + fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf; +} + +/// Puerto secundario para verificaci贸n de existencia de archivos/directorios +#[async_trait] +pub trait StorageVerificationPort: Send + Sync + 'static { + /// Verifica si existe un archivo en la ruta dada + async fn file_exists(&self, storage_path: &StoragePath) -> Result; + + /// Verifica si existe un directorio en la ruta dada + async fn directory_exists(&self, storage_path: &StoragePath) -> Result; +} + +/// Puerto secundario para gesti贸n de directorios +#[async_trait] +pub trait DirectoryManagementPort: Send + Sync + 'static { + /// Crea directorios si no existen + async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>; +} \ No newline at end of file diff --git a/src/application/services/file_management_service.rs b/src/application/services/file_management_service.rs new file mode 100644 index 00000000..8eb9d0d2 --- /dev/null +++ b/src/application/services/file_management_service.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; +use async_trait::async_trait; + +use crate::application::dtos::file_dto::FileDto; +use crate::application::ports::file_ports::FileManagementUseCase; +use crate::application::ports::storage_ports::FileWritePort; +use crate::common::errors::DomainError; + +/// Servicio para operaciones de gesti贸n de archivos +pub struct FileManagementService { + file_repository: Arc, +} + +impl FileManagementService { + /// Crea un nuevo servicio de gesti贸n de archivos + pub fn new(file_repository: Arc) -> Self { + Self { file_repository } + } +} + +#[async_trait] +impl FileManagementUseCase for FileManagementService { + async fn move_file(&self, file_id: &str, folder_id: Option) -> Result { + tracing::info!("Moviendo archivo con ID: {} a carpeta: {:?}", file_id, folder_id); + + 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); + e + })?; + + tracing::info!("Archivo movido exitosamente: {} (ID: {}) a carpeta: {:?}", + moved_file.name(), moved_file.id(), moved_file.folder_id()); + + Ok(FileDto::from(moved_file)) + } + + async fn delete_file(&self, id: &str) -> Result<(), DomainError> { + self.file_repository.delete_file(id).await + } +} \ No newline at end of file diff --git a/src/application/services/file_retrieval_service.rs b/src/application/services/file_retrieval_service.rs new file mode 100644 index 00000000..55bdd5c8 --- /dev/null +++ b/src/application/services/file_retrieval_service.rs @@ -0,0 +1,42 @@ +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::ports::file_ports::FileRetrievalUseCase; +use crate::application::ports::storage_ports::FileReadPort; +use crate::common::errors::DomainError; + +/// Servicio para operaciones de recuperaci贸n de archivos +pub struct FileRetrievalService { + file_repository: Arc, +} + +impl FileRetrievalService { + /// Crea un nuevo servicio de recuperaci贸n de archivos + pub fn new(file_repository: Arc) -> Self { + Self { file_repository } + } +} + +#[async_trait] +impl FileRetrievalUseCase for FileRetrievalService { + async fn get_file(&self, id: &str) -> Result { + let file = self.file_repository.get_file(id).await?; + Ok(FileDto::from(file)) + } + + async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError> { + let files = self.file_repository.list_files(folder_id).await?; + Ok(files.into_iter().map(FileDto::from).collect()) + } + + async fn get_file_content(&self, id: &str) -> Result, DomainError> { + self.file_repository.get_file_content(id).await + } + + async fn get_file_stream(&self, id: &str) -> Result> + Send>, DomainError> { + self.file_repository.get_file_stream(id).await + } +} \ No newline at end of file diff --git a/src/application/services/file_upload_service.rs b/src/application/services/file_upload_service.rs new file mode 100644 index 00000000..4436cf17 --- /dev/null +++ b/src/application/services/file_upload_service.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; +use async_trait::async_trait; + +use crate::application::dtos::file_dto::FileDto; +use crate::application::ports::file_ports::FileUploadUseCase; +use crate::application::ports::storage_ports::FileWritePort; +use crate::common::errors::DomainError; + +/// Servicio para operaciones de subida de archivos +pub struct FileUploadService { + file_repository: Arc, +} + +impl FileUploadService { + /// Crea un nuevo servicio de subida de archivos + pub fn new(file_repository: Arc) -> Self { + Self { file_repository } + } +} + +#[async_trait] +impl FileUploadUseCase for FileUploadService { + async fn upload_file( + &self, + name: String, + folder_id: Option, + content_type: String, + content: Vec, + ) -> Result { + let file = self.file_repository.save_file(name, folder_id, content_type, content).await?; + Ok(FileDto::from(file)) + } +} \ No newline at end of file diff --git a/src/application/services/file_use_case_factory.rs b/src/application/services/file_use_case_factory.rs new file mode 100644 index 00000000..c26f5b17 --- /dev/null +++ b/src/application/services/file_use_case_factory.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use crate::application::ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory}; +use crate::application::services::file_upload_service::FileUploadService; +use crate::application::services::file_retrieval_service::FileRetrievalService; +use crate::application::services::file_management_service::FileManagementService; +use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; + +/// Factory para crear implementaciones de casos de uso de archivos +pub struct AppFileUseCaseFactory { + file_read_repository: Arc, + file_write_repository: Arc, +} + +impl AppFileUseCaseFactory { + /// Crea una nueva factory para casos de uso de archivos + pub fn new( + file_read_repository: Arc, + file_write_repository: Arc + ) -> Self { + Self { + file_read_repository, + file_write_repository, + } + } +} + +impl FileUseCaseFactory for AppFileUseCaseFactory { + fn create_file_upload_use_case(&self) -> Arc { + Arc::new(FileUploadService::new(self.file_write_repository.clone())) + } + + fn create_file_retrieval_use_case(&self) -> Arc { + Arc::new(FileRetrievalService::new(self.file_read_repository.clone())) + } + + fn create_file_management_use_case(&self) -> Arc { + Arc::new(FileManagementService::new(self.file_write_repository.clone())) + } +} \ No newline at end of file diff --git a/src/application/services/mod.rs b/src/application/services/mod.rs index 25f07248..d853d9ba 100644 --- a/src/application/services/mod.rs +++ b/src/application/services/mod.rs @@ -1,6 +1,17 @@ +pub mod batch_operations; pub mod file_service; pub mod folder_service; pub mod i18n_application_service; pub mod storage_mediator; -pub mod batch_operations; +// Nuevos servicios refactorizados +pub mod file_upload_service; +pub mod file_retrieval_service; +pub mod file_management_service; +pub mod file_use_case_factory; + +// Re-exportar para facilitar acceso +pub use file_upload_service::FileUploadService; +pub use file_retrieval_service::FileRetrievalService; +pub use file_management_service::FileManagementService; +pub use file_use_case_factory::AppFileUseCaseFactory; diff --git a/src/application/services/storage_mediator.rs b/src/application/services/storage_mediator.rs index 7302475e..9c8be132 100644 --- a/src/application/services/storage_mediator.rs +++ b/src/application/services/storage_mediator.rs @@ -1,5 +1,6 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::sync::RwLock; use async_trait::async_trait; use thiserror::Error; @@ -97,9 +98,9 @@ pub trait StorageMediator: Send + Sync + 'static { /// Implementaci贸n concreta del mediador de almacenamiento pub struct FileSystemStorageMediator { - folder_repository: Arc, - path_service: Arc, - id_mapping: Arc, + pub folder_repository: Arc, + pub path_service: Arc, + pub id_mapping: Arc, } impl FileSystemStorageMediator { @@ -111,6 +112,86 @@ impl FileSystemStorageMediator { pub fn new_stub() -> StubStorageMediator { StubStorageMediator::new() } + + /// Overload para implementar inicializaci贸n diferida con repository placeholder + pub fn new_with_lazy_folder( + folder_repository: Arc>>>, + path_service: Arc, + id_mapping: Arc + ) -> Self { + // Create temporary stub repository + let temp_repo = Arc::new(FolderRepositoryStub {}); + + Self { + folder_repository: temp_repo, + path_service, + id_mapping, + } + } +} + +/// Stub repository for initialization +#[derive(Debug)] +pub struct FolderRepositoryStub {} + +#[async_trait] +impl FolderRepository for FolderRepositoryStub { + async fn create_folder(&self, _name: String, _parent_id: Option) -> Result { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn get_folder_by_id(&self, _id: &str) -> Result { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn get_folder_by_storage_path(&self, _storage_path: &StoragePath) -> Result { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn list_folders(&self, _parent_id: Option<&str>) -> Result, FolderRepositoryError> { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn list_folders_paginated( + &self, + _parent_id: Option<&str>, + _offset: usize, + _limit: usize, + _include_total: bool + ) -> Result<(Vec, Option), FolderRepositoryError> { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn rename_folder(&self, _id: &str, _new_name: String) -> Result { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn move_folder(&self, _id: &str, _new_parent_id: Option<&str>) -> Result { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn delete_folder(&self, _id: &str) -> Result<(), FolderRepositoryError> { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } + + async fn folder_exists_at_storage_path(&self, _storage_path: &StoragePath) -> Result { + Ok(false) + } + + async fn get_folder_storage_path(&self, _id: &str) -> Result { + Ok(StoragePath::root()) + } + + // Legacy methods + #[allow(deprecated)] + async fn folder_exists(&self, _path: &std::path::PathBuf) -> Result { + Ok(false) + } + + #[allow(deprecated)] + async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> Result { + Err(FolderRepositoryError::Other("Stub repository".to_string())) + } } /// Stub implementation for initialization dependency issues diff --git a/src/common/config.rs b/src/common/config.rs index 3a4ed3a9..07b84e91 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -1,5 +1,26 @@ use std::time::Duration; +/// Configuraci贸n de cach茅 +#[derive(Debug, Clone)] +pub struct CacheConfig { + /// TTL para entradas de archivos en cach茅 (ms) + pub file_ttl_ms: u64, + /// TTL para entradas de directorios en cach茅 (ms) + pub directory_ttl_ms: u64, + /// M谩ximo n煤mero de entradas en cach茅 + pub max_entries: usize, +} + +impl Default for CacheConfig { + fn default() -> Self { + Self { + file_ttl_ms: 60_000, // 1 minuto + directory_ttl_ms: 120_000, // 2 minutos + max_entries: 10_000, // 10,000 entradas + } + } +} + /// Configuraci贸n de timeouts para diferentes operaciones #[derive(Debug, Clone)] pub struct TimeoutConfig { @@ -160,6 +181,8 @@ impl Default for ConcurrencyConfig { /// Configuraci贸n global de la aplicaci贸n #[derive(Debug, Clone)] pub struct AppConfig { + /// Configuraci贸n de cach茅 + pub cache: CacheConfig, /// Configuraci贸n de timeouts pub timeouts: TimeoutConfig, /// Configuraci贸n de recursos @@ -171,6 +194,7 @@ pub struct AppConfig { impl Default for AppConfig { fn default() -> Self { Self { + cache: CacheConfig::default(), timeouts: TimeoutConfig::default(), resources: ResourceConfig::default(), concurrency: ConcurrencyConfig::default(), diff --git a/src/common/di.rs b/src/common/di.rs index 4a23e87c..649ddf4f 100644 --- a/src/common/di.rs +++ b/src/common/di.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; use std::sync::Arc; -use async_trait::async_trait; +use std::sync::RwLock; use crate::domain::services::path_service::PathService; use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository; @@ -8,20 +8,28 @@ use crate::infrastructure::repositories::file_fs_repository::FileFsRepository; use crate::infrastructure::services::file_system_i18n_service::FileSystemI18nService; use crate::infrastructure::services::id_mapping_service::IdMappingService; use crate::infrastructure::services::cache_manager::StorageCacheManager; +use crate::infrastructure::services::file_metadata_cache::FileMetadataCache; use crate::application::services::folder_service::FolderService; use crate::application::services::file_service::FileService; use crate::application::services::i18n_application_service::I18nApplicationService; use crate::application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator}; use crate::application::ports::inbound::{FileUseCase, FolderUseCase, UseCaseFactory}; use crate::application::ports::outbound::{FileStoragePort, FolderStoragePort}; +use crate::application::ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory}; +use crate::application::ports::storage_ports::{FileReadPort, FileWritePort, FilePathResolutionPort}; +use crate::infrastructure::repositories::{FileMetadataManager, FilePathResolver, FileFsReadRepository, FileFsWriteRepository}; +use crate::application::services::{FileUploadService, FileRetrievalService, FileManagementService, AppFileUseCaseFactory}; use crate::common::errors::DomainError; use crate::domain::services::i18n_service::I18nService; +use crate::common::config::AppConfig; +use crate::domain::repositories::folder_repository::FolderRepository; /// F谩brica para los diferentes componentes de la aplicaci贸n #[allow(dead_code)] pub struct AppServiceFactory { storage_path: PathBuf, locales_path: PathBuf, + config: AppConfig, } impl AppServiceFactory { @@ -31,6 +39,17 @@ impl AppServiceFactory { Self { storage_path, locales_path, + config: AppConfig::default(), + } + } + + /// Crea una nueva f谩brica de servicios con configuraci贸n personalizada + #[allow(dead_code)] + pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self { + Self { + storage_path, + locales_path, + config, } } @@ -63,18 +82,18 @@ impl AppServiceFactory { path_service, cache_manager, id_mapping_service, + config: self.config.clone(), }) } - /// Inicializa los servicios de repositorio + /// Inicializa los servicios de repositorio utilizando el patr贸n Builder mejorado #[allow(dead_code)] pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices { - // Storage mediator - create first because it's needed by folder repository - // (temporarily using a placeholder for folder repository, will update later) - let placeholder_folder_repo = Arc::new(FolderFsRepository::new_stub()); + // Storage mediator - con inicializaci贸n diferida para folder repository + let folder_repository_holder = Arc::new(RwLock::new(None)); - let storage_mediator = Arc::new(FileSystemStorageMediator::new( - placeholder_folder_repo.clone(), + let storage_mediator = Arc::new(FileSystemStorageMediator::new_with_lazy_folder( + folder_repository_holder.clone(), core.path_service.clone(), core.id_mapping_service.clone() )); @@ -87,14 +106,47 @@ impl AppServiceFactory { core.path_service.clone(), )); - // Create a file metadata cache with default configuration + // Actualizar el holder para el mediador una vez que el repository est谩 creado + if let Ok(mut holder) = folder_repository_holder.write() { + *holder = Some(folder_repository.clone()); + } + + // Metadata cache let metadata_cache = Arc::new( - crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default_with_config( - crate::common::config::AppConfig::default() - ) + FileMetadataCache::default_with_config(core.config.clone()) ); - // File repository + // Componentes refactorizados + let metadata_manager = Arc::new(FileMetadataManager::new( + metadata_cache.clone(), + core.config.clone() + )); + + let path_resolver = Arc::new(FilePathResolver::new( + core.path_service.clone(), + storage_mediator.clone(), + core.id_mapping_service.clone() + )); + + // File repositories separados para lectura y escritura + let file_read_repository = Arc::new(FileFsReadRepository::new( + self.storage_path.clone(), + metadata_manager.clone(), + path_resolver.clone(), + core.config.clone(), + None // processor will be added later if needed + )); + + let file_write_repository = Arc::new(FileFsWriteRepository::new( + self.storage_path.clone(), + metadata_manager.clone(), + path_resolver.clone(), + storage_mediator.clone(), + core.config.clone(), + None // processor will be added later if needed + )); + + // Legacy file repository - mantenido por compatibilidad let file_repository = Arc::new(FileFsRepository::new( self.storage_path.clone(), storage_mediator.clone(), @@ -111,8 +163,12 @@ impl AppServiceFactory { RepositoryServices { folder_repository, file_repository, + file_read_repository, + file_write_repository, i18n_repository, storage_mediator, + metadata_manager, + path_resolver, } } @@ -124,10 +180,29 @@ impl AppServiceFactory { repos.folder_repository.clone() )); + // Antiguo servicio 煤nico let file_service = Arc::new(FileService::new( repos.file_repository.clone() )); + // Nuevos servicios refactorizados + let file_upload_service = Arc::new(FileUploadService::new( + repos.file_write_repository.clone() + )); + + let file_retrieval_service = Arc::new(FileRetrievalService::new( + repos.file_read_repository.clone() + )); + + let file_management_service = Arc::new(FileManagementService::new( + repos.file_write_repository.clone() + )); + + let file_use_case_factory = Arc::new(AppFileUseCaseFactory::new( + repos.file_read_repository.clone(), + repos.file_write_repository.clone() + )); + let i18n_service = Arc::new(I18nApplicationService::new( repos.i18n_repository.clone() )); @@ -135,6 +210,10 @@ impl AppServiceFactory { ApplicationServices { folder_service, file_service, + file_upload_service, + file_retrieval_service, + file_management_service, + file_use_case_factory, i18n_service, } } @@ -146,6 +225,7 @@ pub struct CoreServices { pub path_service: Arc, pub cache_manager: Arc, pub id_mapping_service: Arc, + pub config: AppConfig, } /// Contenedor para servicios de repositorio @@ -153,8 +233,12 @@ pub struct CoreServices { pub struct RepositoryServices { pub folder_repository: Arc, pub file_repository: Arc, + pub file_read_repository: Arc, + pub file_write_repository: Arc, pub i18n_repository: Arc, pub storage_mediator: Arc, + pub metadata_manager: Arc, + pub path_resolver: Arc, } /// Contenedor para servicios de aplicaci贸n @@ -162,29 +246,9 @@ pub struct RepositoryServices { pub struct ApplicationServices { pub folder_service: Arc, pub file_service: Arc, + pub file_upload_service: Arc, + pub file_retrieval_service: Arc, + pub file_management_service: Arc, + pub file_use_case_factory: Arc, pub i18n_service: Arc, -} - -/// F谩brica de casos de uso para la inyecci贸n de dependencias -#[allow(dead_code)] -pub struct AppUseCaseFactory { - services: ApplicationServices, -} - -impl AppUseCaseFactory { - #[allow(dead_code)] - pub fn new(services: ApplicationServices) -> Self { - Self { services } - } -} - -#[async_trait] -impl UseCaseFactory for AppUseCaseFactory { - fn create_file_use_case(&self) -> Arc { - self.services.file_service.clone() - } - - fn create_folder_use_case(&self) -> Arc { - self.services.folder_service.clone() - } } \ No newline at end of file diff --git a/src/common/errors.rs b/src/common/errors.rs index 12530c28..4de60056 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -17,6 +17,8 @@ pub enum ErrorKind { Timeout, /// Error interno del sistema InternalError, + /// Funcionalidad no implementada + NotImplemented, } impl Display for ErrorKind { @@ -28,6 +30,7 @@ impl Display for ErrorKind { ErrorKind::AccessDenied => write!(f, "Access Denied"), ErrorKind::Timeout => write!(f, "Timeout"), ErrorKind::InternalError => write!(f, "Internal Error"), + ErrorKind::NotImplemented => write!(f, "Not Implemented"), } } } @@ -132,6 +135,17 @@ impl DomainError { source: None, } } + + /// Crea un error de funcionalidad no implementada + pub fn not_implemented>(entity_type: &'static str, message: S) -> Self { + Self { + kind: ErrorKind::NotImplemented, + entity_type, + entity_id: None, + message: message.into(), + source: None, + } + } /// Establece el ID de la entidad #[allow(dead_code)] diff --git a/src/infrastructure/repositories/file_fs_read_repository.rs b/src/infrastructure/repositories/file_fs_read_repository.rs new file mode 100644 index 00000000..86dabe06 --- /dev/null +++ b/src/infrastructure/repositories/file_fs_read_repository.rs @@ -0,0 +1,184 @@ +use std::path::PathBuf; +use std::sync::Arc; +use async_trait::async_trait; +use bytes::Bytes; +use futures::Stream; + +use crate::domain::entities::file::File; +use crate::application::ports::storage_ports::FileReadPort; +use crate::common::errors::DomainError; +use crate::domain::repositories::file_repository::FileRepositoryResult; +use crate::infrastructure::repositories::file_metadata_manager::{FileMetadataManager, MetadataError}; +use crate::infrastructure::repositories::file_path_resolver::FilePathResolver; +use crate::domain::services::path_service::StoragePath; +use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor; +use crate::common::config::AppConfig; + +/// Implementaci贸n de repositorio para operaciones de lectura de archivos +pub struct FileFsReadRepository { + root_path: PathBuf, + metadata_manager: Arc, + path_resolver: Arc, + config: AppConfig, + parallel_processor: Option>, +} + +impl FileFsReadRepository { + /// Crea un nuevo repositorio de lectura de archivos + pub fn new( + root_path: PathBuf, + metadata_manager: Arc, + path_resolver: Arc, + config: AppConfig, + parallel_processor: Option>, + ) -> Self { + Self { + root_path, + metadata_manager, + path_resolver, + config, + parallel_processor, + } + } + + /// Crea una entidad de archivo a partir de metadatos + async fn create_file_entity( + &self, + id: String, + name: String, + storage_path: StoragePath, + size: u64, + mime_type: String, + folder_id: Option, + created_at: Option, + modified_at: Option, + ) -> FileRepositoryResult { + // If timestamps are provided, use them; otherwise, let File::new create default timestamps + if let (Some(created), Some(modified)) = (created_at, modified_at) { + File::with_timestamps( + id, + name, + storage_path, + size, + mime_type, + folder_id, + created, + modified, + ) + .map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string())) + } else { + File::new( + id, + name, + storage_path, + size, + mime_type, + folder_id, + ) + .map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string())) + } + } + + /// Obtiene un archivo por su ID + async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult { + // Obtener la ruta del archivo usando el resolver de rutas + let storage_path = self.path_resolver.get_path_by_id(id).await?; + + // Verificar que el archivo existe f铆sicamente + let abs_path = self.path_resolver.resolve_storage_path(&storage_path); + if !self.metadata_manager.file_exists(&abs_path).await + .map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))? { + return Err(crate::domain::repositories::file_repository::FileRepositoryError::NotFound( + format!("File {} not found at {}", id, storage_path.to_string()) + )); + } + + // Obtener metadatos del archivo + let (size, created_at, modified_at) = self.metadata_manager.get_file_metadata(&abs_path).await + .map_err(|e| match e { + MetadataError::IoError(io_err) => crate::domain::repositories::file_repository::FileRepositoryError::IoError(io_err), + MetadataError::Timeout(msg) => crate::domain::repositories::file_repository::FileRepositoryError::Timeout(msg), + MetadataError::Unavailable(msg) => crate::domain::repositories::file_repository::FileRepositoryError::NotFound(msg), + })?; + + // Obtener nombre del archivo de la ruta + let name = match storage_path.file_name() { + Some(name) => name, + None => { + return Err(crate::domain::repositories::file_repository::FileRepositoryError::InvalidPath( + storage_path.to_string() + )); + } + }; + + // Determinar ID de carpeta padre + let parent = storage_path.parent(); + let folder_id: Option = if parent.is_none() || parent.as_ref().unwrap().is_empty() { + None // Root folder + } else { + None // En implementaci贸n real, buscar ID de la carpeta padre + }; + + // Determinar tipo MIME + let mime_type = mime_guess::from_path(&abs_path) + .first_or_octet_stream() + .to_string(); + + // Crear entidad de archivo + let file = self.create_file_entity( + id.to_string(), + name, + storage_path, + size, + mime_type, + folder_id, + Some(created_at), + Some(modified_at), + ).await?; + + Ok(file) + } +} + +#[async_trait] +impl FileReadPort for FileFsReadRepository { + async fn get_file(&self, id: &str) -> Result { + self.get_file_by_id(id).await + .map_err(|e| match e { + crate::domain::repositories::file_repository::FileRepositoryError::NotFound(msg) => DomainError::not_found("File", msg), + crate::domain::repositories::file_repository::FileRepositoryError::IoError(io_err) => DomainError::internal_error("File", io_err.to_string()), + crate::domain::repositories::file_repository::FileRepositoryError::Timeout(msg) => DomainError::internal_error("File", msg), + _ => DomainError::internal_error("File", e.to_string()), + }) + } + + async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError> { + // Implementaci贸n real debe obtener la lista de archivos en una carpeta + // Por ahora, devolvemos lista vac铆a + Ok(Vec::new()) + } + + async fn get_file_content(&self, id: &str) -> Result, DomainError> { + // Primero obtenemos el archivo para verificar existencia + let file = self.get_file_by_id(id).await + .map_err(|e| match e { + crate::domain::repositories::file_repository::FileRepositoryError::NotFound(msg) => DomainError::not_found("File", msg), + crate::domain::repositories::file_repository::FileRepositoryError::IoError(io_err) => DomainError::internal_error("File", io_err.to_string()), + crate::domain::repositories::file_repository::FileRepositoryError::Timeout(msg) => DomainError::internal_error("File", msg), + _ => DomainError::internal_error("File", e.to_string()), + })?; + + // Ruta absoluta del archivo + let abs_path = self.path_resolver.resolve_storage_path(file.storage_path()); + + // Implementaci贸n real debe leer el contenido del archivo + // Por ahora, devolvemos un vector vac铆o + Ok(Vec::new()) + } + + async fn get_file_stream(&self, id: &str) -> Result> + Send>, DomainError> { + // Implementaci贸n real debe devolver un stream de bytes del archivo + // Por ahora, lanzamos un error + Err(DomainError::internal_error("File stream", "Stream functionality not yet implemented")) + } +} \ No newline at end of file diff --git a/src/infrastructure/repositories/file_fs_write_repository.rs b/src/infrastructure/repositories/file_fs_write_repository.rs new file mode 100644 index 00000000..0624417d --- /dev/null +++ b/src/infrastructure/repositories/file_fs_write_repository.rs @@ -0,0 +1,132 @@ +use std::path::PathBuf; +use std::sync::Arc; +use async_trait::async_trait; + +use crate::domain::entities::file::File; +use crate::application::ports::storage_ports::FileWritePort; +use crate::common::errors::DomainError; +use crate::domain::repositories::file_repository::FileRepositoryResult; +use crate::infrastructure::repositories::file_metadata_manager::{FileMetadataManager, MetadataError}; +use crate::infrastructure::repositories::file_path_resolver::FilePathResolver; +use crate::domain::services::path_service::StoragePath; +use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor; +use crate::common::config::AppConfig; +use crate::application::services::storage_mediator::StorageMediator; + +/// Implementaci贸n de repositorio para operaciones de escritura de archivos +pub struct FileFsWriteRepository { + root_path: PathBuf, + metadata_manager: Arc, + path_resolver: Arc, + storage_mediator: Arc, + config: AppConfig, + parallel_processor: Option>, +} + +impl FileFsWriteRepository { + /// Crea un nuevo repositorio de escritura de archivos + pub fn new( + root_path: PathBuf, + metadata_manager: Arc, + path_resolver: Arc, + storage_mediator: Arc, + config: AppConfig, + parallel_processor: Option>, + ) -> Self { + Self { + root_path, + metadata_manager, + path_resolver, + storage_mediator, + config, + parallel_processor, + } + } + + /// Crea directorios padres si es necesario + async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> { + if let Some(parent) = abs_path.parent() { + tokio::time::timeout( + self.config.timeouts.dir_timeout(), + tokio::fs::create_dir_all(parent) + ).await + .map_err(|_| crate::domain::repositories::file_repository::FileRepositoryError::Timeout( + format!("Timeout creating parent directory: {}", parent.display()) + ))? + .map_err(crate::domain::repositories::file_repository::FileRepositoryError::IoError)?; + } + Ok(()) + } + + /// Crea una entidad de archivo a partir de metadatos + async fn create_file_entity( + &self, + id: String, + name: String, + storage_path: StoragePath, + size: u64, + mime_type: String, + folder_id: Option, + created_at: Option, + modified_at: Option, + ) -> FileRepositoryResult { + // If timestamps are provided, use them; otherwise, let File::new create default timestamps + if let (Some(created), Some(modified)) = (created_at, modified_at) { + File::with_timestamps( + id, + name, + storage_path, + size, + mime_type, + folder_id, + created, + modified, + ) + .map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string())) + } else { + File::new( + id, + name, + storage_path, + size, + mime_type, + folder_id, + ) + .map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string())) + } + } + + /// Elimina un archivo de forma no bloqueante + async fn delete_file_non_blocking(&self, _abs_path: PathBuf) -> FileRepositoryResult<()> { + // Implementaci贸n real debe eliminar el archivo + // Por ahora, devolvemos OK + Ok(()) + } +} + +#[async_trait] +impl FileWritePort for FileFsWriteRepository { + async fn save_file( + &self, + name: String, + folder_id: Option, + content_type: String, + content: Vec, + ) -> Result { + // Implementaci贸n real debe guardar el archivo en disco + // Por ahora, devolvemos un error + Err(DomainError::internal_error("File save", "Save functionality not yet implemented")) + } + + async fn move_file(&self, file_id: &str, target_folder_id: Option) -> Result { + // Implementaci贸n real debe mover el archivo a otra carpeta + // Por ahora, devolvemos un error + Err(DomainError::internal_error("File move", "Move functionality not yet implemented")) + } + + async fn delete_file(&self, id: &str) -> Result<(), DomainError> { + // Implementaci贸n real debe eliminar el archivo + // Por ahora, devolvemos un error + Err(DomainError::internal_error("File delete", "Delete functionality not yet implemented")) + } +} \ No newline at end of file diff --git a/src/infrastructure/repositories/file_metadata_manager.rs b/src/infrastructure/repositories/file_metadata_manager.rs new file mode 100644 index 00000000..dd4f387a --- /dev/null +++ b/src/infrastructure/repositories/file_metadata_manager.rs @@ -0,0 +1,158 @@ +use std::path::PathBuf; +use std::sync::Arc; +use tokio::time; +use tokio::fs; +use std::time::Duration; + +use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType, FileMetadata}; +use crate::common::config::AppConfig; +use crate::common::errors::DomainError; + +/// Gestor de metadatos de archivos que encapsula la l贸gica de cach茅 +pub struct FileMetadataManager { + metadata_cache: Arc, + config: AppConfig, +} + +#[derive(Debug, thiserror::Error)] +pub enum MetadataError { + #[error("Error de E/S al acceder a los metadatos: {0}")] + IoError(#[from] std::io::Error), + + #[error("Timeout al acceder a los metadatos: {0}")] + Timeout(String), + + #[error("Metadatos no disponibles: {0}")] + Unavailable(String), +} + +impl From for DomainError { + fn from(err: MetadataError) -> Self { + match err { + MetadataError::IoError(e) => DomainError::internal_error("FileMetadata", e.to_string()), + MetadataError::Timeout(msg) => DomainError::internal_error("FileMetadata", msg), + MetadataError::Unavailable(msg) => DomainError::not_found("FileMetadata", msg), + } + } +} + +impl FileMetadataManager { + /// Crea un nuevo gestor de metadatos + pub fn new(metadata_cache: Arc, config: AppConfig) -> Self { + Self { + metadata_cache, + config, + } + } + + /// Comprueba si un archivo existe en la ruta especificada con cach茅 + pub async fn file_exists(&self, abs_path: &PathBuf) -> Result { + // Intentar obtener del cach茅 avanzado primero + if let Some(is_file) = self.metadata_cache.is_file(&abs_path).await { + tracing::debug!("Metadata cache hit for existence check: {} - path: {}", is_file, abs_path.display()); + return Ok(is_file); + } + + // Si no est谩 en cach茅, verificar directamente y actualizar cach茅 + tracing::debug!("Metadata cache miss for existence check: {}", abs_path.display()); + + // Utilizar timeout para evitar bloqueo + match time::timeout( + self.config.timeouts.file_timeout(), + fs::metadata(&abs_path) + ).await { + Ok(Ok(metadata)) => { + let is_file = metadata.is_file(); + + // Actualizar la cach茅 con informaci贸n fresca + if let Err(e) = self.metadata_cache.refresh_metadata(&abs_path).await { + tracing::warn!("Failed to update cache for {}: {}", abs_path.display(), e); + } + + if is_file { + tracing::debug!("File exists and is accessible: {}", abs_path.display()); + Ok(true) + } else { + tracing::warn!("Path exists but is not a file: {}", abs_path.display()); + Ok(false) + } + }, + Ok(Err(e)) => { + tracing::warn!("File check failed: {} - {}", abs_path.display(), e); + + // A帽adir a cach茅 como no existente + let entry_type = CacheEntryType::Unknown; + let file_metadata = FileMetadata::new( + abs_path.clone(), + false, + entry_type, + None, + None, + None, + None, + Duration::from_millis(self.config.timeouts.file_operation_ms), + ); + self.metadata_cache.update_cache(file_metadata).await; + + Ok(false) + }, + Err(_) => { + tracing::warn!("Timeout checking file metadata: {}", abs_path.display()); + Err(MetadataError::Timeout(format!("Timeout checking file: {}", abs_path.display()))) + } + } + } + + /// Obtiene metadatos de archivo (tama帽o, fechas creaci贸n/modificaci贸n) con cach茅 + pub async fn get_file_metadata(&self, abs_path: &PathBuf) -> Result<(u64, u64, u64), MetadataError> { + // Intentar obtener de cach茅 primero + if let Some(cached_metadata) = self.metadata_cache.get_metadata(abs_path).await { + if let (Some(size), Some(created_at), Some(modified_at)) = + (cached_metadata.size, cached_metadata.created_at, cached_metadata.modified_at) { + tracing::debug!("Using cached metadata for: {}", abs_path.display()); + return Ok((size, created_at, modified_at)); + } + } + + // Si no est谩 en cach茅 o metadatos incompletos, cargar desde sistema de archivos + let metadata = match time::timeout( + self.config.timeouts.file_timeout(), + fs::metadata(&abs_path) + ).await { + Ok(Ok(metadata)) => metadata, + Ok(Err(e)) => return Err(MetadataError::IoError(e)), + Err(_) => return Err(MetadataError::Timeout( + format!("Timeout getting metadata for: {}", abs_path.display()) + )), + }; + + let size = metadata.len(); + + // Get creation timestamp + let created_at = metadata.created() + .map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()) + .unwrap_or_else(|_| 0); + + // Get modification timestamp + let modified_at = metadata.modified() + .map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()) + .unwrap_or_else(|_| 0); + + // Actualizar cach茅 si es posible + if let Err(e) = self.metadata_cache.refresh_metadata(abs_path).await { + tracing::warn!("Failed to update metadata cache for {}: {}", abs_path.display(), e); + } + + Ok((size, created_at, modified_at)) + } + + /// Invalida la entrada de cach茅 para un archivo + pub async fn invalidate(&self, abs_path: &PathBuf) { + self.metadata_cache.invalidate(abs_path).await; + } + + /// Invalida la entrada de cach茅 para un directorio y su contenido + pub async fn invalidate_directory(&self, dir_path: &PathBuf) { + self.metadata_cache.invalidate_directory(dir_path).await; + } +} \ No newline at end of file diff --git a/src/infrastructure/repositories/file_path_resolver.rs b/src/infrastructure/repositories/file_path_resolver.rs new file mode 100644 index 00000000..a09361de --- /dev/null +++ b/src/infrastructure/repositories/file_path_resolver.rs @@ -0,0 +1,90 @@ +use std::path::PathBuf; +use std::sync::Arc; +use async_trait::async_trait; + +use crate::domain::services::path_service::{PathService, StoragePath}; +use crate::application::services::storage_mediator::StorageMediator; +use crate::infrastructure::services::id_mapping_service::{IdMappingService, IdMappingError}; +use crate::domain::repositories::file_repository::FileRepositoryError; +use crate::common::errors::DomainError; +use crate::application::ports::storage_ports::FilePathResolutionPort; + +/// Resuelve rutas de archivos y gestiona el mapeo de IDs a rutas +pub struct FilePathResolver { + path_service: Arc, + storage_mediator: Arc, + id_mapping_service: Arc, +} + +impl FilePathResolver { + /// Crea un nuevo resolver de rutas + pub fn new( + path_service: Arc, + storage_mediator: Arc, + id_mapping_service: Arc, + ) -> Self { + Self { + path_service, + storage_mediator, + id_mapping_service, + } + } + + /// Resuelve una ruta de dominio a una ruta f铆sica absoluta + pub fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf { + self.path_service.resolve_path(storage_path) + } + + /// Resuelve una ruta PathBuf a una ruta f铆sica absoluta (legacy) + pub fn resolve_legacy_path(&self, relative_path: &std::path::Path) -> PathBuf { + self.storage_mediator.resolve_path(relative_path) + } + + /// Obtiene la ruta de un archivo por su ID + pub async fn get_path_by_id(&self, id: &str) -> Result { + self.id_mapping_service.get_path_by_id(id).await + .map_err(FileRepositoryError::from) + } + + /// Actualiza la ruta para un ID existente + pub async fn update_path(&self, id: &str, storage_path: &StoragePath) -> Result<(), FileRepositoryError> { + self.id_mapping_service.update_path(id, storage_path).await + .map_err(FileRepositoryError::from) + } + + /// Obtiene o crea un ID para una ruta + pub async fn get_or_create_id(&self, storage_path: &StoragePath) -> Result { + self.id_mapping_service.get_or_create_id(storage_path).await + .map_err(FileRepositoryError::from) + } + + /// Elimina un ID del mapeo + pub async fn remove_id(&self, id: &str) -> Result<(), FileRepositoryError> { + self.id_mapping_service.remove_id(id).await + .map_err(FileRepositoryError::from) + } + + /// Guarda cambios pendientes + pub async fn save_changes(&self) -> Result<(), FileRepositoryError> { + self.id_mapping_service.save_pending_changes().await + .map_err(FileRepositoryError::from) + } +} + +// Implementaci贸n de FilePathResolutionPort +#[async_trait] +impl FilePathResolutionPort for FilePathResolver { + async fn get_file_path(&self, id: &str) -> Result { + self.get_path_by_id(id).await + .map_err(|e| match e { + FileRepositoryError::NotFound(id) => DomainError::not_found("File", id), + FileRepositoryError::IoError(e) => DomainError::internal_error("FilePath", e.to_string()), + FileRepositoryError::Timeout(msg) => DomainError::internal_error("FilePath", msg), + _ => DomainError::internal_error("FilePath", e.to_string()), + }) + } + + fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf { + self.resolve_storage_path(storage_path) + } +} \ No newline at end of file diff --git a/src/infrastructure/repositories/mod.rs b/src/infrastructure/repositories/mod.rs index fd7caf18..796f9b8a 100644 --- a/src/infrastructure/repositories/mod.rs +++ b/src/infrastructure/repositories/mod.rs @@ -2,3 +2,14 @@ pub mod file_fs_repository; pub mod folder_fs_repository; pub mod parallel_file_processor; +// Nuevos repositorios refactorizados +pub mod file_metadata_manager; +pub mod file_path_resolver; +pub mod file_fs_read_repository; +pub mod file_fs_write_repository; + +// Re-exportar para facilitar acceso +pub use file_metadata_manager::FileMetadataManager; +pub use file_path_resolver::FilePathResolver; +pub use file_fs_read_repository::FileFsReadRepository; +pub use file_fs_write_repository::FileFsWriteRepository; \ No newline at end of file