Merge pull request #2 from DioCrafts/feature/improvements

adding features
This commit is contained in:
Dionisio Pozo
2025-03-19 19:54:22 +01:00
committed by GitHub
19 changed files with 1150 additions and 42 deletions
+54 -1
View File
@@ -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
+5
View File
@@ -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};
+53
View File
@@ -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<String>,
content_type: String,
content: Vec<u8>,
) -> Result<FileDto, DomainError>;
}
/// 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<FileDto, DomainError>;
/// Lista archivos en una carpeta
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, 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>;
}
/// 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<String>) -> Result<FileDto, DomainError>;
/// 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<dyn FileUploadUseCase>;
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>;
fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase>;
}
+3 -1
View File
@@ -1,2 +1,4 @@
pub mod inbound;
pub mod outbound;
pub mod outbound;
pub mod file_ports;
pub mod storage_ports;
+70
View File
@@ -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<File, DomainError>;
/// Lista archivos en una carpeta
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, 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>;
}
/// 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<String>,
content_type: String,
content: Vec<u8>,
) -> Result<File, DomainError>;
/// Mueve un archivo a otra carpeta
async fn move_file(&self, file_id: &str, target_folder_id: Option<String>) -> Result<File, DomainError>;
/// 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<StoragePath, DomainError>;
/// 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<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 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>;
}
@@ -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<dyn FileWritePort>,
}
impl FileManagementService {
/// Crea un nuevo servicio de gestión de archivos
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
Self { file_repository }
}
}
#[async_trait]
impl FileManagementUseCase for FileManagementService {
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError> {
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
}
}
@@ -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<dyn FileReadPort>,
}
impl FileRetrievalService {
/// Crea un nuevo servicio de recuperación de archivos
pub fn new(file_repository: Arc<dyn FileReadPort>) -> Self {
Self { file_repository }
}
}
#[async_trait]
impl FileRetrievalUseCase for FileRetrievalService {
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError> {
let file = self.file_repository.get_file(id).await?;
Ok(FileDto::from(file))
}
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, 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<Vec<u8>, DomainError> {
self.file_repository.get_file_content(id).await
}
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
self.file_repository.get_file_stream(id).await
}
}
@@ -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<dyn FileWritePort>,
}
impl FileUploadService {
/// Crea un nuevo servicio de subida de archivos
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
Self { file_repository }
}
}
#[async_trait]
impl FileUploadUseCase for FileUploadService {
async fn upload_file(
&self,
name: String,
folder_id: Option<String>,
content_type: String,
content: Vec<u8>,
) -> Result<FileDto, DomainError> {
let file = self.file_repository.save_file(name, folder_id, content_type, content).await?;
Ok(FileDto::from(file))
}
}
@@ -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<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort>,
}
impl AppFileUseCaseFactory {
/// Crea una nueva factory para casos de uso de archivos
pub fn new(
file_read_repository: Arc<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort>
) -> Self {
Self {
file_read_repository,
file_write_repository,
}
}
}
impl FileUseCaseFactory for AppFileUseCaseFactory {
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase> {
Arc::new(FileUploadService::new(self.file_write_repository.clone()))
}
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase> {
Arc::new(FileRetrievalService::new(self.file_read_repository.clone()))
}
fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase> {
Arc::new(FileManagementService::new(self.file_write_repository.clone()))
}
}
+12 -1
View File
@@ -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;
+84 -3
View File
@@ -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<dyn FolderRepository>,
path_service: Arc<PathService>,
id_mapping: Arc<dyn IdMappingPort>,
pub folder_repository: Arc<dyn FolderRepository>,
pub path_service: Arc<PathService>,
pub id_mapping: Arc<dyn IdMappingPort>,
}
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<RwLock<Option<Arc<dyn FolderRepository>>>>,
path_service: Arc<PathService>,
id_mapping: Arc<dyn IdMappingPort>
) -> 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<String>) -> Result<Folder, FolderRepositoryError> {
Err(FolderRepositoryError::Other("Stub repository".to_string()))
}
async fn get_folder_by_id(&self, _id: &str) -> Result<Folder, FolderRepositoryError> {
Err(FolderRepositoryError::Other("Stub repository".to_string()))
}
async fn get_folder_by_storage_path(&self, _storage_path: &StoragePath) -> Result<Folder, FolderRepositoryError> {
Err(FolderRepositoryError::Other("Stub repository".to_string()))
}
async fn list_folders(&self, _parent_id: Option<&str>) -> Result<Vec<Folder>, 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<Folder>, Option<usize>), FolderRepositoryError> {
Err(FolderRepositoryError::Other("Stub repository".to_string()))
}
async fn rename_folder(&self, _id: &str, _new_name: String) -> Result<Folder, FolderRepositoryError> {
Err(FolderRepositoryError::Other("Stub repository".to_string()))
}
async fn move_folder(&self, _id: &str, _new_parent_id: Option<&str>) -> Result<Folder, FolderRepositoryError> {
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<bool, FolderRepositoryError> {
Ok(false)
}
async fn get_folder_storage_path(&self, _id: &str) -> Result<StoragePath, FolderRepositoryError> {
Ok(StoragePath::root())
}
// Legacy methods
#[allow(deprecated)]
async fn folder_exists(&self, _path: &std::path::PathBuf) -> Result<bool, FolderRepositoryError> {
Ok(false)
}
#[allow(deprecated)]
async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> Result<Folder, FolderRepositoryError> {
Err(FolderRepositoryError::Other("Stub repository".to_string()))
}
}
/// Stub implementation for initialization dependency issues
+24
View File
@@ -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(),
+100 -36
View File
@@ -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<PathService>,
pub cache_manager: Arc<StorageCacheManager>,
pub id_mapping_service: Arc<IdMappingService>,
pub config: AppConfig,
}
/// Contenedor para servicios de repositorio
@@ -153,8 +233,12 @@ pub struct CoreServices {
pub struct RepositoryServices {
pub folder_repository: Arc<dyn FolderStoragePort>,
pub file_repository: Arc<dyn FileStoragePort>,
pub file_read_repository: Arc<dyn FileReadPort>,
pub file_write_repository: Arc<dyn FileWritePort>,
pub i18n_repository: Arc<dyn I18nService>,
pub storage_mediator: Arc<dyn StorageMediator>,
pub metadata_manager: Arc<FileMetadataManager>,
pub path_resolver: Arc<FilePathResolver>,
}
/// Contenedor para servicios de aplicación
@@ -162,29 +246,9 @@ pub struct RepositoryServices {
pub struct ApplicationServices {
pub folder_service: Arc<dyn FolderUseCase>,
pub file_service: Arc<dyn FileUseCase>,
pub file_upload_service: Arc<dyn FileUploadUseCase>,
pub file_retrieval_service: Arc<dyn FileRetrievalUseCase>,
pub file_management_service: Arc<dyn FileManagementUseCase>,
pub file_use_case_factory: Arc<dyn FileUseCaseFactory>,
pub i18n_service: Arc<I18nApplicationService>,
}
/// 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<dyn FileUseCase> {
self.services.file_service.clone()
}
fn create_folder_use_case(&self) -> Arc<dyn FolderUseCase> {
self.services.folder_service.clone()
}
}
+14
View File
@@ -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<S: Into<String>>(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)]
@@ -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<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
}
impl FileFsReadRepository {
/// Crea un nuevo repositorio de lectura de archivos
pub fn new(
root_path: PathBuf,
metadata_manager: Arc<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
) -> 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<String>,
created_at: Option<u64>,
modified_at: Option<u64>,
) -> FileRepositoryResult<File> {
// 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<File> {
// 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<String> = 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<File, DomainError> {
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<Vec<File>, 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<Vec<u8>, 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<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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"))
}
}
@@ -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<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
storage_mediator: Arc<dyn StorageMediator>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
}
impl FileFsWriteRepository {
/// Crea un nuevo repositorio de escritura de archivos
pub fn new(
root_path: PathBuf,
metadata_manager: Arc<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
storage_mediator: Arc<dyn StorageMediator>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
) -> 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<String>,
created_at: Option<u64>,
modified_at: Option<u64>,
) -> FileRepositoryResult<File> {
// 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<String>,
content_type: String,
content: Vec<u8>,
) -> Result<File, DomainError> {
// 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<String>) -> Result<File, DomainError> {
// 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"))
}
}
@@ -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<FileMetadataCache>,
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<MetadataError> 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<FileMetadataCache>, 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<bool, MetadataError> {
// 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;
}
}
@@ -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<PathService>,
storage_mediator: Arc<dyn StorageMediator>,
id_mapping_service: Arc<IdMappingService>,
}
impl FilePathResolver {
/// Crea un nuevo resolver de rutas
pub fn new(
path_service: Arc<PathService>,
storage_mediator: Arc<dyn StorageMediator>,
id_mapping_service: Arc<IdMappingService>,
) -> 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<StoragePath, FileRepositoryError> {
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<String, FileRepositoryError> {
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<StoragePath, DomainError> {
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)
}
}
+11
View File
@@ -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;