From d31a413e57a448c36124d60b59889a5979d0b8f7 Mon Sep 17 00:00:00 2001 From: Dionisio Date: Thu, 12 Feb 2026 09:41:25 +0100 Subject: [PATCH] chore: translate all Spanish comments and log messages to English --- src/application/dtos/file_dto.rs | 8 +- src/application/dtos/folder_dto.rs | 6 +- src/application/dtos/pagination.rs | 42 +-- src/application/dtos/recent_dto.rs | 12 +- src/application/dtos/user_dto.rs | 2 +- src/application/ports/auth_ports.rs | 36 +-- src/application/ports/favorites_ports.rs | 16 +- src/application/ports/file_ports.rs | 30 +-- src/application/ports/inbound.rs | 34 +-- src/application/ports/outbound.rs | 54 ++-- src/application/ports/recent_ports.rs | 26 +- src/application/ports/storage_ports.rs | 74 +++--- .../services/auth_application_service.rs | 170 ++++++------ src/application/services/batch_operations.rs | 236 ++++++++--------- .../services/file_retrieval_service.rs | 2 +- .../services/file_upload_service.rs | 4 +- .../services/file_use_case_factory.rs | 4 +- src/application/services/folder_service.rs | 86 +++--- src/application/services/recent_service.rs | 34 +-- src/application/services/search_service.rs | 186 ++++++------- src/application/services/share_service.rs | 76 +++--- src/application/services/storage_mediator.rs | 62 ++--- src/application/services/trash_service.rs | 18 +- .../transactions/storage_transaction.rs | 62 ++--- src/bin/migrate.rs | 28 +- src/common/config.rs | 154 +++++------ src/common/di.rs | 106 ++++---- src/common/errors.rs | 28 +- src/domain/entities/calendar.rs | 2 +- src/domain/entities/calendar_event.rs | 2 +- src/domain/entities/entity_errors.rs | 84 +++--- src/domain/entities/file.rs | 4 +- src/domain/entities/folder.rs | 2 +- src/domain/entities/share.rs | 2 +- src/domain/entities/user.rs | 24 +- src/domain/errors.rs | 68 ++--- src/domain/repositories/file_repository.rs | 84 +++--- src/domain/repositories/folder_repository.rs | 46 ++-- src/domain/repositories/session_repository.rs | 22 +- src/domain/repositories/user_repository.rs | 48 ++-- src/domain/services/path_service.rs | 38 +-- src/infrastructure/auth_factory.rs | 12 +- src/infrastructure/db.rs | 26 +- .../repositories/composite_file_repository.rs | 8 +- .../repositories/file_fs_read_repository.rs | 10 +- .../repositories/file_fs_write_repository.rs | 8 +- .../folder_fs_repository_trash.rs | 84 +++--- .../pg/calendar_event_pg_repository.rs | 18 +- .../repositories/pg/calendar_pg_repository.rs | 8 +- .../pg/favorites_pg_repository.rs | 2 +- .../pg/recent_items_pg_repository.rs | 2 +- .../repositories/pg/session_pg_repository.rs | 62 ++--- .../repositories/pg/user_pg_repository.rs | 76 +++--- .../repositories/share_fs_repository.rs | 38 +-- .../repositories/trash_fs_repository.rs | 34 +-- src/infrastructure/services/buffer_pool.rs | 218 ++++++++-------- .../services/compression_service.rs | 150 +++++------ .../services/file_metadata_cache.rs | 244 +++++++++--------- .../services/id_mapping_optimizer.rs | 104 ++++---- .../services/id_mapping_service.rs | 144 +++++------ src/infrastructure/services/jwt_service.rs | 6 +- src/infrastructure/services/path_service.rs | 36 +-- .../services/trash_cleanup_service.rs | 38 +-- src/infrastructure/services/zip_service.rs | 94 +++---- src/interfaces/api/handlers/auth_handler.rs | 58 ++--- src/interfaces/api/handlers/batch_handler.rs | 168 ++++++------ src/interfaces/api/handlers/recent_handler.rs | 48 ++-- src/interfaces/api/handlers/search_handler.rs | 102 ++++---- src/interfaces/api/handlers/trash_handler.rs | 60 ++--- src/interfaces/api/routes.rs | 12 +- src/interfaces/middleware/auth.rs | 62 ++--- src/interfaces/middleware/cache.rs | 164 ++++++------ src/lib.rs | 4 +- static/css/favorites.css | 14 +- static/css/recent.css | 16 +- static/css/style.css | 20 +- static/index.html | 56 ++-- static/js/app.js | 138 +++++----- static/js/auth.js | 40 +-- static/js/contextMenus.js | 52 ++-- static/js/favorites.js | 34 +-- static/js/fileOperations.js | 182 ++++++------- static/js/fileSharing.js | 10 +- static/js/recent.js | 22 +- static/js/search.js | 24 +- static/js/ui.js | 132 +++++----- static/login.html | 68 ++--- static/test.html | 20 +- storage/folder_ids.json | 12 +- 89 files changed, 2480 insertions(+), 2482 deletions(-) diff --git a/src/application/dtos/file_dto.rs b/src/application/dtos/file_dto.rs index 4d41657b..7f8b4d14 100644 --- a/src/application/dtos/file_dto.rs +++ b/src/application/dtos/file_dto.rs @@ -44,12 +44,12 @@ impl From for FileDto { } } -// Para convertir de FileDto a File para los batch handlers +// To convert from FileDto to File for batch handlers impl From for File { fn from(dto: FileDto) -> Self { - // Usar constructor para crear una entidad desde DTO - // Nota: esto debe simplificarse si File tiene un constructor adecuado - // Si no, deberías hacer la conversión de la mejor manera posible + // Use constructor to create an entity from DTO + // Note: this should be simplified if File has a proper constructor + // If not, make the conversion as best as possible File::from_dto( dto.id, dto.name, diff --git a/src/application/dtos/folder_dto.rs b/src/application/dtos/folder_dto.rs index 7a66996d..b6e6cf6b 100644 --- a/src/application/dtos/folder_dto.rs +++ b/src/application/dtos/folder_dto.rs @@ -66,11 +66,11 @@ impl From for FolderDto { } } -// Para convertir de FolderDto a Folder para los batch handlers +// To convert from FolderDto to Folder for batch handlers impl From for Folder { fn from(dto: FolderDto) -> Self { - // Usar constructor para crear una entidad desde DTO - // Nota: esto debe simplificarse si Folder tiene un constructor adecuado + // Use constructor to create an entity from DTO + // Note: this should be simplified if Folder has a proper constructor Folder::from_dto( dto.id, dto.name, diff --git a/src/application/dtos/pagination.rs b/src/application/dtos/pagination.rs index c9ce8d85..37bc27dd 100644 --- a/src/application/dtos/pagination.rs +++ b/src/application/dtos/pagination.rs @@ -1,39 +1,39 @@ use serde::{Serialize, Deserialize}; -/// Un DTO para representar información de paginación +/// A DTO to represent pagination information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginationDto { - /// Página actual (comienza en 0) + /// Current page (starts at 0) pub page: usize, - /// Tamaño de página + /// Page size pub page_size: usize, - /// Número total de elementos + /// Total number of items pub total_items: usize, - /// Número total de páginas + /// Total number of pages pub total_pages: usize, - /// Indica si hay una página siguiente + /// Indicates if there is a next page pub has_next: bool, - /// Indica si hay una página anterior + /// Indicates if there is a previous page pub has_prev: bool, } -/// Un DTO para representar una solicitud de paginación +/// A DTO to represent a pagination request #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginationRequestDto { - /// Página solicitada (comienza en 0) + /// Requested page (starts at 0) #[serde(default)] pub page: usize, - /// Tamaño de página solicitado + /// Requested page size #[serde(default = "default_page_size")] pub page_size: usize, } -/// Un DTO para representar una respuesta paginada +/// A DTO to represent a paginated response #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginatedResponseDto { - /// Datos en la página actual + /// Data on the current page pub items: Vec, - /// Información de paginación + /// Pagination information pub pagination: PaginationDto, } @@ -46,33 +46,33 @@ impl Default for PaginationRequestDto { } } -/// Función para establecer el tamaño de página por defecto +/// Function to set the default page size fn default_page_size() -> usize { - 100 // Por defecto, 100 items por página + 100 // By default, 100 items per page } impl PaginationRequestDto { - /// Calcula el offset para consultas paginadas + /// Calculates the offset for paginated queries pub fn offset(&self) -> usize { self.page * self.page_size } - /// Calcula el límite para consultas paginadas + /// Calculates the limit for paginated queries pub fn limit(&self) -> usize { self.page_size } - /// Valida y ajusta los parámetros de paginación + /// Validates and adjusts the pagination parameters pub fn validate_and_adjust(&self) -> Self { let mut page = self.page; let mut page_size = self.page_size; - // Asegurar que la página sea al menos 0 + // Ensure the page is at least 0 if page < 1 { page = 0; } - // Asegurar que el tamaño de página esté entre 10 y 500 + // Ensure the page size is between 10 and 500 if page_size < 10 { page_size = 10; } else if page_size > 500 { @@ -87,7 +87,7 @@ impl PaginationRequestDto { } impl PaginatedResponseDto { - /// Crea una nueva respuesta paginada a partir de los datos y la información de paginación + /// Creates a new paginated response from the data and pagination information pub fn new( items: Vec, page: usize, diff --git a/src/application/dtos/recent_dto.rs b/src/application/dtos/recent_dto.rs index 9e1774f4..75a7c4e9 100644 --- a/src/application/dtos/recent_dto.rs +++ b/src/application/dtos/recent_dto.rs @@ -1,21 +1,21 @@ use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; -/// DTO para elementos recientes +/// DTO for recent items #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecentItemDto { - /// Identificador único para el elemento reciente + /// Unique identifier for the recent item pub id: String, - /// ID del usuario propietario + /// Owner user ID pub user_id: String, - /// ID del elemento (archivo o carpeta) + /// Item ID (file or folder) pub item_id: String, - /// Tipo del elemento ('file' o 'folder') + /// Item type ('file' or 'folder') pub item_type: String, - /// Cuándo se accedió al elemento + /// When the item was accessed pub accessed_at: DateTime, } \ No newline at end of file diff --git a/src/application/dtos/user_dto.rs b/src/application/dtos/user_dto.rs index 12bf8a33..c6c4c726 100644 --- a/src/application/dtos/user_dto.rs +++ b/src/application/dtos/user_dto.rs @@ -67,7 +67,7 @@ pub struct RefreshTokenDto { pub refresh_token: String, } -/// Datos del usuario autenticado actual (para uso en servicios de application) +/// Authenticated current user data (for use in application services) #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CurrentUser { pub id: String, diff --git a/src/application/ports/auth_ports.rs b/src/application/ports/auth_ports.rs index 58c94bc8..50fd049b 100644 --- a/src/application/ports/auth_ports.rs +++ b/src/application/ports/auth_ports.rs @@ -65,49 +65,49 @@ pub trait TokenServicePort: Send + Sync + 'static { #[async_trait] pub trait UserStoragePort: Send + Sync + 'static { - /// Crea un nuevo usuario + /// Creates a new user async fn create_user(&self, user: User) -> Result; - /// Obtiene un usuario por ID + /// Gets a user by ID async fn get_user_by_id(&self, id: &str) -> Result; - /// Obtiene un usuario por nombre de usuario + /// Gets a user by username async fn get_user_by_username(&self, username: &str) -> Result; - /// Obtiene un usuario por correo electrónico + /// Gets a user by email async fn get_user_by_email(&self, email: &str) -> Result; - /// Actualiza un usuario existente + /// Updates an existing user async fn update_user(&self, user: User) -> Result; - /// Actualiza solo el uso de almacenamiento de un usuario + /// Updates only the storage usage of a user async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError>; - /// Lista usuarios con paginación + /// Lists users with pagination async fn list_users(&self, limit: i64, offset: i64) -> Result, DomainError>; - /// Lista usuarios por rol (por ejemplo, "admin" o "user") + /// Lists users by role (e.g., "admin" or "user") async fn list_users_by_role(&self, role: &str) -> Result, DomainError>; - /// Elimina un usuario por su ID + /// Deletes a user by their ID async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>; - /// Cambia la contraseña de un usuario + /// Changes a user's password async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>; /// Finds a user by OIDC provider + subject pair async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result; - /// Activa o desactiva un usuario + /// Activates or deactivates a user async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>; - /// Cambia el rol de un usuario + /// Changes a user's role async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>; - /// Actualiza la cuota de almacenamiento de un usuario + /// Updates a user's storage quota async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError>; - /// Cuenta el número total de usuarios + /// Counts the total number of users async fn count_users(&self) -> Result; } @@ -156,15 +156,15 @@ pub trait OidcServicePort: Send + Sync + 'static { #[async_trait] pub trait SessionStoragePort: Send + Sync + 'static { - /// Crea una nueva sesión + /// Creates a new session async fn create_session(&self, session: Session) -> Result; - /// Obtiene una sesión por token de actualización + /// Gets a session by refresh token async fn get_session_by_refresh_token(&self, refresh_token: &str) -> Result; - /// Revoca una sesión específica + /// Revokes a specific session async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>; - /// Revoca todas las sesiones de un usuario + /// Revokes all sessions of a user async fn revoke_all_user_sessions(&self, user_id: &str) -> Result; } \ No newline at end of file diff --git a/src/application/ports/favorites_ports.rs b/src/application/ports/favorites_ports.rs index 2a52d2f2..d68f846c 100644 --- a/src/application/ports/favorites_ports.rs +++ b/src/application/ports/favorites_ports.rs @@ -22,22 +22,22 @@ pub trait FavoritesUseCase: Send + Sync { // Outbound port — persistence abstraction // ───────────────────────────────────────────────────── -/// Puerto secundario (outbound) para persistencia de favoritos. +/// Secondary (outbound) port for favorites persistence. /// -/// Los servicios de aplicación dependen de este trait en lugar de -/// acceder directamente a `PgPool`. La implementación concreta -/// vive en `infrastructure::repositories::pg`. +/// Application services depend on this trait instead of +/// accessing `PgPool` directly. The concrete implementation +/// lives in `infrastructure::repositories::pg`. #[async_trait] pub trait FavoritesRepositoryPort: Send + Sync + 'static { - /// Obtiene todos los favoritos de un usuario. + /// Gets all favorites for a user. async fn get_favorites(&self, user_id: &str) -> Result>; - /// Añade un ítem a favoritos. Devuelve `Ok(())` si ya existía (idempotente). + /// Adds an item to favorites. Returns `Ok(())` if it already existed (idempotent). async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; - /// Elimina un ítem de favoritos. Devuelve `true` si existía. + /// Removes an item from favorites. Returns `true` if it existed. async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; - /// Comprueba si un ítem está en favoritos. + /// Checks if an item is in favorites. async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; } \ No newline at end of file diff --git a/src/application/ports/file_ports.rs b/src/application/ports/file_ports.rs index 08efe106..72fbb734 100644 --- a/src/application/ports/file_ports.rs +++ b/src/application/ports/file_ports.rs @@ -22,10 +22,10 @@ pub enum UploadStrategy { Streaming, } -/// Puerto primario para operaciones de subida de archivos +/// Primary port for file upload operations #[async_trait] pub trait FileUploadUseCase: Send + Sync + 'static { - /// Sube un nuevo archivo desde bytes + /// Uploads a new file from bytes async fn upload_file( &self, name: String, @@ -47,10 +47,10 @@ pub trait FileUploadUseCase: Send + Sync + 'static { total_size: usize, ) -> Result<(FileDto, UploadStrategy), DomainError>; - /// Crea un nuevo archivo en la ruta especificada (para WebDAV) + /// Creates a new file at the specified path (for WebDAV) async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result; - /// Actualiza el contenido de un archivo existente (para WebDAV) + /// Updates the content of an existing file (for WebDAV) async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>; } @@ -76,22 +76,22 @@ pub enum OptimizedFileContent { Stream(Pin> + Send>>), } -/// Puerto primario para operaciones de recuperación de archivos +/// Primary port for file retrieval operations #[async_trait] pub trait FileRetrievalUseCase: Send + Sync + 'static { - /// Obtiene un archivo por su ID + /// Gets a file by its ID async fn get_file(&self, id: &str) -> Result; - /// Obtiene un archivo por su ruta (para WebDAV) + /// Gets a file by its path (for WebDAV) async fn get_file_by_path(&self, path: &str) -> Result; - /// Lista archivos en una carpeta + /// Lists files in a folder async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError>; - /// Obtiene contenido de archivo como bytes (para archivos pequeños) + /// Gets file content as bytes (for small files) async fn get_file_content(&self, id: &str) -> Result, DomainError>; - /// Obtiene contenido de archivo como stream (para archivos grandes) + /// Gets file content as a stream (for large files) async fn get_file_stream(&self, id: &str) -> Result> + Send>, DomainError>; /// Optimized multi-tier download. @@ -119,16 +119,16 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { // Management port (delete, move) // ───────────────────────────────────────────────────── -/// Puerto primario para operaciones de gestión de archivos +/// Primary port for file management operations #[async_trait] pub trait FileManagementUseCase: Send + Sync + 'static { - /// Mueve un archivo a otra carpeta + /// Moves a file to another folder async fn move_file(&self, file_id: &str, folder_id: Option) -> Result; - /// Renombra un archivo + /// Renames a file async fn rename_file(&self, file_id: &str, new_name: &str) -> Result; - /// Elimina un archivo + /// Deletes a file async fn delete_file(&self, id: &str) -> Result<(), DomainError>; /// Smart delete: trash-first with dedup reference cleanup. @@ -145,7 +145,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static { ) -> Result; } -/// Factory para crear implementaciones de casos de uso de archivos +/// Factory for creating file use case implementations pub trait FileUseCaseFactory: Send + Sync + 'static { fn create_file_upload_use_case(&self) -> Arc; fn create_file_retrieval_use_case(&self) -> Arc; diff --git a/src/application/ports/inbound.rs b/src/application/ports/inbound.rs index 942778f0..d8441a89 100644 --- a/src/application/ports/inbound.rs +++ b/src/application/ports/inbound.rs @@ -4,58 +4,58 @@ use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolde use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto}; use crate::common::errors::DomainError; -/// Puerto primario para operaciones de carpetas +/// Primary port for folder operations #[async_trait] pub trait FolderUseCase: Send + Sync + 'static { - /// Crea una nueva carpeta + /// Creates a new folder async fn create_folder(&self, dto: CreateFolderDto) -> Result; - /// Obtiene una carpeta por su ID + /// Gets a folder by its ID async fn get_folder(&self, id: &str) -> Result; - /// Obtiene una carpeta por su ruta + /// Gets a folder by its path async fn get_folder_by_path(&self, path: &str) -> Result; - /// Lista carpetas dentro de una carpeta padre + /// Lists folders within a parent folder async fn list_folders(&self, parent_id: Option<&str>) -> Result, DomainError>; - /// Lista carpetas con paginación + /// Lists folders with pagination async fn list_folders_paginated( &self, parent_id: Option<&str>, pagination: &crate::application::dtos::pagination::PaginationRequestDto ) -> Result, DomainError>; - /// Renombra una carpeta + /// Renames a folder async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result; - /// Mueve una carpeta a otro padre + /// Moves a folder to another parent async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result; - /// Elimina una carpeta + /// Deletes a folder async fn delete_folder(&self, id: &str) -> Result<(), DomainError>; } /** - * Puerto primario para búsqueda de archivos y carpetas + * Primary port for file and folder search * - * Define las operaciones relacionadas con la búsqueda avanzada de - * archivos y carpetas basándose en diversos criterios. + * Defines the operations related to advanced search of + * files and folders based on various criteria. */ #[async_trait] pub trait SearchUseCase: Send + Sync + 'static { /** - * Realiza una búsqueda basada en los criterios especificados + * Performs a search based on the specified criteria * - * @param criteria Criterios de búsqueda que incluyen texto, fechas, tamaños, etc. - * @return Resultados de la búsqueda que contienen archivos y carpetas coincidentes + * @param criteria Search criteria including text, dates, sizes, etc. + * @return Search results containing matching files and folders */ async fn search(&self, criteria: SearchCriteriaDto) -> Result; /** - * Limpia la caché de resultados de búsqueda + * Clears the search results cache * - * @return Resultado indicando éxito o error + * @return Result indicating success or error */ async fn clear_search_cache(&self) -> Result<(), DomainError>; } \ No newline at end of file diff --git a/src/application/ports/outbound.rs b/src/application/ports/outbound.rs index 5090ebdf..fb23eea7 100644 --- a/src/application/ports/outbound.rs +++ b/src/application/ports/outbound.rs @@ -9,71 +9,71 @@ pub use crate::domain::repositories::folder_repository::FolderRepository; use super::storage_ports::{FileReadPort, FileWritePort}; -/// Puerto secundario para operaciones de almacenamiento +/// Secondary port for storage operations #[async_trait] pub trait StoragePort: Send + Sync + 'static { - /// Resuelve una ruta de dominio a una ruta física + /// Resolves a domain path to a physical path fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf; - /// Crea directorios si no existen + /// Creates directories if they don't exist async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>; - /// Verifica si existe un archivo en la ruta dada + /// Checks if a file exists at the given path async fn file_exists(&self, storage_path: &StoragePath) -> Result; - /// Verifica si existe un directorio en la ruta dada + /// Checks if a directory exists at the given path async fn directory_exists(&self, storage_path: &StoragePath) -> Result; } -/// Puerto unificado para persistencia de archivos (backward-compatible). +/// Unified port for file persistence (backward-compatible). /// -/// Ahora es un **supertrait** de `FileReadPort + FileWritePort`. -/// Cualquier tipo que implemente ambos ports obtiene `FileStoragePort` -/// automáticamente via blanket impl. Esto permite migrar consumidores -/// gradualmente a los ports granulares mientras los existentes siguen -/// funcionando sin cambios. +/// Now it is a **supertrait** of `FileReadPort + FileWritePort`. +/// Any type that implements both ports gets `FileStoragePort` +/// automatically via blanket impl. This allows consumers to be migrated +/// gradually to granular ports while existing ones continue +/// working without changes. pub trait FileStoragePort: FileReadPort + FileWritePort {} -/// Blanket implementation: cualquier tipo que implemente ambos ports -/// es automáticamente un FileStoragePort. +/// Blanket implementation: any type that implements both ports +/// is automatically a FileStoragePort. impl FileStoragePort for T {} -/// Puerto secundario para persistencia de carpetas (application layer). +/// Secondary port for folder persistence (application layer). /// -/// Tiene la misma firma que `FolderRepository` del dominio. -/// Las implementaciones concretas deben implementar `FolderRepository`, -/// obteniendo `FolderStoragePort` automáticamente vía blanket impl. +/// Has the same signature as the domain's `FolderRepository`. +/// Concrete implementations must implement `FolderRepository`, +/// getting `FolderStoragePort` automatically via blanket impl. pub trait FolderStoragePort: FolderRepository {} -/// Blanket implementation: cualquier tipo que implemente FolderRepository -/// es automáticamente un FolderStoragePort. +/// Blanket implementation: any type that implements FolderRepository +/// is automatically a FolderStoragePort. impl FolderStoragePort for T {} -/// Puerto secundario para mapeo de IDs +/// Secondary port for ID mapping #[async_trait] pub trait IdMappingPort: Send + Sync + 'static { - /// Obtiene o crea un ID para una ruta + /// Gets or creates an ID for a path async fn get_or_create_id(&self, path: &StoragePath) -> Result; - /// Obtiene una ruta por su ID + /// Gets a path by its ID async fn get_path_by_id(&self, id: &str) -> Result; - /// Actualiza la ruta para un ID existente + /// Updates the path for an existing ID async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError>; - /// Elimina un ID del mapeo + /// Removes an ID from the mapping async fn remove_id(&self, id: &str) -> Result<(), DomainError>; - /// Guarda cambios pendientes + /// Saves pending changes async fn save_changes(&self) -> Result<(), DomainError>; - /// Obtiene la ruta de archivo como PathBuf + /// Gets the file path as a PathBuf async fn get_file_path(&self, file_id: &str) -> Result { let storage_path = self.get_path_by_id(file_id).await?; Ok(PathBuf::from(storage_path.to_string())) } - /// Actualiza la ruta de un archivo + /// Updates a file's path async fn update_file_path(&self, file_id: &str, new_path: &PathBuf) -> Result<(), DomainError> { let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string()); self.update_path(file_id, &storage_path).await diff --git a/src/application/ports/recent_ports.rs b/src/application/ports/recent_ports.rs index ffa27593..714a4b3a 100644 --- a/src/application/ports/recent_ports.rs +++ b/src/application/ports/recent_ports.rs @@ -2,19 +2,19 @@ use async_trait::async_trait; use crate::common::errors::Result; use crate::application::dtos::recent_dto::RecentItemDto; -/// Define operaciones para gestionar elementos recientes del usuario +/// Defines operations for managing user recent items #[async_trait] pub trait RecentItemsUseCase: Send + Sync { - /// Obtener todos los elementos recientes de un usuario + /// Get all recent items for a user async fn get_recent_items(&self, user_id: &str, limit: Option) -> Result>; - /// Registrar acceso a un elemento + /// Record access to an item async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; - /// Eliminar un elemento de recientes + /// Remove an item from recents async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; - /// Limpiar toda la lista de elementos recientes + /// Clear the entire recent items list async fn clear_recent_items(&self, user_id: &str) -> Result<()>; } @@ -22,24 +22,24 @@ pub trait RecentItemsUseCase: Send + Sync { // Outbound port — persistence abstraction // ───────────────────────────────────────────────────── -/// Puerto secundario (outbound) para persistencia de elementos recientes. +/// Secondary (outbound) port for recent items persistence. /// -/// Abstrae el acceso a la tabla `auth.user_recent_files` para que -/// `RecentService` no dependa directamente de `PgPool`. +/// Abstracts access to the `auth.user_recent_files` table so that +/// `RecentService` does not depend directly on `PgPool`. #[async_trait] pub trait RecentItemsRepositoryPort: Send + Sync + 'static { - /// Obtiene los últimos elementos recientes de un usuario (ordenados por fecha desc). + /// Gets the latest recent items for a user (ordered by date desc). async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result>; - /// Registra/actualiza el acceso a un ítem (upsert por user+item+type). + /// Records/updates access to an item (upsert by user+item+type). async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; - /// Elimina un ítem de recientes. Devuelve `true` si existía. + /// Removes an item from recents. Returns `true` if it existed. async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; - /// Elimina todos los elementos recientes de un usuario. + /// Removes all recent items for a user. async fn clear_all(&self, user_id: &str) -> Result<()>; - /// Elimina elementos que excedan `max_items` (los más antiguos). + /// Removes items exceeding `max_items` (the oldest ones). async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>; } \ No newline at end of file diff --git a/src/application/ports/storage_ports.rs b/src/application/ports/storage_ports.rs index 2d8cc1c3..1235af49 100644 --- a/src/application/ports/storage_ports.rs +++ b/src/application/ports/storage_ports.rs @@ -17,28 +17,28 @@ pub use crate::domain::repositories::folder_repository::FolderRepository; // FileReadPort — application-layer alias for FileReadRepository // ───────────────────────────────────────────────────── -/// Puerto secundario para **lectura** de archivos. +/// Secondary port for file **reading**. /// -/// Encapsula toda operación que consulta estado sin modificarlo: -/// get, list, content, stream, mmap, range, resolución de rutas. +/// Encapsulates every operation that queries state without modifying it: +/// get, list, content, stream, mmap, range, path resolution. #[async_trait] pub trait FileReadPort: Send + Sync + 'static { - /// Obtiene un archivo por su ID. + /// Gets a file by its ID. async fn get_file(&self, id: &str) -> Result; - /// Lista archivos en una carpeta. + /// Lists files in a folder. async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError>; - /// Obtiene contenido completo como bytes (solo archivos pequeños/medianos). + /// Gets the full content as bytes (small/medium files only). async fn get_file_content(&self, id: &str) -> Result, DomainError>; - /// Obtiene contenido como stream (ideal para archivos grandes). + /// Gets content as a stream (ideal for large files). async fn get_file_stream( &self, id: &str, ) -> Result> + Send>, DomainError>; - /// Stream de un rango de bytes (HTTP Range Requests, video seek). + /// Stream of a byte range (HTTP Range Requests, video seek). async fn get_file_range_stream( &self, id: &str, @@ -46,13 +46,13 @@ pub trait FileReadPort: Send + Sync + 'static { end: Option, ) -> Result> + Send>, DomainError>; - /// Memory-map de archivo para acceso zero-copy (10–100 MB). + /// Memory-map of a file for zero-copy access (10–100 MB). async fn get_file_mmap(&self, id: &str) -> Result; - /// Obtiene la ruta de almacenamiento lógica de un archivo. + /// Gets the logical storage path of a file. async fn get_file_path(&self, id: &str) -> Result; - /// Obtiene el ID de la carpeta padre a partir de una ruta (WebDAV). + /// Gets the parent folder ID from a path (WebDAV). async fn get_parent_folder_id(&self, path: &str) -> Result; } @@ -60,13 +60,13 @@ pub trait FileReadPort: Send + Sync + 'static { // FileWritePort — all write / mutate operations // ───────────────────────────────────────────────────── -/// Puerto secundario para **escritura** de archivos. +/// Secondary port for file **writing**. /// -/// Cubre: upload (buffered + streaming), move, delete, update, -/// y el registro diferido para write-behind cache. +/// Covers: upload (buffered + streaming), move, delete, update, +/// and deferred registration for the write-behind cache. #[async_trait] pub trait FileWritePort: Send + Sync + 'static { - /// Guarda un nuevo archivo desde bytes. + /// Saves a new file from bytes. async fn save_file( &self, name: String, @@ -75,7 +75,7 @@ pub trait FileWritePort: Send + Sync + 'static { content: Vec, ) -> Result; - /// Upload en streaming — escribe chunks a disco sin acumular en RAM. + /// Streaming upload — writes chunks to disk without accumulating in RAM. async fn save_file_from_stream( &self, name: String, @@ -84,30 +84,30 @@ pub trait FileWritePort: Send + Sync + 'static { stream: std::pin::Pin> + Send>>, ) -> Result; - /// Mueve un archivo a otra carpeta. + /// Moves a file to another folder. async fn move_file( &self, file_id: &str, target_folder_id: Option, ) -> Result; - /// Renombra un archivo (same folder, different name). + /// Renames a file (same folder, different name). async fn rename_file( &self, file_id: &str, new_name: &str, ) -> Result; - /// Elimina un archivo. + /// Deletes a file. async fn delete_file(&self, id: &str) -> Result<(), DomainError>; - /// Actualiza el contenido de un archivo existente. + /// Updates the content of an existing file. async fn update_file_content(&self, file_id: &str, content: Vec) -> Result<(), DomainError>; - /// Registra metadatos de archivo SIN escribir contenido a disco (write-behind). + /// Registers file metadata WITHOUT writing content to disk (write-behind). /// - /// Devuelve `(File, PathBuf)` donde `PathBuf` es la ruta destino para la - /// escritura diferida que realizará el `WriteBehindCache`. + /// Returns `(File, PathBuf)` where `PathBuf` is the destination path for the + /// deferred write that the `WriteBehindCache` will perform. async fn register_file_deferred( &self, name: String, @@ -118,13 +118,13 @@ pub trait FileWritePort: Send + Sync + 'static { // ── Trash operations ── - /// Mueve un archivo a la papelera + /// Moves a file to the trash async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>; - /// Restaura un archivo desde la papelera a su ubicación original + /// Restores a file from the trash to its original location async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>; - /// Elimina un archivo permanentemente (usado por la papelera) + /// Permanently deletes a file (used by the trash) async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>; } @@ -132,40 +132,40 @@ pub trait FileWritePort: Send + Sync + 'static { // Auxiliary ports (unchanged) // ───────────────────────────────────────────────────── -/// Puerto secundario para resolución de rutas de archivos +/// Secondary port for file path resolution #[async_trait] pub trait FilePathResolutionPort: Send + Sync + 'static { - /// Obtiene la ruta de almacenamiento de un archivo + /// Gets the storage path of a file async fn get_file_path(&self, id: &str) -> Result; - /// Resuelve una ruta de dominio a una ruta física + /// Resolves a domain path to a physical path fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf; } -/// Puerto secundario para verificación de existencia de archivos/directorios +/// Secondary port for file/directory existence verification #[async_trait] pub trait StorageVerificationPort: Send + Sync + 'static { - /// Verifica si existe un archivo en la ruta dada + /// Checks whether a file exists at the given path async fn file_exists(&self, storage_path: &StoragePath) -> Result; - /// Verifica si existe un directorio en la ruta dada + /// Checks whether a directory exists at the given path async fn directory_exists(&self, storage_path: &StoragePath) -> Result; } -/// Puerto secundario para gestión de directorios +/// Secondary port for directory management #[async_trait] pub trait DirectoryManagementPort: Send + Sync + 'static { - /// Crea directorios si no existen + /// Creates directories if they do not exist async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>; } -/// Puerto secundario para gestión de uso de almacenamiento +/// Secondary port for storage usage management #[async_trait] pub trait StorageUsagePort: Send + Sync + 'static { - /// Actualiza estadísticas de uso de almacenamiento para un usuario + /// Updates storage usage statistics for a user async fn update_user_storage_usage(&self, user_id: &str) -> Result; - /// Actualiza estadísticas de uso de almacenamiento para todos los usuarios + /// Updates storage usage statistics for all users async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>; } diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index 66a83bc3..1940ebe0 100644 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -68,13 +68,13 @@ impl AuthApplicationService { } } - /// Configura el servicio de carpetas, necesario para crear carpetas personales + /// Configures the folder service, needed to create personal folders pub fn with_folder_service(mut self, folder_service: Arc) -> Self { self.folder_service = Some(folder_service); self } - /// Configura el servicio OIDC + /// Configures the OIDC service pub fn with_oidc(self, oidc_service: Arc, oidc_config: OidcConfig) -> Self { { let mut state = self.oidc.write().unwrap(); @@ -123,12 +123,12 @@ impl AuthApplicationService { } pub async fn register(&self, dto: RegisterDto) -> Result { - // Verificar usuario duplicado + // Check for duplicate user if self.user_storage.get_user_by_username(&dto.username).await.is_ok() { return Err(DomainError::new( ErrorKind::AlreadyExists, "User", - format!("El usuario '{}' ya existe", dto.username) + format!("User '{}' already exists", dto.username) )); } @@ -136,62 +136,62 @@ impl AuthApplicationService { return Err(DomainError::new( ErrorKind::AlreadyExists, "User", - format!("El email '{}' ya está registrado", dto.email) + format!("Email '{}' is already registered", dto.email) )); } - // Verificar si el usuario quiere crear un admin + // Check if the user wants to create an admin let is_admin_request = dto.username.to_lowercase() == "admin" || (dto.role.is_some() && dto.role.as_ref().unwrap().to_lowercase() == "admin"); - // Si está intentando crear un admin, verificar si ya existen admins en el sistema + // If trying to create an admin, check if admins already exist in the system if is_admin_request { match self.count_admin_users().await { Ok(admin_count) => { - // Si ya hay admins en el sistema y no estamos en instalación limpia, - // no permitimos crear otro admin desde el registro + // If there are already admins in the system and this is not a clean install, + // we do not allow creating another admin from registration if admin_count > 0 { - // Verificar si es una instalación limpia (solo el admin predeterminado) + // Check if this is a clean install (only the default admin) match self.count_all_users().await { Ok(user_count) => { - // Si hay más de 2 usuarios (admin + test), no es instalación limpia + // If there are more than 2 users (admin + test), it is not a clean install if user_count > 2 { - tracing::warn!("Intento de crear admin adicional rechazado: ya existe al menos un admin"); + tracing::warn!("Attempt to create additional admin rejected: at least one admin already exists"); return Err(DomainError::new( ErrorKind::AccessDenied, "User", - "No se permite crear usuarios admin adicionales desde la página de registro" + "Creating additional admin users from the registration page is not allowed" )); } - // En caso contrario, es instalación limpia y se permite el primer admin - tracing::info!("Permitiendo creación de admin en instalación limpia"); + // Otherwise, it is a clean install and the first admin is allowed + tracing::info!("Allowing admin creation on clean install"); }, Err(e) => { - tracing::error!("Error al contar usuarios: {}", e); - // Por seguridad, si no podemos verificar, rechazamos la creación de admin + tracing::error!("Error counting users: {}", e); + // For security, if we cannot verify, we reject admin creation return Err(DomainError::new( ErrorKind::AccessDenied, "User", - "No se permite crear usuarios admin adicionales" + "Creating additional admin users is not allowed" )); } } } }, Err(e) => { - tracing::error!("Error al contar usuarios admin: {}", e); - // Por seguridad, si no podemos verificar, rechazamos la creación de admin + tracing::error!("Error counting admin users: {}", e); + // For security, if we cannot verify, we reject admin creation return Err(DomainError::new( ErrorKind::AccessDenied, "User", - "No se permite crear usuarios admin adicionales" + "Creating additional admin users is not allowed" )); } } } - // Determinar rol y cuota según el tipo de usuario - // Si se proporciona un rol explícito de "admin", usar rol de administrador + // Determine role and quota based on user type + // If an explicit "admin" role is provided, use the administrator role let role = if let Some(role_str) = &dto.role { if role_str.to_lowercase() == "admin" { UserRole::Admin @@ -199,7 +199,7 @@ impl AuthApplicationService { UserRole::User } } else { - // Caso especial: si el nombre es "admin", asignar rol de admin aunque no se especifique + // Special case: if the username is "admin", assign admin role even if not specified if dto.username.to_lowercase() == "admin" { UserRole::Admin } else { @@ -207,26 +207,26 @@ impl AuthApplicationService { } }; - // Cuota según el rol: 100GB para admin, 1GB para usuarios normales + // Quota based on role: 100GB for admin, 1GB for regular users let quota = if role == UserRole::Admin { - 107374182400 // 100GB para admin + 107374182400 // 100GB for admin } else { - 1024 * 1024 * 1024 // 1GB para usuarios normales + 1024 * 1024 * 1024 // 1GB for regular users }; - // Validar longitud de password antes de hashear + // Validate password length before hashing if dto.password.len() < 8 { return Err(DomainError::new( ErrorKind::InvalidInput, "User", - "Password debe tener al menos 8 caracteres" + "Password must be at least 8 characters long" )); } - // Hashear el password usando el servicio de infraestructura + // Hash the password using the infrastructure service let password_hash = self.password_hasher.hash_password(&dto.password)?; - // Crear usuario con el hash pre-generado + // Create user with the pre-generated hash let user = User::new( dto.username.clone(), dto.email, @@ -236,13 +236,13 @@ impl AuthApplicationService { ).map_err(|e| DomainError::new( ErrorKind::InvalidInput, "User", - format!("Error al crear usuario: {}", e) + format!("Error creating user: {}", e) ))?; - // Guardar usuario + // Save user let created_user = self.user_storage.create_user(user).await?; - // Crear carpeta personal para el usuario + // Create personal folder for the user if let Some(folder_service) = &self.folder_service { let folder_name = format!("Mi Carpeta - {}", dto.username); @@ -252,20 +252,20 @@ impl AuthApplicationService { }).await { Ok(folder) => { tracing::info!( - "Carpeta personal creada para el usuario {}: {} (ID: {})", + "Personal folder created for user {}: {} (ID: {})", created_user.id(), folder.name, folder.id ); - // Aquí se podría guardar la asociación de la carpeta al usuario - // por ejemplo, en una tabla de relación carpeta-usuario + // Here we could save the folder-to-user association, + // for example, in a folder-user relationship table }, Err(e) => { - // No fallamos el registro por un error en la creación de la carpeta - // pero lo registramos para investigación + // We don't fail registration due to a folder creation error, + // but we log it for investigation tracing::error!( - "No se pudo crear la carpeta personal para el usuario {}: {}", + "Could not create personal folder for user {}: {}", created_user.id(), e ); @@ -273,67 +273,67 @@ impl AuthApplicationService { } } else { tracing::warn!( - "No se configuró el servicio de carpetas, no se puede crear carpeta personal para el usuario: {}", + "Folder service not configured, cannot create personal folder for user: {}", created_user.id() ); } - tracing::info!("Usuario registrado: {}", created_user.id()); + tracing::info!("User registered: {}", created_user.id()); Ok(UserDto::from(created_user)) } pub async fn login(&self, dto: LoginDto) -> Result { - // Buscar usuario + // Find user let mut user = self.user_storage .get_user_by_username(&dto.username) .await .map_err(|_| DomainError::new( ErrorKind::AccessDenied, "Auth", - "Credenciales inválidas" + "Invalid credentials" ))?; - // Verificar si usuario está activo + // Check if user is active if !user.is_active() { return Err(DomainError::new( ErrorKind::AccessDenied, "Auth", - "Cuenta desactivada" + "Account deactivated" )); } - // Verificar contraseña usando el hasher inyectado + // Verify password using the injected hasher let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?; if !is_valid { return Err(DomainError::new( ErrorKind::AccessDenied, "Auth", - "Credenciales inválidas" + "Invalid credentials" )); } - // Actualizar último login + // Update last login user.register_login(); self.user_storage.update_user(user.clone()).await?; - // Generar tokens usando el servicio de tokens inyectado + // Generate tokens using the injected token service let access_token = self.token_service.generate_access_token(&user)?; let refresh_token = self.token_service.generate_refresh_token(); - // Guardar sesión + // Save session let session = Session::new( user.id().to_string(), refresh_token.clone(), - None, // IP (se puede añadir desde la capa HTTP) - None, // User-Agent (se puede añadir desde la capa HTTP) + None, // IP (can be added from the HTTP layer) + None, // User-Agent (can be added from the HTTP layer) self.token_service.refresh_token_expiry_days(), ); self.session_storage.create_session(session).await?; - // Respuesta de autenticación + // Authentication response Ok(AuthResponseDto { user: UserDto::from(user), access_token, @@ -344,43 +344,43 @@ impl AuthApplicationService { } pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result { - // Obtener sesión válida + // Get valid session let session = self.session_storage .get_session_by_refresh_token(&dto.refresh_token) .await?; - // Verificar si la sesión está expirada o revocada + // Check if the session is expired or revoked if session.is_expired() || session.is_revoked() { return Err(DomainError::new( ErrorKind::AccessDenied, "Auth", - "Sesión expirada o inválida" + "Session expired or invalid" )); } - // Obtener usuario + // Get user let user = self.user_storage .get_user_by_id(session.user_id()) .await?; - // Verificar si usuario está activo + // Check if user is active if !user.is_active() { return Err(DomainError::new( ErrorKind::AccessDenied, "Auth", - "Cuenta desactivada" + "Account deactivated" )); } - // Revocar sesión actual + // Revoke current session self.session_storage.revoke_session(session.id()).await?; - // Generar nuevos tokens + // Generate new tokens let access_token = self.token_service.generate_access_token(&user)?; let new_refresh_token = self.token_service.generate_refresh_token(); - // Crear nueva sesión + // Create new session let new_session = Session::new( user.id().to_string(), new_refresh_token.clone(), @@ -401,67 +401,67 @@ impl AuthApplicationService { } pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> { - // Obtener sesión + // Get session let session = match self.session_storage.get_session_by_refresh_token(refresh_token).await { Ok(s) => s, - // Si la sesión no existe, consideramos el logout como exitoso + // If the session doesn't exist, we consider the logout successful Err(_) => return Ok(()), }; - // Verificar que la sesión pertenece al usuario + // Verify that the session belongs to the user if session.user_id() != user_id { return Err(DomainError::new( ErrorKind::AccessDenied, "Auth", - "La sesión no pertenece al usuario" + "The session does not belong to the user" )); } - // Revocar sesión + // Revoke session self.session_storage.revoke_session(session.id()).await?; Ok(()) } pub async fn logout_all(&self, user_id: &str) -> Result { - // Revocar todas las sesiones del usuario + // Revoke all user sessions let revoked_count = self.session_storage.revoke_all_user_sessions(user_id).await?; Ok(revoked_count) } pub async fn change_password(&self, user_id: &str, dto: ChangePasswordDto) -> Result<(), DomainError> { - // Obtener usuario + // Get user let mut user = self.user_storage.get_user_by_id(user_id).await?; - // Verificar contraseña actual usando el hasher inyectado + // Verify current password using the injected hasher let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?; if !is_valid { return Err(DomainError::new( ErrorKind::AccessDenied, "Auth", - "Contraseña actual incorrecta" + "Current password is incorrect" )); } - // Validar nueva contraseña + // Validate new password if dto.new_password.len() < 8 { return Err(DomainError::new( ErrorKind::InvalidInput, "User", - "Password debe tener al menos 8 caracteres" + "Password must be at least 8 characters long" )); } - // Hashear nueva contraseña y actualizar usuario + // Hash new password and update user let new_hash = self.password_hasher.hash_password(&dto.new_password)?; user.update_password_hash(new_hash); - // Guardar usuario actualizado + // Save updated user self.user_storage.update_user(user).await?; - // Opcional: revocar todas las sesiones para forzar re-login con nueva contraseña + // Optional: revoke all sessions to force re-login with new password self.session_storage.revoke_all_user_sessions(user_id).await?; Ok(()) @@ -492,7 +492,7 @@ impl AuthApplicationService { .map_err(|e| DomainError::new( ErrorKind::InternalError, "User", - format!("Error al contar usuarios administradores: {}", e) + format!("Error counting admin users: {}", e) ))?; Ok(admin_users.len() as i64) @@ -506,7 +506,7 @@ impl AuthApplicationService { .map_err(|e| DomainError::new( ErrorKind::InternalError, "User", - format!("Error al contar usuarios: {}", e) + format!("Error counting users: {}", e) ))?; Ok(all_users.len() as i64) @@ -523,7 +523,7 @@ impl AuthApplicationService { .map_err(|e| DomainError::new( ErrorKind::InternalError, "User", - format!("Error al eliminar usuario admin predeterminado: {}", e) + format!("Error deleting default admin user: {}", e) )) }, Err(_) => { @@ -545,7 +545,7 @@ impl AuthApplicationService { .map_err(|e| DomainError::new( ErrorKind::InternalError, "User", - format!("Error al eliminar usuario admin predeterminado: {}", e) + format!("Error deleting default admin user: {}", e) ))?; // 3. Create new admin user with the provided credentials but admin role @@ -564,7 +564,7 @@ impl AuthApplicationService { ).map_err(|e| DomainError::new( ErrorKind::InvalidInput, "User", - format!("Error al crear usuario admin: {}", e) + format!("Error creating admin user: {}", e) ))?; // 4. Save the new admin user @@ -580,7 +580,7 @@ impl AuthApplicationService { }).await { Ok(folder) => { tracing::info!( - "Carpeta personal creada para el admin {}: {} (ID: {})", + "Personal folder created for admin {}: {} (ID: {})", created_user.id(), folder.name, folder.id @@ -588,7 +588,7 @@ impl AuthApplicationService { }, Err(e) => { tracing::error!( - "No se pudo crear la carpeta personal para el admin {}: {}", + "Could not create personal folder for admin {}: {}", created_user.id(), e ); @@ -596,7 +596,7 @@ impl AuthApplicationService { } } - tracing::info!("Admin personalizado creado: {}", created_user.id()); + tracing::info!("Custom admin created: {}", created_user.id()); Ok(UserDto::from(created_user)) } diff --git a/src/application/services/batch_operations.rs b/src/application/services/batch_operations.rs index da76bbf0..9c279a8b 100644 --- a/src/application/services/batch_operations.rs +++ b/src/application/services/batch_operations.rs @@ -12,52 +12,52 @@ use crate::application::ports::inbound::FolderUseCase; use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::folder_dto::FolderDto; -/// Errores específicos para operaciones por lotes +/// Specific errors for batch operations #[derive(Debug, Error)] pub enum BatchOperationError { - #[error("Error de dominio: {0}")] + #[error("Domain error: {0}")] Domain(#[from] DomainError), - #[error("Operación cancelada: {0}")] + #[error("Operation cancelled: {0}")] Cancelled(String), - #[error("Límite de concurrencia excedido: {0}")] + #[error("Concurrency limit exceeded: {0}")] ConcurrencyLimit(String), - #[error("Error en operación del lote: {0} ({1} de {2} completadas)")] + #[error("Batch operation error: {0} ({1} of {2} completed)")] PartialFailure(String, usize, usize), - #[error("Error interno: {0}")] + #[error("Internal error: {0}")] Internal(String), } -/// Resultado de una operación por lotes con estadísticas +/// Result of a batch operation with statistics #[derive(Debug, Clone)] pub struct BatchResult { - /// Resultados exitosos + /// Successful results pub successful: Vec, - /// Operaciones fallidas con sus errores + /// Failed operations with their errors pub failed: Vec<(String, String)>, - /// Estadísticas de la operación + /// Operation statistics pub stats: BatchStats, } -/// Estadísticas de una operación por lotes +/// Statistics of a batch operation #[derive(Debug, Clone, Default)] pub struct BatchStats { - /// Número total de operaciones + /// Total number of operations pub total: usize, - /// Número de operaciones exitosas + /// Number of successful operations pub successful: usize, - /// Número de operaciones fallidas + /// Number of failed operations pub failed: usize, - /// Tiempo total de ejecución en milisegundos + /// Total execution time in milliseconds pub execution_time_ms: u128, - /// Concurrencia máxima alcanzada + /// Maximum concurrency reached pub max_concurrency: usize, } -/// Servicio de operaciones por lotes +/// Batch operations service pub struct BatchOperationService { file_retrieval: Arc, file_management: Arc, @@ -67,14 +67,14 @@ pub struct BatchOperationService { } impl BatchOperationService { - /// Crea una nueva instancia del servicio de operaciones por lotes + /// Creates a new instance of the batch operations service pub fn new( file_retrieval: Arc, file_management: Arc, folder_service: Arc, config: AppConfig ) -> Self { - // Limitar la concurrencia basada en la configuración + // Limit concurrency based on configuration let max_concurrency = config.concurrency.max_concurrent_files; Self { @@ -86,7 +86,7 @@ impl BatchOperationService { } } - /// Crea una nueva instancia con la configuración por defecto + /// Creates a new instance with default configuration pub fn default( file_retrieval: Arc, file_management: Arc, @@ -95,16 +95,16 @@ impl BatchOperationService { Self::new(file_retrieval, file_management, folder_service, AppConfig::default()) } - /// Copia múltiples archivos en paralelo + /// Copies multiple files in parallel pub async fn copy_files( &self, file_ids: Vec, target_folder_id: Option, ) -> Result, BatchOperationError> { - info!("Iniciando copia en lote de {} archivos", file_ids.len()); + info!("Starting batch copy of {} files", file_ids.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -114,30 +114,30 @@ impl BatchOperationService { }, }; - // Definir la operación a realizar para cada archivo + // Define the operation to perform for each file let operations = file_ids.into_iter().map(|file_id| { let mgmt = self.file_management.clone(); let target_folder = target_folder_id.clone(); let semaphore = self.semaphore.clone(); async move { - // Adquirir permiso del semáforo + // Acquire semaphore permit let permit = semaphore.acquire().await.unwrap(); let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await; - // Liberar el permiso explícitamente (también se libera al hacer drop) + // Release the permit explicitly (also released on drop) drop(permit); - // Devolver el resultado junto con el ID para identificar éxitos/fallos + // Return the result along with the ID to identify successes/failures (file_id, copy_result) } }); - // Ejecutar todas las operaciones en paralelo con control de concurrencia + // Execute all operations in parallel with concurrency control let operation_results = join_all(operations).await; - // Procesar los resultados + // Process the results for (file_id, operation_result) in operation_results { match operation_result { Ok(file) => { @@ -151,13 +151,13 @@ impl BatchOperationService { } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Copia en lote completada: {}/{} exitosas en {}ms", + "Batch copy completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -166,16 +166,16 @@ impl BatchOperationService { Ok(result) } - /// Mueve múltiples archivos en paralelo + /// Moves multiple files in parallel pub async fn move_files( &self, file_ids: Vec, target_folder_id: Option, ) -> Result, BatchOperationError> { - info!("Iniciando movimiento en lote de {} archivos", file_ids.len()); + info!("Starting batch move of {} files", file_ids.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -185,30 +185,30 @@ impl BatchOperationService { }, }; - // Definir la operación a realizar para cada archivo + // Define the operation to perform for each file let operations = file_ids.into_iter().map(|file_id| { let mgmt = self.file_management.clone(); let target_folder = target_folder_id.clone(); let semaphore = self.semaphore.clone(); async move { - // Adquirir permiso del semáforo + // Acquire semaphore permit let permit = semaphore.acquire().await.unwrap(); let move_result = mgmt.move_file(&file_id, target_folder.clone()).await; - // Liberar el permiso explícitamente + // Release the permit explicitly drop(permit); - // Devolver el resultado junto con el ID para identificar éxitos/fallos + // Return the result along with the ID to identify successes/failures (file_id, move_result) } }); - // Ejecutar todas las operaciones en paralelo con control de concurrencia + // Execute all operations in parallel with concurrency control let operation_results = join_all(operations).await; - // Procesar los resultados + // Process the results for (file_id, operation_result) in operation_results { match operation_result { Ok(file) => { @@ -222,13 +222,13 @@ impl BatchOperationService { } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Movimiento en lote completado: {}/{} exitosas en {}ms", + "Batch move completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -237,15 +237,15 @@ impl BatchOperationService { Ok(result) } - /// Elimina múltiples archivos en paralelo + /// Deletes multiple files in parallel pub async fn delete_files( &self, file_ids: Vec, ) -> Result, BatchOperationError> { - info!("Iniciando eliminación en lote de {} archivos", file_ids.len()); + info!("Starting batch deletion of {} files", file_ids.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -255,30 +255,30 @@ impl BatchOperationService { }, }; - // Definir la operación a realizar para cada archivo + // Define the operation to perform for each file let operations = file_ids.into_iter().map(|file_id| { let mgmt = self.file_management.clone(); let semaphore = self.semaphore.clone(); let id_clone = file_id.clone(); async move { - // Adquirir permiso del semáforo + // Acquire semaphore permit let permit = semaphore.acquire().await.unwrap(); let delete_result = mgmt.delete_file(&file_id).await; - // Liberar el permiso explícitamente + // Release the permit explicitly drop(permit); - // Devolver el resultado junto con el ID + // Return the result along with the ID (id_clone.clone(), delete_result.map(|_| id_clone)) } }); - // Ejecutar todas las operaciones en paralelo con control de concurrencia + // Execute all operations in parallel with concurrency control let operation_results = join_all(operations).await; - // Procesar los resultados + // Process the results for (file_id, operation_result) in operation_results { match operation_result { Ok(id) => { @@ -292,13 +292,13 @@ impl BatchOperationService { } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Eliminación en lote completada: {}/{} exitosas en {}ms", + "Batch deletion completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -307,15 +307,15 @@ impl BatchOperationService { Ok(result) } - /// Carga múltiples archivos en paralelo (datos en memoria) + /// Loads multiple files in parallel (data in memory) pub async fn get_multiple_files( &self, file_ids: Vec, ) -> Result, BatchOperationError> { - info!("Iniciando carga en lote de {} archivos", file_ids.len()); + info!("Starting batch load of {} files", file_ids.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -325,29 +325,29 @@ impl BatchOperationService { }, }; - // Definir la operación a realizar para cada archivo + // Define the operation to perform for each file let operations = file_ids.into_iter().map(|file_id| { let retrieval = self.file_retrieval.clone(); let semaphore = self.semaphore.clone(); async move { - // Adquirir permiso del semáforo + // Acquire semaphore permit let permit = semaphore.acquire().await.unwrap(); let get_result = retrieval.get_file(&file_id).await; - // Liberar el permiso explícitamente + // Release the permit explicitly drop(permit); - // Devolver el resultado junto con el ID + // Return the result along with the ID (file_id, get_result) } }); - // Ejecutar todas las operaciones en paralelo con control de concurrencia + // Execute all operations in parallel with concurrency control let operation_results = join_all(operations).await; - // Procesar los resultados + // Process the results for (file_id, operation_result) in operation_results { match operation_result { Ok(file) => { @@ -361,13 +361,13 @@ impl BatchOperationService { } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Carga en lote completada: {}/{} exitosas en {}ms", + "Batch load completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -376,16 +376,16 @@ impl BatchOperationService { Ok(result) } - /// Elimina múltiples carpetas en paralelo + /// Deletes multiple folders in parallel pub async fn delete_folders( &self, folder_ids: Vec, _recursive: bool, ) -> Result, BatchOperationError> { - info!("Iniciando eliminación en lote de {} carpetas", folder_ids.len()); + info!("Starting batch deletion of {} folders", folder_ids.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -395,32 +395,32 @@ impl BatchOperationService { }, }; - // Definir la operación a realizar para cada carpeta + // Define the operation to perform for each folder let operations = folder_ids.into_iter().map(|folder_id| { let folder_service = self.folder_service.clone(); let semaphore = self.semaphore.clone(); let id_clone = folder_id.clone(); async move { - // Adquirir permiso del semáforo + // Acquire semaphore permit let permit = semaphore.acquire().await.unwrap(); // For both recursive and non-recursive, use the standard delete_folder method // since FolderUseCase only has a single delete_folder method let delete_result = folder_service.delete_folder(&folder_id).await; - // Liberar el permiso explícitamente + // Release the permit explicitly drop(permit); - // Devolver el resultado junto con el ID + // Return the result along with the ID (id_clone.clone(), delete_result.map(|_| id_clone)) } }); - // Ejecutar todas las operaciones en paralelo con control de concurrencia + // Execute all operations in parallel with concurrency control let operation_results = join_all(operations).await; - // Procesar los resultados + // Process the results for (folder_id, operation_result) in operation_results { match operation_result { Ok(id) => { @@ -434,13 +434,13 @@ impl BatchOperationService { } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Eliminación en lote de carpetas completada: {}/{} exitosas en {}ms", + "Batch folder deletion completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -449,7 +449,7 @@ impl BatchOperationService { Ok(result) } - /// Operación genérica de lote para cualquier tipo de función asíncrona + /// Generic batch operation for any type of async function pub async fn generic_batch_operation( &self, items: Vec, @@ -460,10 +460,10 @@ impl BatchOperationService { F: Fn(T, Arc) -> Fut + Clone + Send + Sync + 'static, Fut: Future> + Send + 'static, { - info!("Iniciando operación genérica en lote con {} items", items.len()); + info!("Starting generic batch operation with {} items", items.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -473,25 +473,25 @@ impl BatchOperationService { }, }; - // Convertir cada item a una tarea + // Convert each item to a task let tasks = items.iter().map(|item| { let item_clone = item.clone(); let op = operation.clone(); let semaphore = self.semaphore.clone(); async move { - // La función proporcionada debe manejar la adquisición del semáforo + // The provided function must handle semaphore acquisition let op_result = op(item_clone.clone(), semaphore).await; - // Devolver el resultado junto con el item original para identificación + // Return the result along with the original item for identification (item_clone, op_result) } }); - // Ejecutar todas las tareas en paralelo + // Execute all tasks in parallel let operation_results = join_all(tasks).await; - // Procesar resultados + // Process results for (item, operation_result) in operation_results { match operation_result { Ok(result_item) => { @@ -499,20 +499,20 @@ impl BatchOperationService { result.stats.successful += 1; } Err(e) => { - // Convertir el item a string para el reporte de error + // Convert item to string for error reporting result.failed.push((format!("{:?}", item), e.to_string())); result.stats.failed += 1; } } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Operación genérica en lote completada: {}/{} exitosas en {}ms", + "Generic batch operation completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -521,15 +521,15 @@ impl BatchOperationService { Ok(result) } - /// Crear múltiples carpetas en paralelo + /// Create multiple folders in parallel pub async fn create_folders( &self, - folders: Vec<(String, Option)>, // (nombre, padre_id) + folders: Vec<(String, Option)>, // (name, parent_id) ) -> Result, BatchOperationError> { - info!("Iniciando creación en lote de {} carpetas", folders.len()); + info!("Starting batch creation of {} folders", folders.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -539,13 +539,13 @@ impl BatchOperationService { }, }; - // Definir la operación para cada carpeta + // Define the operation for each folder let operations = folders.into_iter().map(|(name, parent_id)| { let folder_service = self.folder_service.clone(); let semaphore = self.semaphore.clone(); async move { - // Adquirir permiso del semáforo + // Acquire semaphore permit let permit = semaphore.acquire().await.unwrap(); let dto = crate::application::dtos::folder_dto::CreateFolderDto { @@ -554,19 +554,19 @@ impl BatchOperationService { }; let create_result = folder_service.create_folder(dto).await; - // Liberar el permiso explícitamente + // Release the permit explicitly drop(permit); - // Devolver el resultado con un identificador para los errores + // Return the result with an identifier for errors let id = format!("{}:{}", name, parent_id.unwrap_or_default()); (id, create_result) } }); - // Ejecutar todas las operaciones en paralelo + // Execute all operations in parallel let operation_results = join_all(operations).await; - // Procesar los resultados + // Process the results for (id, operation_result) in operation_results { match operation_result { Ok(folder) => { @@ -580,13 +580,13 @@ impl BatchOperationService { } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Creación en lote de carpetas completada: {}/{} exitosas en {}ms", + "Batch folder creation completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -595,15 +595,15 @@ impl BatchOperationService { Ok(result) } - /// Obtener metadatos de múltiples carpetas en paralelo + /// Get metadata of multiple folders in parallel pub async fn get_multiple_folders( &self, folder_ids: Vec, ) -> Result, BatchOperationError> { - info!("Iniciando carga en lote de {} carpetas", folder_ids.len()); + info!("Starting batch load of {} folders", folder_ids.len()); let start_time = std::time::Instant::now(); - // Crear estructura para el resultado + // Create result structure let mut result = BatchResult { successful: Vec::new(), failed: Vec::new(), @@ -613,29 +613,29 @@ impl BatchOperationService { }, }; - // Definir la operación para cada carpeta + // Define the operation for each folder let operations = folder_ids.into_iter().map(|folder_id| { let folder_service = self.folder_service.clone(); let semaphore = self.semaphore.clone(); async move { - // Adquirir permiso del semáforo + // Acquire semaphore permit let permit = semaphore.acquire().await.unwrap(); let get_result = folder_service.get_folder(&folder_id).await; - // Liberar el permiso explícitamente + // Release the permit explicitly drop(permit); - // Devolver el resultado con su ID + // Return the result with its ID (folder_id, get_result) } }); - // Ejecutar todas las operaciones en paralelo + // Execute all operations in parallel let operation_results = join_all(operations).await; - // Procesar los resultados + // Process the results for (folder_id, operation_result) in operation_results { match operation_result { Ok(folder) => { @@ -649,13 +649,13 @@ impl BatchOperationService { } } - // Completar estadísticas + // Complete statistics result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.max_concurrency = self.config.concurrency.max_concurrent_files .min(result.stats.total); info!( - "Carga en lote de carpetas completada: {}/{} exitosas en {}ms", + "Batch folder load completed: {}/{} successful in {}ms", result.stats.successful, result.stats.total, result.stats.execution_time_ms @@ -673,7 +673,7 @@ mod tests { #[tokio::test] async fn test_generic_batch_operation() { - // Crear el servicio de batch with stubs + // Create the batch service with stubs let batch_service = BatchOperationService::new( Arc::new(StubFileRetrievalUseCase), Arc::new(StubFileManagementUseCase), @@ -683,35 +683,35 @@ mod tests { AppConfig::default() ); - // Definir una operación genérica de prueba + // Define a generic test operation let operation = |item: i32, semaphore: Arc| async move { - // Adquirir y liberar el semáforo + // Acquire and release the semaphore let _permit = semaphore.acquire().await.unwrap(); if item % 2 == 0 { - // Simular éxito para números pares + // Simulate success for even numbers Ok(item * 2) } else { - // Simular error para números impares + // Simulate error for odd numbers Err(DomainError::validation_error("Odd number not allowed")) } }; - // Ejecutar la operación de batch + // Execute the batch operation let items = vec![1, 2, 3, 4, 5]; let result = batch_service.generic_batch_operation(items, operation).await.unwrap(); - // Verificar los resultados + // Verify the results assert_eq!(result.stats.total, 5); assert_eq!(result.stats.successful, 2); assert_eq!(result.stats.failed, 3); - // Los números pares deberían estar en los éxitos, duplicados + // Even numbers should be in successes, doubled assert!(result.successful.contains(&4)); // 2*2 assert!(result.successful.contains(&8)); // 4*2 - // Los impares deberían estar en los fallos + // Odd numbers should be in failures assert_eq!(result.failed.len(), 3); } } \ No newline at end of file diff --git a/src/application/services/file_retrieval_service.rs b/src/application/services/file_retrieval_service.rs index 88390e95..671c025f 100644 --- a/src/application/services/file_retrieval_service.rs +++ b/src/application/services/file_retrieval_service.rs @@ -16,7 +16,7 @@ const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024; /// Threshold above which mmap is used instead of streaming (100 MB). const MMAP_THRESHOLD: u64 = 100 * 1024 * 1024; -/// Servicio para operaciones de recuperación de archivos +/// Service for file retrieval operations /// /// Implements a multi-tier download strategy: /// - Tier 0: Write-behind cache (just-uploaded files still in RAM) diff --git a/src/application/services/file_upload_service.rs b/src/application/services/file_upload_service.rs index 329d8d50..a21c37b2 100644 --- a/src/application/services/file_upload_service.rs +++ b/src/application/services/file_upload_service.rs @@ -29,7 +29,7 @@ fn extract_username_from_path(path: &str) -> Option { Some(parts[1].trim().to_string()) } -/// Servicio para operaciones de subida de archivos +/// Service for file upload operations /// /// Encapsulates the three-tier upload strategy: /// 1. **Write-Behind** (<256 KB): store in RAM, respond instantly, flush async. @@ -78,7 +78,7 @@ impl FileUploadService { } } - /// Configura el servicio de uso de almacenamiento + /// Configures the storage usage service pub fn with_storage_usage_service( mut self, storage_usage_service: Arc, diff --git a/src/application/services/file_use_case_factory.rs b/src/application/services/file_use_case_factory.rs index c26f5b17..877d9a68 100644 --- a/src/application/services/file_use_case_factory.rs +++ b/src/application/services/file_use_case_factory.rs @@ -6,14 +6,14 @@ 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 +/// Factory for creating file use case implementations pub struct AppFileUseCaseFactory { file_read_repository: Arc, file_write_repository: Arc, } impl AppFileUseCaseFactory { - /// Crea una nueva factory para casos de uso de archivos + /// Creates a new factory for file use cases pub fn new( file_read_repository: Arc, file_write_repository: Arc diff --git a/src/application/services/folder_service.rs b/src/application/services/folder_service.rs index 2312e282..5e10013f 100644 --- a/src/application/services/folder_service.rs +++ b/src/application/services/folder_service.rs @@ -7,13 +7,13 @@ use crate::application::ports::outbound::FolderStoragePort; use crate::application::transactions::storage_transaction::StorageTransaction; use crate::common::errors::{DomainError, ErrorKind}; -/// Implementación del caso de uso para operaciones de carpetas +/// Implementation of the use case for folder operations pub struct FolderService { folder_storage: Arc, } impl FolderService { - /// Crea un nuevo servicio de carpetas + /// Creates a new folder service pub fn new(folder_storage: Arc) -> Self { Self { folder_storage } } @@ -72,9 +72,9 @@ impl FolderService { #[async_trait] impl FolderUseCase for FolderService { - /// Crea una nueva carpeta + /// Creates a new folder async fn create_folder(&self, dto: CreateFolderDto) -> Result { - // Validación de entrada + // Input validation if dto.name.is_empty() { return Err(DomainError::new( ErrorKind::InvalidInput, @@ -83,7 +83,7 @@ impl FolderUseCase for FolderService { )); } - // Si se proporciona un parent_id, verificar que existe + // If a parent_id is provided, verify it exists if let Some(parent_id) = &dto.parent_id { let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok(); if !parent_exists { @@ -91,16 +91,16 @@ impl FolderUseCase for FolderService { } } - // Crear la carpeta + // Create the folder let folder = self.folder_storage.create_folder(dto.name, dto.parent_id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?; - // Convertir a DTO + // Convert to DTO Ok(FolderDto::from(folder)) } - /// Obtiene una carpeta por su ID + /// Gets a folder by its ID async fn get_folder(&self, id: &str) -> Result { let folder = self.folder_storage.get_folder(id) .await @@ -109,9 +109,9 @@ impl FolderUseCase for FolderService { Ok(FolderDto::from(folder)) } - /// Obtiene una carpeta por su ruta + /// Gets a folder by its path async fn get_folder_by_path(&self, path: &str) -> Result { - // Convertir la ruta de string a StoragePath + // Convert the string path to StoragePath let storage_path = StoragePath::from_string(path); let folder = self.folder_storage.get_folder_by_path(&storage_path) @@ -121,39 +121,39 @@ impl FolderUseCase for FolderService { Ok(FolderDto::from(folder)) } - /// Lista carpetas dentro de una carpeta padre + /// Lists folders within a parent folder async fn list_folders(&self, parent_id: Option<&str>) -> Result, DomainError> { let folders = self.folder_storage.list_folders(parent_id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?; - // Convertir a DTOs + // Convert to DTOs Ok(folders.into_iter().map(FolderDto::from).collect()) } - /// Lista carpetas con paginación + /// Lists folders with pagination async fn list_folders_paginated( &self, parent_id: Option<&str>, pagination: &crate::application::dtos::pagination::PaginationRequestDto ) -> Result, DomainError> { - // Validar y ajustar la paginación + // Validate and adjust pagination let pagination = pagination.validate_and_adjust(); - // Obtener carpetas paginadas y conteo total + // Get paginated folders and total count let (folders, total_items) = self.folder_storage.list_folders_paginated( parent_id, pagination.offset(), pagination.limit(), - true // Siempre incluir total para mejor UX + true // Always include total for better UX ) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?; - // El total es necesario para calcular la paginación + // The total is needed to calculate pagination let total = total_items.unwrap_or(folders.len()); - // Convertir a PaginatedResponseDto + // Convert to PaginatedResponseDto let response = crate::application::dtos::pagination::PaginatedResponseDto::new( folders.into_iter().map(FolderDto::from).collect(), pagination.page, @@ -164,9 +164,9 @@ impl FolderUseCase for FolderService { Ok(response) } - /// Renombra una carpeta + /// Renames a folder async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result { - // Validación de entrada + // Input validation if dto.name.is_empty() { return Err(DomainError::new( ErrorKind::InvalidInput, @@ -175,15 +175,15 @@ impl FolderUseCase for FolderService { )); } - // Verificar que la carpeta existe + // Verify the folder exists let existing_folder = self.folder_storage.get_folder(id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?; - // Crear transacción para renombrar + // Create transaction for renaming let mut transaction = StorageTransaction::new("rename_folder"); - // Operación principal: renombrar carpeta + // Main operation: rename folder // Clone all values to avoid lifetime issues let folder_storage = self.folder_storage.clone(); let id_owned = id.to_string(); @@ -200,7 +200,7 @@ impl FolderUseCase for FolderService { let id_clone = id.to_string(); async move { - // En caso de fallo, restaurar el nombre original + // In case of failure, restore the original name storage.rename_folder(&id_clone, original_name).await .map(|_| ()) .map_err(|e| DomainError::new( @@ -211,13 +211,13 @@ impl FolderUseCase for FolderService { } }; - // Añadir a la transacción + // Add to the transaction transaction.add_operation(rename_op, rollback_op); - // Ejecutar transacción + // Execute transaction transaction.commit().await?; - // Obtener la carpeta renombrada + // Get the renamed folder let folder = self.folder_storage.get_folder(id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?; @@ -225,16 +225,16 @@ impl FolderUseCase for FolderService { Ok(FolderDto::from(folder)) } - /// Mueve una carpeta a un nuevo padre + /// Moves a folder to a new parent async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result { - // Verificar que la carpeta origen existe + // Verify the source folder exists let source_folder = self.folder_storage.get_folder(id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?; - // Si se especifica un parent_id, verificar que existe + // If a parent_id is specified, verify it exists if let Some(parent_id) = &dto.parent_id { - // Verificar que no estamos intentando mover la carpeta a sí misma o a uno de sus descendientes + // Verify we are not trying to move the folder into itself or one of its descendants if parent_id == id { return Err(DomainError::new( ErrorKind::InvalidInput, @@ -243,19 +243,19 @@ impl FolderUseCase for FolderService { )); } - // Verificar que el destino existe + // Verify the destination exists let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok(); if !parent_exists { return Err(DomainError::not_found("Folder", parent_id)); } - // TODO: Idealmente deberíamos verificar toda la jerarquía para evitar ciclos + // TODO: Ideally we should verify the entire hierarchy to prevent cycles } - // Crear transacción para mover + // Create transaction for moving let mut transaction = StorageTransaction::new("move_folder"); - // Operación principal: mover carpeta + // Main operation: move folder // Clone all values to avoid lifetime issues let folder_storage = self.folder_storage.clone(); let id_owned = id.to_string(); @@ -275,7 +275,7 @@ impl FolderUseCase for FolderService { let id_clone = id.to_string(); async move { - // En caso de fallo, restaurar la ubicación original + // In case of failure, restore the original location storage.move_folder(&id_clone, original_parent_id.as_deref()).await .map(|_| ()) .map_err(|e| DomainError::new( @@ -286,13 +286,13 @@ impl FolderUseCase for FolderService { } }; - // Añadir a la transacción + // Add to the transaction transaction.add_operation(move_op, rollback_op); - // Ejecutar transacción + // Execute transaction transaction.commit().await?; - // Obtener la carpeta movida + // Get the moved folder let folder = self.folder_storage.get_folder(id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?; @@ -300,16 +300,16 @@ impl FolderUseCase for FolderService { Ok(FolderDto::from(folder)) } - /// Elimina una carpeta + /// Deletes a folder async fn delete_folder(&self, id: &str) -> Result<(), DomainError> { - // Verificar que la carpeta existe + // Verify the folder exists let _folder = self.folder_storage.get_folder(id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?; - // En una implementación real, podríamos verificar permisos, dependencias, etc. + // In a real implementation, we could verify permissions, dependencies, etc. - // Eliminar la carpeta + // Delete the folder self.folder_storage.delete_folder(id) .await .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e))) diff --git a/src/application/services/recent_service.rs b/src/application/services/recent_service.rs index 5fe8d916..62cddc23 100644 --- a/src/application/services/recent_service.rs +++ b/src/application/services/recent_service.rs @@ -5,17 +5,17 @@ use crate::common::errors::{Result, DomainError, ErrorKind}; use crate::application::ports::recent_ports::{RecentItemsUseCase, RecentItemsRepositoryPort}; use crate::application::dtos::recent_dto::RecentItemDto; -/// Implementación del caso de uso para gestionar elementos recientes. +/// Implementation of the use case for managing recent items. /// -/// Depende de `RecentItemsRepositoryPort` (outbound port) en lugar -/// de acceder directamente a `PgPool`, siguiendo la arquitectura hexagonal. +/// Depends on `RecentItemsRepositoryPort` (outbound port) instead +/// of accessing `PgPool` directly, following the hexagonal architecture. pub struct RecentService { repo: Arc, max_recent_items: i32, } impl RecentService { - /// Crear un nuevo servicio de elementos recientes + /// Create a new recent items service pub fn new(repo: Arc, max_recent_items: i32) -> Self { Self { repo, @@ -26,51 +26,51 @@ impl RecentService { #[async_trait] impl RecentItemsUseCase for RecentService { - /// Obtener elementos recientes de un usuario + /// Get recent items for a user async fn get_recent_items(&self, user_id: &str, limit: Option) -> Result> { - info!("Obteniendo elementos recientes para usuario: {}", user_id); + info!("Getting recent items for user: {}", user_id); let limit_value = limit.unwrap_or(self.max_recent_items).min(self.max_recent_items); let items = self.repo.get_recent_items(user_id, limit_value).await?; - info!("Recuperados {} elementos recientes para usuario {}", items.len(), user_id); + info!("Retrieved {} recent items for user {}", items.len(), user_id); Ok(items) } - /// Registrar acceso a un elemento + /// Record access to an item async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> { - info!("Registrando acceso a {} '{}' para usuario {}", item_type, item_id, user_id); + info!("Recording access to {} '{}' for user {}", item_type, item_id, user_id); if item_type != "file" && item_type != "folder" { return Err(DomainError::new( ErrorKind::InvalidInput, "RecentItems", - "El tipo de elemento debe ser 'file' o 'folder'", + "Item type must be 'file' or 'folder'", )); } self.repo.upsert_access(user_id, item_id, item_type).await?; self.repo.prune(user_id, self.max_recent_items).await?; - info!("Registrado correctamente acceso a {} '{}' para usuario {}", item_type, item_id, user_id); + info!("Successfully recorded access to {} '{}' for user {}", item_type, item_id, user_id); Ok(()) } - /// Eliminar un elemento de recientes + /// Remove an item from recent async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result { - info!("Eliminando {} '{}' de recientes para usuario {}", item_type, item_id, user_id); + info!("Removing {} '{}' from recent for user {}", item_type, item_id, user_id); let removed = self.repo.remove_item(user_id, item_id, item_type).await?; info!( "{} {} '{}' de recientes para usuario {}", - if removed { "Eliminado correctamente" } else { "No se encontró" }, + if removed { "Successfully removed" } else { "Not found" }, item_type, item_id, user_id ); Ok(removed) } - /// Limpiar todos los elementos recientes + /// Clear all recent items async fn clear_recent_items(&self, user_id: &str) -> Result<()> { - info!("Limpiando todos los elementos recientes para usuario {}", user_id); + info!("Clearing all recent items for user {}", user_id); self.repo.clear_all(user_id).await?; - info!("Limpiados todos los elementos recientes para usuario {}", user_id); + info!("Cleared all recent items for user {}", user_id); Ok(()) } } \ No newline at end of file diff --git a/src/application/services/search_service.rs b/src/application/services/search_service.rs index f0e01e66..5c639fd7 100644 --- a/src/application/services/search_service.rs +++ b/src/application/services/search_service.rs @@ -14,57 +14,57 @@ use crate::application::ports::outbound::FolderStoragePort; use crate::application::ports::storage_ports::FileReadPort; /** - * Implementación del servicio de búsqueda para archivos y carpetas. + * Search service implementation for files and folders. * - * Este servicio implementa la funcionalidad de búsqueda avanzada que permite - * a los usuarios encontrar archivos y carpetas basados en diversos criterios - * como nombre, tipo, fecha y tamaño. También incluye una caché para mejorar - * el rendimiento de búsquedas repetidas. + * This service implements the advanced search functionality that allows + * users to find files and folders based on various criteria + * such as name, type, date and size. It also includes a cache to improve + * the performance of repeated searches. */ pub struct SearchService { - /// Repositorio para operaciones con archivos + /// Repository for file operations file_repository: Arc, - /// Repositorio para operaciones con carpetas + /// Repository for folder operations folder_repository: Arc, - /// Caché de resultados de búsqueda con tiempo de expiración + /// Search results cache with expiration time search_cache: Arc>>, - /// Duración de validez de la caché en segundos + /// Cache validity duration in seconds cache_ttl: u64, - /// Tamaño máximo de la caché (número de resultados almacenados) + /// Maximum cache size (number of stored results) max_cache_size: usize, } -/// Clave para la caché de búsqueda +/// Key for the search cache #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct SearchCacheKey { - /// Representación serializada de los criterios de búsqueda + /// Serialized representation of the search criteria criteria_hash: String, - /// ID del usuario (para aislar búsquedas entre usuarios) + /// User ID (to isolate searches between users) user_id: String, } -/// Resultado de búsqueda en caché con tiempo de expiración +/// Cached search result with expiration time struct CachedSearchResult { - /// Resultados de la búsqueda + /// Search results results: SearchResultsDto, - /// Momento en que se creó la entrada de caché + /// Time when the cache entry was created timestamp: Instant, } impl SearchService { /** - * Crea una nueva instancia del servicio de búsqueda. + * Creates a new instance of the search service. * - * @param file_repository Repositorio para operaciones con archivos - * @param folder_repository Repositorio para operaciones con carpetas - * @param cache_ttl Tiempo de vida de la caché en segundos (0 para desactivar) - * @param max_cache_size Tamaño máximo de la caché + * @param file_repository Repository for file operations + * @param folder_repository Repository for folder operations + * @param cache_ttl Cache time-to-live in seconds (0 to disable) + * @param max_cache_size Maximum cache size */ pub fn new( file_repository: Arc, @@ -80,7 +80,7 @@ impl SearchService { max_cache_size, }; - // Iniciar tarea de limpieza de caché si TTL > 0 + // Start cache cleanup task if TTL > 0 if cache_ttl > 0 { Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl); } @@ -89,10 +89,10 @@ impl SearchService { } /** - * Inicia una tarea asíncrona para limpiar entradas expiradas de la caché. + * Starts an asynchronous task to clean up expired cache entries. * - * @param cache_ref Referencia a la caché compartida - * @param ttl_seconds TTL en segundos + * @param cache_ref Reference to the shared cache + * @param ttl_seconds TTL in seconds */ fn start_cache_cleanup_task( cache_ref: Arc>>, @@ -105,18 +105,18 @@ impl SearchService { loop { time::sleep(cleanup_interval).await; - // Obtener lock y limpiar entradas expiradas + // Acquire lock and clean up expired entries if let Ok(mut cache) = cache_ref.lock() { let now = Instant::now(); - // Identificar entradas expiradas + // Identify expired entries let expired_keys: Vec = cache .iter() .filter(|(_, result)| now.duration_since(result.timestamp) > ttl) .map(|(key, _)| key.clone()) .collect(); - // Eliminar entradas expiradas + // Remove expired entries for key in expired_keys { cache.remove(&key); } @@ -126,14 +126,14 @@ impl SearchService { } /** - * Crea una clave de caché a partir de los criterios de búsqueda. + * Creates a cache key from the search criteria. * - * @param criteria Criterios de búsqueda - * @param user_id ID del usuario (para aislar caché entre usuarios) - * @return Clave para la caché + * @param criteria Search criteria + * @param user_id User ID (to isolate cache between users) + * @return Cache key */ fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey { - // Serializar criterios para generar un hash + // Serialize criteria to generate a hash let criteria_str = serde_json::to_string(criteria).unwrap_or_default(); SearchCacheKey { @@ -143,13 +143,13 @@ impl SearchService { } /** - * Intenta obtener resultados de la caché. + * Attempts to retrieve results from the cache. * - * @param key Clave de caché - * @return Opcionalmente, los resultados si existen y no han expirado + * @param key Cache key + * @return Optionally, the results if they exist and have not expired */ fn get_from_cache(&self, key: &SearchCacheKey) -> Option { - // Si TTL es 0, la caché está desactivada + // If TTL is 0, the cache is disabled if self.cache_ttl == 0 { return None; } @@ -159,7 +159,7 @@ impl SearchService { let now = Instant::now(); let ttl = Duration::from_secs(self.cache_ttl); - // Comprobar si la entrada ha expirado + // Check if the entry has expired if now.duration_since(cached_result.timestamp) < ttl { return Some(cached_result.results.clone()); } @@ -170,19 +170,19 @@ impl SearchService { } /** - * Almacena resultados en la caché. + * Stores results in the cache. * - * @param key Clave de caché - * @param results Resultados a almacenar + * @param key Cache key + * @param results Results to store */ fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) { - // Si TTL es 0, la caché está desactivada + // If TTL is 0, the cache is disabled if self.cache_ttl == 0 { return; } if let Ok(mut cache) = self.search_cache.lock() { - // Si la caché está llena, eliminar la entrada más antigua + // If the cache is full, remove the oldest entry if cache.len() >= self.max_cache_size { if let Some((oldest_key, _)) = cache .iter() @@ -192,7 +192,7 @@ impl SearchService { } } - // Almacenar el nuevo resultado + // Store the new result cache.insert(key, CachedSearchResult { results, timestamp: Instant::now(), @@ -201,35 +201,35 @@ impl SearchService { } /** - * Filtra archivos según los criterios de búsqueda. + * Filters files according to the search criteria. * - * @param files Lista de archivos a filtrar - * @param criteria Criterios de búsqueda - * @return Archivos que cumplen con los criterios + * @param files List of files to filter + * @param criteria Search criteria + * @return Files that match the criteria */ fn filter_files(&self, files: Vec, criteria: &SearchCriteriaDto) -> Vec { files.into_iter() .filter(|file| { - // Filtrar por nombre + // Filter by name if let Some(name_query) = &criteria.name_contains { if !file.name.to_lowercase().contains(&name_query.to_lowercase()) { return false; } } - // Filtrar por tipo de archivo (extensión) + // Filter by file type (extension) if let Some(file_types) = &criteria.file_types { if let Some(extension) = file.name.split('.').last() { if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) { return false; } } else { - // No tiene extensión + // Has no extension return false; } } - // Filtrar por fecha de creación + // Filter by creation date if let Some(created_after) = criteria.created_after { if file.created_at < created_after { return false; @@ -242,7 +242,7 @@ impl SearchService { } } - // Filtrar por fecha de modificación + // Filter by modification date if let Some(modified_after) = criteria.modified_after { if file.modified_at < modified_after { return false; @@ -255,7 +255,7 @@ impl SearchService { } } - // Filtrar por tamaño + // Filter by size if let Some(min_size) = criteria.min_size { if file.size < min_size { return false; @@ -274,23 +274,23 @@ impl SearchService { } /** - * Filtra carpetas según los criterios de búsqueda. + * Filters folders according to the search criteria. * - * @param folders Lista de carpetas a filtrar - * @param criteria Criterios de búsqueda - * @return Carpetas que cumplen con los criterios + * @param folders List of folders to filter + * @param criteria Search criteria + * @return Folders that match the criteria */ fn filter_folders(&self, folders: Vec, criteria: &SearchCriteriaDto) -> Vec { folders.into_iter() .filter(|folder| { - // Filtrar por nombre + // Filter by name if let Some(name_query) = &criteria.name_contains { if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) { return false; } } - // Filtrar por fecha de creación + // Filter by creation date if let Some(created_after) = criteria.created_after { if folder.created_at < created_after { return false; @@ -303,7 +303,7 @@ impl SearchService { } } - // Filtrar por fecha de modificación + // Filter by modification date if let Some(modified_after) = criteria.modified_after { if folder.modified_at < modified_after { return false; @@ -322,12 +322,12 @@ impl SearchService { } /** - * Implementación de la búsqueda recursiva a través de carpetas. + * Implementation of recursive search through folders. * - * @param current_folder_id ID de la carpeta actual - * @param criteria Criterios de búsqueda - * @param found_files Archivos encontrados hasta ahora - * @param found_folders Carpetas encontradas hasta ahora + * @param current_folder_id ID of the current folder + * @param criteria Search criteria + * @param found_files Files found so far + * @param found_folders Folders found so far */ async fn search_recursive( &self, @@ -337,31 +337,31 @@ impl SearchService { found_folders: &mut Vec, ) -> Result<()> { Box::pin(async move { - // Listar archivos en la carpeta actual + // List files in the current folder let files = self.file_repository.list_files(current_folder_id).await?; - // Filtrar archivos según criterios y agregarlos a los resultados + // Filter files according to criteria and add them to the results let filtered_files = self.filter_files( files.into_iter().map(FileDto::from).collect(), criteria ); found_files.extend(filtered_files); - // Si la búsqueda es recursiva, procesar subcarpetas + // If the search is recursive, process subfolders if criteria.recursive { - // Listar subcarpetas + // List subfolders let folders = self.folder_repository.list_folders(current_folder_id).await?; - // Filtrar carpetas según criterios y agregarlas a los resultados + // Filter folders according to criteria and add them to the results let filtered_folders: Vec = self.filter_folders( folders.into_iter().map(FolderDto::from).collect(), criteria ); - // Añadir las carpetas filtradas a los resultados + // Add filtered folders to the results found_folders.extend(filtered_folders.iter().cloned()); - // Buscar recursivamente en cada subcarpeta + // Search recursively in each subfolder for folder in filtered_folders { self.search_recursive( Some(&folder.id), @@ -380,26 +380,26 @@ impl SearchService { #[async_trait] impl SearchUseCase for SearchService { /** - * Realiza una búsqueda basada en los criterios especificados. + * Performs a search based on the specified criteria. * - * @param criteria Criterios de búsqueda - * @return Resultados de la búsqueda + * @param criteria Search criteria + * @return Search results */ async fn search(&self, criteria: SearchCriteriaDto) -> Result { - // TODO: Obtener ID de usuario del contexto de autenticación + // TODO: Get user ID from the authentication context let user_id = "default-user"; let cache_key = self.create_cache_key(&criteria, user_id); - // Intentar obtener resultados de la caché + // Try to get results from the cache if let Some(cached_results) = self.get_from_cache(&cache_key) { return Ok(cached_results); } - // Inicializar colecciones para resultados + // Initialize collections for results let mut found_files: Vec = Vec::new(); let mut found_folders: Vec = Vec::new(); - // Realizar búsqueda en la carpeta especificada o en la raíz + // Perform search in the specified folder or at the root self.search_recursive( criteria.folder_id.as_deref(), &criteria, @@ -407,29 +407,29 @@ impl SearchUseCase for SearchService { &mut found_folders, ).await?; - // Aplicar paginación + // Apply pagination let total_count = found_files.len() + found_folders.len(); - // Ordenar por relevancia o fecha según criterios - // Por defecto, ordenamos por fecha de modificación (más reciente primero) + // Sort by relevance or date according to criteria + // By default, sort by modification date (most recent first) found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); found_folders.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); - // Aplicar límite y offset para paginación + // Apply limit and offset for pagination let start_idx = criteria.offset.min(total_count); let end_idx = (criteria.offset + criteria.limit).min(total_count); let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx) .map(|i| { if i < found_folders.len() { - (true, i) // Es una carpeta + (true, i) // It's a folder } else { - (false, i - found_folders.len()) // Es un archivo + (false, i - found_folders.len()) // It's a file } }) .collect(); - // Extraer elementos paginados + // Extract paginated items let mut paginated_folders = Vec::new(); let mut paginated_files = Vec::new(); @@ -445,7 +445,7 @@ impl SearchUseCase for SearchService { } } - // Crear objeto de resultados + // Create results object let search_results = SearchResultsDto::new( paginated_files, paginated_folders, @@ -454,16 +454,16 @@ impl SearchUseCase for SearchService { Some(total_count), ); - // Almacenar en caché + // Store in cache self.store_in_cache(cache_key, search_results.clone()); Ok(search_results) } /** - * Limpia la caché de resultados de búsqueda. + * Clears the search results cache. * - * @return Resultado indicando éxito + * @return Result indicating success */ async fn clear_search_cache(&self) -> Result<()> { if let Ok(mut cache) = self.search_cache.lock() { @@ -473,9 +473,9 @@ impl SearchUseCase for SearchService { } } -// Implementar el caso de uso de prueba (stub) +// Implement the test use case (stub) impl SearchService { - /// Crea una versión stub del servicio para pruebas + /// Creates a stub version of the service for testing pub fn new_stub() -> impl SearchUseCase { struct SearchServiceStub; diff --git a/src/application/services/share_service.rs b/src/application/services/share_service.rs index 4fd048cf..35a8554f 100644 --- a/src/application/services/share_service.rs +++ b/src/application/services/share_service.rs @@ -80,7 +80,7 @@ impl ShareService { } } - /// Verifica que el elemento a compartir existe + /// Verifies that the item to share exists async fn verify_item_exists( &self, item_id: &str, @@ -89,13 +89,13 @@ impl ShareService { match item_type { ShareItemType::File => { self.file_repository - .get_file(item_id) // Usando el método correcto del trait FileStoragePort + .get_file(item_id) // Using the correct method from the FileStoragePort trait .await .map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?; } ShareItemType::Folder => { self.folder_repository - .get_folder(item_id) // Usando el método correcto del trait FolderStoragePort + .get_folder(item_id) // Using the correct method from the FolderStoragePort trait .await .map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?; } @@ -103,7 +103,7 @@ impl ShareService { Ok(()) } - /// Hash de contraseña usando Argon2id (resistente a timing attacks y GPU attacks) + /// Password hash using Argon2id (resistant to timing attacks and GPU attacks) fn hash_password(&self, password: &str) -> String { use argon2::{Argon2, PasswordHasher}; use argon2::password_hash::SaltString; @@ -126,20 +126,20 @@ impl ShareUseCase for ShareService { user_id: &str, dto: CreateShareDto, ) -> Result { - // Convertir el tipo de elemento + // Convert the item type let item_type = ShareItemType::try_from(dto.item_type.as_str()) .map_err(|e| ShareServiceError::InvalidItemType(e.to_string()))?; - // Verificar que el elemento existe + // Verify that the item exists self.verify_item_exists(&dto.item_id, &item_type).await?; - // Convertir el DTO de permisos si existe + // Convert the permissions DTO if it exists let permissions = dto.permissions.map(|p| p.to_entity()); - // Hash de contraseña si existe + // Hash the password if provided let password_hash = dto.password.map(|p| self.hash_password(&p)); - // Crear la entidad Share + // Create the Share entity let share = Share::new( dto.item_id.clone(), item_type, @@ -150,48 +150,48 @@ impl ShareUseCase for ShareService { ) .map_err(|e| ShareServiceError::Validation(e.to_string()))?; - // Guardar en el repositorio + // Save to the repository let saved_share = self .share_repository .save_share(&share) .await .map_err(|e| ShareServiceError::Repository(e.to_string()))?; - // Convertir la entidad a DTO para la respuesta + // Convert the entity to DTO for the response Ok(ShareDto::from_entity(&saved_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) } async fn get_shared_link(&self, id: &str) -> Result { - // Buscar el enlace compartido por su ID + // Find the shared link by its ID let share = self .share_repository .find_share_by_id(id) .await .map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?; - // Verificar si ha expirado + // Check if it has expired if share.is_expired() { return Err(ShareServiceError::Expired.into()); } - // Convertir la entidad a DTO para la respuesta + // Convert the entity to DTO for the response Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) } async fn get_shared_link_by_token(&self, token: &str) -> Result { - // Buscar el enlace compartido por su token + // Find the shared link by its token let share = self .share_repository .find_share_by_token(token) .await .map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; - // Verificar si ha expirado + // Check if it has expired if share.is_expired() { return Err(ShareServiceError::Expired.into()); } - // Convertir la entidad a DTO para la respuesta + // Convert the entity to DTO for the response Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) } @@ -200,17 +200,17 @@ impl ShareUseCase for ShareService { item_id: &str, item_type: &ShareItemType, ) -> Result, DomainError> { - // Buscar todos los enlaces compartidos para el elemento + // Find all shared links for the item let shares = self .share_repository .find_shares_by_item(item_id, item_type) .await .map_err(|e| ShareServiceError::Repository(e.to_string()))?; - // Filtrar los enlaces expirados + // Filter out expired links let active_shares: Vec = shares.into_iter().filter(|s| !s.is_expired()).collect(); - // Convertir las entidades a DTOs para la respuesta + // Convert the entities to DTOs for the response let share_dtos = active_shares .iter() .map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) @@ -224,14 +224,14 @@ impl ShareUseCase for ShareService { id: &str, dto: UpdateShareDto, ) -> Result { - // Buscar el enlace compartido existente + // Find the existing shared link let mut share = self .share_repository .find_share_by_id(id) .await .map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?; - // Actualizar permisos si se proporcionan + // Update permissions if provided if let Some(permissions_dto) = dto.permissions { let permissions = SharePermissions::new( permissions_dto.read, @@ -241,7 +241,7 @@ impl ShareUseCase for ShareService { share = share.with_permissions(permissions); } - // Actualizar contraseña si se proporciona + // Update password if provided if let Some(password) = dto.password { let password_hash = if password.is_empty() { None @@ -251,24 +251,24 @@ impl ShareUseCase for ShareService { share = share.with_password(password_hash); } - // Actualizar fecha de expiración si se proporciona + // Update expiration date if provided if dto.expires_at.is_some() { share = share.with_expiration(dto.expires_at); } - // Guardar los cambios + // Save the changes let updated_share = self .share_repository .update_share(&share) .await .map_err(|e| ShareServiceError::Repository(e.to_string()))?; - // Convertir la entidad a DTO para la respuesta + // Convert the entity to DTO for the response Ok(ShareDto::from_entity(&updated_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) } async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> { - // Eliminar el enlace compartido + // Delete the shared link self.share_repository .delete_share(id) .await @@ -283,23 +283,23 @@ impl ShareUseCase for ShareService { page: usize, per_page: usize, ) -> Result, DomainError> { - // Calcular offset para paginación + // Calculate offset for pagination let offset = (page - 1) * per_page; - // Buscar los enlaces compartidos del usuario + // Find the user's shared links let (shares, total) = self .share_repository .find_shares_by_user(user_id, offset, per_page) .await .map_err(|e| ShareServiceError::Repository(e.to_string()))?; - // Convertir las entidades a DTOs + // Convert the entities to DTOs let share_dtos: Vec = shares .iter() .map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) .collect(); - // Crear el resultado paginado + // Create the paginated result let paginated = PaginatedResponseDto::new( share_dtos, page, @@ -315,19 +315,19 @@ impl ShareUseCase for ShareService { token: &str, password: &str, ) -> Result { - // Buscar el enlace compartido por su token + // Find the shared link by its token let share = self .share_repository .find_share_by_token(token) .await .map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; - // Verificar si ha expirado + // Check if it has expired if share.is_expired() { return Err(ShareServiceError::Expired.into()); } - // Verificar la contraseña usando el port de infraestructura + // Verify the password using the infrastructure port match share.password_hash() { Some(hash) => { self.password_hasher.verify_password(password, hash) @@ -337,22 +337,22 @@ impl ShareUseCase for ShareService { } async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError> { - // Buscar el enlace compartido por su token + // Find the shared link by its token let share = self .share_repository .find_share_by_token(token) .await .map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; - // Verificar si ha expirado + // Check if it has expired if share.is_expired() { return Err(ShareServiceError::Expired.into()); } - // Incrementar el contador de accesos + // Increment the access counter let updated_share = share.increment_access_count(); - // Guardar los cambios + // Save the changes self.share_repository .update_share(&updated_share) .await diff --git a/src/application/services/storage_mediator.rs b/src/application/services/storage_mediator.rs index 7dfb31c0..34751c94 100644 --- a/src/application/services/storage_mediator.rs +++ b/src/application/services/storage_mediator.rs @@ -7,69 +7,69 @@ use crate::domain::entities::folder::Folder; use crate::domain::services::path_service::StoragePath; use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort}; -/// Errores específicos del mediador de almacenamiento +/// Storage mediator specific errors #[derive(Debug, Error)] pub enum StorageMediatorError { - #[error("Entidad no encontrada: {0}")] + #[error("Entity not found: {0}")] NotFound(String), - #[error("Entidad ya existe: {0}")] + #[error("Entity already exists: {0}")] AlreadyExists(String), - #[error("Ruta inválida: {0}")] + #[error("Invalid path: {0}")] InvalidPath(String), - #[error("Error de acceso: {0}")] + #[error("Access error: {0}")] AccessError(String), - #[error("Error interno: {0}")] + #[error("Internal error: {0}")] InternalError(String), - #[error("Error de dominio: {0}")] + #[error("Domain error: {0}")] DomainError(#[from] crate::common::errors::DomainError), } -/// Tipo de resultado para las operaciones del mediador +/// Result type for mediator operations pub type StorageMediatorResult = Result; -/// Interfaz del servicio mediador entre repositorios de archivos y carpetas +/// Interface for the mediator service between file and folder repositories #[async_trait] pub trait StorageMediator: Send + Sync + 'static { - /// Obtiene la ruta de una carpeta por su ID + /// Gets the path of a folder by its ID async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult; - /// Obtiene la ruta de dominio de una carpeta por su ID + /// Gets the domain path of a folder by its ID async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult; - /// Obtiene todos los detalles de una carpeta por su ID + /// Gets all details of a folder by its ID async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult; - /// Verifica si existe un archivo en una ruta específica + /// Checks if a file exists at a specific path async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult; - /// Verifica si existe un archivo en una ruta de dominio específica + /// Checks if a file exists at a specific domain path async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult; - /// Verifica si existe una carpeta en una ruta específica + /// Checks if a folder exists at a specific path async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult; - /// Verifica si existe una carpeta en una ruta de dominio específica + /// Checks if a folder exists at a specific domain path async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult; - /// Resuelve una ruta relativa a absoluta (legacy) + /// Resolves a relative path to absolute (legacy) fn resolve_path(&self, relative_path: &Path) -> PathBuf; - /// Resuelve una ruta de dominio a una ruta física absoluta + /// Resolves a domain path to an absolute physical path fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf; - /// Crea un directorio si no existe (legacy) + /// Creates a directory if it does not exist (legacy) async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>; - /// Crea un directorio si no existe + /// Creates a directory if it does not exist async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>; } -/// Implementación concreta del mediador de almacenamiento +/// Concrete implementation of the storage mediator pub struct FileSystemStorageMediator { pub folder_storage_port: Arc, pub path_service: Arc, @@ -189,7 +189,7 @@ impl StorageMediator for FileSystemStorageMediator { async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult { let abs_path = self.resolve_path(path); - // Verificar si existe como archivo (no como directorio) + // Check if it exists as a file (not as a directory) let exists = abs_path.exists() && abs_path.is_file(); Ok(exists) @@ -198,7 +198,7 @@ impl StorageMediator for FileSystemStorageMediator { async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult { let abs_path = self.resolve_storage_path(storage_path); - // Verificar si existe como archivo (no como directorio) + // Check if it exists as a file (not as a directory) let exists = abs_path.exists() && abs_path.is_file(); Ok(exists) @@ -207,7 +207,7 @@ impl StorageMediator for FileSystemStorageMediator { async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult { let abs_path = self.resolve_path(path); - // Verificar si existe como directorio + // Check if it exists as a directory let exists = abs_path.exists() && abs_path.is_dir(); Ok(exists) @@ -216,7 +216,7 @@ impl StorageMediator for FileSystemStorageMediator { async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult { let abs_path = self.resolve_storage_path(storage_path); - // Verificar si existe como directorio + // Check if it exists as a directory let exists = abs_path.exists() && abs_path.is_dir(); Ok(exists) @@ -236,13 +236,13 @@ impl StorageMediator for FileSystemStorageMediator { async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> { let abs_path = self.resolve_path(path); - // Crear directorios si no existen + // Create directories if they don't exist if !abs_path.exists() { tokio::fs::create_dir_all(&abs_path).await - .map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?; + .map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?; } else if !abs_path.is_dir() { return Err(StorageMediatorError::InvalidPath( - format!("La ruta existe pero no es un directorio: {}", abs_path.display()) + format!("Path exists but is not a directory: {}", abs_path.display()) )); } @@ -252,13 +252,13 @@ impl StorageMediator for FileSystemStorageMediator { async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> { let abs_path = self.resolve_storage_path(storage_path); - // Crear directorios si no existen + // Create directories if they don't exist if !abs_path.exists() { tokio::fs::create_dir_all(&abs_path).await - .map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?; + .map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?; } else if !abs_path.is_dir() { return Err(StorageMediatorError::InvalidPath( - format!("La ruta existe pero no es un directorio: {}", abs_path.display()) + format!("Path exists but is not a directory: {}", abs_path.display()) )); } diff --git a/src/application/services/trash_service.rs b/src/application/services/trash_service.rs index 33b8bb65..ea52483e 100644 --- a/src/application/services/trash_service.rs +++ b/src/application/services/trash_service.rs @@ -303,17 +303,7 @@ impl TrashUseCase for TrashService { } }; - // Obtener el elemento de la papelera - info!("Retrieving trash item from repository: ID={}", trash_id); - let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await; - - match item_result { - Ok(Some(item)) => { - info!("Found item in trash: ID={}, Type={:?}, OriginalID={}", - trash_id, item.item_type(), item.original_id()); - - // Restore based on type - match item.item_type() { + // Get the trash item TrashedItemType::File => { // Restore the file to its original location let file_id = item.original_id().to_string(); @@ -428,7 +418,7 @@ impl TrashUseCase for TrashService { } }; - // Obtener el elemento de la papelera + // Get the trash item info!("Retrieving trash item from repository: ID={}", trash_id); let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await; @@ -440,7 +430,7 @@ impl TrashUseCase for TrashService { // Permanently delete based on type match item.item_type() { TrashedItemType::File => { - // Eliminar el archivo permanentemente + // Permanently delete the file let file_id = item.original_id().to_string(); info!("Permanently deleting file: {}", file_id); @@ -466,7 +456,7 @@ impl TrashUseCase for TrashService { } }, TrashedItemType::Folder => { - // Eliminar la carpeta permanentemente + // Permanently delete the folder let folder_id = item.original_id().to_string(); info!("Permanently deleting folder: {}", folder_id); diff --git a/src/application/transactions/storage_transaction.rs b/src/application/transactions/storage_transaction.rs index 06782cb0..afd32b41 100644 --- a/src/application/transactions/storage_transaction.rs +++ b/src/application/transactions/storage_transaction.rs @@ -2,22 +2,22 @@ use std::future::Future; use std::pin::Pin; use crate::common::errors::{DomainError, ErrorKind}; -/// Tipo para operaciones y rollbacks asíncronos +/// Type for async operations and rollbacks type TransactionOp = Pin> + Send>>; -/// Transacción para operaciones de almacenamiento -/// Permite definir un conjunto de operaciones y sus rollbacks correspondientes +/// Transaction for storage operations +/// Allows defining a set of operations and their corresponding rollbacks pub struct StorageTransaction { - /// Operaciones a ejecutar + /// Operations to execute operations: Vec TransactionOp + Send>>, - /// Operaciones de rollback para revertir cambios en caso de error + /// Rollback operations to revert changes in case of error rollbacks: Vec TransactionOp + Send>>, - /// Nombre de la transacción para logging + /// Transaction name for logging name: String, } impl StorageTransaction { - /// Crea una nueva transacción + /// Creates a new transaction pub fn new(name: &str) -> Self { Self { operations: Vec::new(), @@ -26,7 +26,7 @@ impl StorageTransaction { } } - /// Añade una operación a la transacción con su correspondiente rollback + /// Adds an operation to the transaction with its corresponding rollback pub fn add_operation(&mut self, operation: F, rollback: R) where F: Future> + Send + 'static, @@ -36,74 +36,74 @@ impl StorageTransaction { self.rollbacks.push(Box::new(move || Box::pin(rollback))); } - /// Añade una operación sin rollback (para limpieza o logging) + /// Adds an operation without rollback (for cleanup or logging) pub fn add_finalizer(&mut self, finalizer: F) where F: Future> + Send + 'static, { - // El rollback es una operación nula + // The rollback is a no-op let noop = async { Ok(()) }; self.operations.push(Box::new(move || Box::pin(finalizer))); self.rollbacks.push(Box::new(move || Box::pin(noop))); } - /// Ejecuta la transacción aplicando todas las operaciones en orden - /// Si alguna falla, ejecuta los rollbacks en orden inverso + /// Executes the transaction by applying all operations in order + /// If any fails, executes rollbacks in reverse order pub async fn commit(mut self) -> Result<(), DomainError> { - tracing::debug!("Iniciando transacción: {}", self.name); + tracing::debug!("Starting transaction: {}", self.name); let mut completed_ops = Vec::new(); - // Extraer operaciones para evitar problemas de propiedad + // Extract operations to avoid ownership issues let operations = std::mem::take(&mut self.operations); let transaction_name = self.name.clone(); - // Ejecutar operaciones + // Execute operations for (i, op) in operations.into_iter().enumerate() { match op().await { Ok(()) => { completed_ops.push(i); - tracing::trace!("Operación {} completada en transacción: {}", i, transaction_name); + tracing::trace!("Operation {} completed in transaction: {}", i, transaction_name); } Err(e) => { - tracing::error!("Error en operación {} de transacción {}: {}", i, transaction_name, e); + tracing::error!("Error in operation {} of transaction {}: {}", i, transaction_name, e); - // Ejecutar rollbacks para las operaciones completadas en orden inverso + // Execute rollbacks for completed operations in reverse order self.rollback(completed_ops).await?; return Err(DomainError::new( ErrorKind::InternalError, "Transaction", - format!("Falló la transacción '{}': {}", transaction_name, e) + format!("Transaction '{}' failed: {}", transaction_name, e) ).with_source(e)); } } } - tracing::debug!("Transacción completada exitosamente: {}", transaction_name); + tracing::debug!("Transaction completed successfully: {}", transaction_name); Ok(()) } - /// Ejecuta rollbacks para las operaciones completadas + /// Executes rollbacks for completed operations async fn rollback(mut self, completed_ops: Vec) -> Result<(), DomainError> { - tracing::warn!("Iniciando rollback para transacción: {}", self.name); + tracing::warn!("Starting rollback for transaction: {}", self.name); let mut rollback_errors = Vec::new(); - // Extraer rollbacks para evitar problemas de propiedad + // Extract rollbacks to avoid ownership issues let mut rollbacks = Vec::new(); std::mem::swap(&mut rollbacks, &mut self.rollbacks); - // Ejecutar rollbacks en orden inverso + // Execute rollbacks in reverse order for i in completed_ops.into_iter().rev() { if i < rollbacks.len() { - // Tomar propiedad del rollback (obtener una referencia mutable) + // Take ownership of the rollback (get a mutable reference) if let Some(rb) = rollbacks.get_mut(i) { - // Intercambiar con una función vacía + // Swap with an empty function let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) }))); if let Err(e) = rollback().await { - tracing::error!("Error en rollback de operación {} en transacción {}: {}", + tracing::error!("Error in rollback of operation {} in transaction {}: {}", i, self.name, e); rollback_errors.push(e); } @@ -111,20 +111,20 @@ impl StorageTransaction { } } - // Si hubo errores en el rollback, reportarlos + // If there were errors during rollback, report them if !rollback_errors.is_empty() { - tracing::error!("Errores durante rollback de transacción {}: {} errores", + tracing::error!("Errors during transaction rollback {}: {} errors", self.name, rollback_errors.len()); return Err(DomainError::new( ErrorKind::InternalError, "Transaction", - format!("Errores durante rollback de transacción '{}': {} errores", + format!("Errors during transaction '{}' rollback: {} errors", self.name, rollback_errors.len()) )); } - tracing::info!("Rollback de transacción completado: {}", self.name); + tracing::info!("Transaction rollback completed: {}", self.name); Ok(()) } } \ No newline at end of file diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index a37643cf..c7b44f33 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -5,10 +5,10 @@ use std::time::Duration; #[tokio::main] async fn main() -> Result<(), Box> { - // Configurar logging + // Configure logging tracing_subscriber::fmt::init(); - // Cargar variables de entorno (primero .env.local, luego .env) + // Load environment variables (.env.local first, then .env) if let Ok(path) = env::var("DOTENV_PATH") { dotenv::from_path(Path::new(&path)).ok(); } else { @@ -16,34 +16,34 @@ async fn main() -> Result<(), Box> { dotenv::dotenv().ok(); } - // Obtener DATABASE_URL desde variables de entorno - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada"); + // Get DATABASE_URL from environment variables + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be configured"); - println!("Conectando a la base de datos..."); + println!("Connecting to the database..."); - // Crear pool de conexiones + // Create connection pool let pool = PgPoolOptions::new() .max_connections(5) .acquire_timeout(Duration::from_secs(10)) .connect(&database_url) .await?; - // Ejecutar migraciones - println!("Ejecutando migraciones..."); + // Run migrations + println!("Running migrations..."); - // Obtenemos el directorio desde una variable de entorno o usamos un valor por defecto + // Get the directory from an environment variable or use a default value let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string()); - println!("Directorio de migraciones: {}", migrations_dir); + println!("Migrations directory: {}", migrations_dir); - // Crear un migrator + // Create a migrator let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir)) .await - .expect("No se pudo crear el migrator"); + .expect("Could not create the migrator"); - // Ejecutar todas las migraciones pendientes + // Run all pending migrations migrator.run(&pool).await?; - println!("Migraciones aplicadas correctamente"); + println!("Migrations applied successfully"); Ok(()) } \ No newline at end of file diff --git a/src/common/config.rs b/src/common/config.rs index b0ccb2ed..b4c562ed 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -2,98 +2,98 @@ use std::time::Duration; use std::path::PathBuf; use std::env; -/// Configuración de caché +/// Cache configuration #[derive(Debug, Clone)] pub struct CacheConfig { - /// TTL para entradas de archivos en caché (ms) + /// TTL for file cache entries (ms) pub file_ttl_ms: u64, - /// TTL para entradas de directorios en caché (ms) + /// TTL for directory cache entries (ms) pub directory_ttl_ms: u64, - /// Máximo número de entradas en caché + /// Maximum number of cache entries 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 + file_ttl_ms: 60_000, // 1 minute + directory_ttl_ms: 120_000, // 2 minutes + max_entries: 10_000, // 10,000 entries } } } -/// Configuración de timeouts para diferentes operaciones +/// Timeout configuration for different operations #[derive(Debug, Clone)] pub struct TimeoutConfig { - /// Timeout para operaciones de archivo (ms) + /// Timeout for file operations (ms) pub file_operation_ms: u64, - /// Timeout para operaciones de directorio (ms) + /// Timeout for directory operations (ms) pub dir_operation_ms: u64, - /// Timeout para adquisición de locks (ms) + /// Timeout for lock acquisition (ms) pub lock_acquisition_ms: u64, - /// Timeout para operaciones de red (ms) + /// Timeout for network operations (ms) pub network_operation_ms: u64, } impl Default for TimeoutConfig { fn default() -> Self { Self { - file_operation_ms: 10000, // 10 segundos - dir_operation_ms: 30000, // 30 segundos - lock_acquisition_ms: 5000, // 5 segundos - network_operation_ms: 15000, // 15 segundos + file_operation_ms: 10000, // 10 seconds + dir_operation_ms: 30000, // 30 seconds + lock_acquisition_ms: 5000, // 5 seconds + network_operation_ms: 15000, // 15 seconds } } } impl TimeoutConfig { - /// Obtiene un Duration para operaciones de archivo + /// Gets a Duration for file operations pub fn file_timeout(&self) -> Duration { Duration::from_millis(self.file_operation_ms) } - /// Obtiene un Duration para operaciones de escritura de archivo + /// Gets a Duration for file write operations pub fn file_write_timeout(&self) -> Duration { Duration::from_millis(self.file_operation_ms) } - /// Obtiene un Duration para operaciones de lectura de archivo + /// Gets a Duration for file read operations pub fn file_read_timeout(&self) -> Duration { Duration::from_millis(self.file_operation_ms) } - /// Obtiene un Duration para operaciones de eliminación de archivo + /// Gets a Duration for file delete operations pub fn file_delete_timeout(&self) -> Duration { Duration::from_millis(self.file_operation_ms) } - /// Obtiene un Duration para operaciones de directorio + /// Gets a Duration for directory operations pub fn dir_timeout(&self) -> Duration { Duration::from_millis(self.dir_operation_ms) } - /// Obtiene un Duration para adquisición de locks + /// Gets a Duration for lock acquisition pub fn lock_timeout(&self) -> Duration { Duration::from_millis(self.lock_acquisition_ms) } - /// Obtiene un Duration para operaciones de red + /// Gets a Duration for network operations pub fn network_timeout(&self) -> Duration { Duration::from_millis(self.network_operation_ms) } } -/// Configuración para manejo de recursos grandes +/// Configuration for large resource handling #[derive(Debug, Clone)] pub struct ResourceConfig { - /// Umbral en MB para considerar un archivo como grande + /// Threshold in MB to consider a file as large pub large_file_threshold_mb: u64, - /// Umbral de entradas para considerar un directorio como grande + /// Entry threshold to consider a directory as large pub large_dir_threshold_entries: usize, - /// Tamaño de chunk para procesamiento de archivos grandes (bytes) + /// Chunk size for large file processing (bytes) pub chunk_size_bytes: usize, - /// Límite de tamaño de archivo para cargar en memoria (MB) + /// File size limit for loading into memory (MB) pub max_in_memory_file_size_mb: u64, } @@ -101,7 +101,7 @@ impl Default for ResourceConfig { fn default() -> Self { Self { large_file_threshold_mb: 100, // 100 MB - large_dir_threshold_entries: 1000, // 1000 entradas + large_dir_threshold_entries: 1000, // 1000 entries chunk_size_bytes: 1024 * 1024, // 1 MB max_in_memory_file_size_mb: 50, // 50 MB } @@ -109,71 +109,71 @@ impl Default for ResourceConfig { } impl ResourceConfig { - /// Convierte un tamaño en bytes a MB + /// Converts a size in bytes to MB pub fn bytes_to_mb(&self, bytes: u64) -> u64 { bytes / (1024 * 1024) } - /// Determina si un archivo es considerado grande + /// Determines if a file is considered large pub fn is_large_file(&self, size_bytes: u64) -> bool { self.bytes_to_mb(size_bytes) >= self.large_file_threshold_mb } - /// Determina si un archivo es suficientemente grande para procesamiento paralelo + /// Determines if a file is large enough for parallel processing pub fn needs_parallel_processing(&self, size_bytes: u64, config: &ConcurrencyConfig) -> bool { self.bytes_to_mb(size_bytes) >= config.min_size_for_parallel_chunks_mb } - /// Determina si un archivo puede cargarse completo en memoria + /// Determines if a file can be fully loaded into memory pub fn can_load_in_memory(&self, size_bytes: u64) -> bool { self.bytes_to_mb(size_bytes) <= self.max_in_memory_file_size_mb } - /// Determina si un directorio es considerado grande + /// Determines if a directory is considered large pub fn is_large_directory(&self, entry_count: usize) -> bool { entry_count >= self.large_dir_threshold_entries } - /// Calcula el número de chunks para procesamiento paralelo + /// Calculates the number of chunks for parallel processing pub fn calculate_optimal_chunks(&self, size_bytes: u64, config: &ConcurrencyConfig) -> usize { - // Si el archivo no es suficientemente grande, retornar 1 + // If the file is not large enough, return 1 if !self.needs_parallel_processing(size_bytes, config) { return 1; } - // Calcular el número de chunks basado en el tamaño + // Calculate the number of chunks based on size let chunk_count = (size_bytes as usize + config.parallel_chunk_size_bytes - 1) / config.parallel_chunk_size_bytes; - // Limitar al máximo de chunks en paralelo + // Limit to the maximum number of parallel chunks chunk_count.min(config.max_parallel_chunks) } - /// Calcula el tamaño óptimo de cada chunk para procesamiento paralelo + /// Calculates the optimal size of each chunk for parallel processing pub fn calculate_chunk_size(&self, file_size: u64, chunk_count: usize) -> usize { if chunk_count <= 1 { return file_size as usize; } - // Distribuir equitativamente el tamaño entre los chunks + // Distribute the size evenly among the chunks ((file_size as usize) + chunk_count - 1) / chunk_count } } -/// Configuración para operaciones concurrentes +/// Configuration for concurrent operations #[derive(Debug, Clone)] pub struct ConcurrencyConfig { - /// Máximo de tareas de archivo concurrentes + /// Maximum concurrent file tasks pub max_concurrent_files: usize, - /// Máximo de tareas de directorio concurrentes + /// Maximum concurrent directory tasks pub max_concurrent_dirs: usize, - /// Máximo de operaciones de IO concurrentes + /// Maximum concurrent IO operations pub max_concurrent_io: usize, - /// Máximo de chunks para procesar en paralelo por archivo + /// Maximum chunks to process in parallel per file pub max_parallel_chunks: usize, - /// Tamaño mínimo de archivo (MB) para aplicar procesamiento paralelo de chunks + /// Minimum file size (MB) to apply parallel chunk processing pub min_size_for_parallel_chunks_mb: u64, - /// Tamaño de chunk para procesamiento paralelo (bytes) + /// Chunk size for parallel processing (bytes) pub parallel_chunk_size_bytes: usize, } @@ -190,16 +190,16 @@ impl Default for ConcurrencyConfig { } } -/// Configuración de almacenamiento +/// Storage configuration #[derive(Debug, Clone)] pub struct StorageConfig { - /// Directorio raíz para el almacenamiento + /// Root directory for storage pub root_dir: String, - /// Tamaño de chunk para procesamiento de archivos + /// Chunk size for file processing pub chunk_size: usize, - /// Umbral para procesamiento paralelo + /// Threshold for parallel processing pub parallel_threshold: usize, - /// Días de retención para archivos en la papelera + /// Retention days for files in the trash pub trash_retention_days: u32, } @@ -209,12 +209,12 @@ impl Default for StorageConfig { root_dir: "storage".to_string(), chunk_size: 1024 * 1024, // 1 MB parallel_threshold: 100 * 1024 * 1024, // 100 MB - trash_retention_days: 30, // 30 días + trash_retention_days: 30, // 30 days } } } -/// Configuración de base de datos +/// Database configuration #[derive(Debug, Clone)] pub struct DatabaseConfig { pub connection_string: String, @@ -239,7 +239,7 @@ impl Default for DatabaseConfig { } } -/// Configuración de autenticación +/// Authentication configuration #[derive(Debug, Clone)] pub struct AuthConfig { pub jwt_secret: String, @@ -256,15 +256,15 @@ impl Default for AuthConfig { // to set OXICLOUD_JWT_SECRET in production. The from_env() method // will validate this and warn/panic if not configured. jwt_secret: String::new(), - access_token_expiry_secs: 3600, // 1 hora - refresh_token_expiry_secs: 2592000, // 30 días + access_token_expiry_secs: 3600, // 1 hour + refresh_token_expiry_secs: 2592000, // 30 days hash_memory_cost: 65536, // 64MB hash_time_cost: 3, } } } -/// Configuración de OpenID Connect (OIDC) +/// OpenID Connect (OIDC) configuration #[derive(Debug, Clone)] pub struct OidcConfig { /// Whether OIDC authentication is enabled @@ -335,7 +335,7 @@ impl OidcConfig { } } -/// Configuración de funcionalidades (feature flags) +/// Feature configuration (feature flags) #[derive(Debug, Clone)] pub struct FeaturesConfig { pub enable_auth: bool, @@ -357,34 +357,34 @@ impl Default for FeaturesConfig { } } -/// Configuración global de la aplicación +/// Global application configuration #[derive(Debug, Clone)] pub struct AppConfig { - /// Ruta del directorio de almacenamiento + /// Storage directory path pub storage_path: PathBuf, - /// Ruta del directorio de archivos estáticos + /// Static files directory path pub static_path: PathBuf, - /// Puerto del servidor + /// Server port pub server_port: u16, - /// Host del servidor + /// Server host pub server_host: String, - /// Configuración de caché + /// Cache configuration pub cache: CacheConfig, - /// Configuración de timeouts + /// Timeout configuration pub timeouts: TimeoutConfig, - /// Configuración de recursos + /// Resource configuration pub resources: ResourceConfig, - /// Configuración de concurrencia + /// Concurrency configuration pub concurrency: ConcurrencyConfig, - /// Configuración de almacenamiento + /// Storage configuration pub storage: StorageConfig, - /// Configuración de base de datos + /// Database configuration pub database: DatabaseConfig, - /// Configuración de autenticación + /// Authentication configuration pub auth: AuthConfig, - /// Configuración de funcionalidades + /// Feature configuration pub features: FeaturesConfig, - /// Configuración OIDC + /// OIDC configuration pub oidc: OidcConfig, } @@ -412,7 +412,7 @@ impl AppConfig { pub fn from_env() -> Self { let mut config = Self::default(); - // Usar variables de entorno para sobrescribir valores por defecto + // Use environment variables to override default values if let Ok(storage_path) = env::var("OXICLOUD_STORAGE_PATH") { config.storage_path = PathBuf::from(storage_path); } @@ -431,7 +431,7 @@ impl AppConfig { config.server_host = server_host; } - // Configuración de Database + // Database configuration if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") { config.database.connection_string = connection_string; } @@ -450,7 +450,7 @@ impl AppConfig { } } - // Configuración Auth + // Auth configuration if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") { config.auth.jwt_secret = jwt_secret; } @@ -582,7 +582,7 @@ impl AppConfig { } } -/// Obtenemos una configuración global por defecto +/// Gets a default global configuration pub fn default_config() -> AppConfig { AppConfig::default() } \ No newline at end of file diff --git a/src/common/di.rs b/src/common/di.rs index c3df2cbf..3c880e87 100644 --- a/src/common/di.rs +++ b/src/common/di.rs @@ -53,10 +53,10 @@ use crate::common::stubs::{ StubSearchUseCase, }; -/// Fábrica para los diferentes componentes de la aplicación +/// Factory for the different application components /// -/// Esta fábrica centraliza la creación de todos los servicios de la aplicación, -/// garantizando el orden correcto de inicialización y resolviendo dependencias circulares. +/// This factory centralizes the creation of all application services, +/// ensuring the correct initialization order and resolving circular dependencies. pub struct AppServiceFactory { storage_path: PathBuf, locales_path: PathBuf, @@ -64,7 +64,7 @@ pub struct AppServiceFactory { } impl AppServiceFactory { - /// Crea una nueva fábrica de servicios + /// Creates a new service factory pub fn new(storage_path: PathBuf, locales_path: PathBuf) -> Self { Self { storage_path, @@ -73,7 +73,7 @@ impl AppServiceFactory { } } - /// Crea una nueva fábrica de servicios con configuración personalizada + /// Creates a new service factory with custom configuration pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self { Self { storage_path, @@ -82,17 +82,17 @@ impl AppServiceFactory { } } - /// Obtiene la configuración + /// Gets the configuration pub fn config(&self) -> &AppConfig { &self.config } - /// Obtiene la ruta de almacenamiento + /// Gets the storage path pub fn storage_path(&self) -> &PathBuf { &self.storage_path } - /// Inicializa los servicios base del sistema + /// Initializes the core system services pub async fn create_core_services(&self) -> Result { // Path service let path_service = Arc::new(PathService::new(self.storage_path.clone())); @@ -105,57 +105,57 @@ impl AppServiceFactory { })); tracing::info!("FileContentCache initialized: max 10MB/file, 512MB total, 10k entries"); - // ID mapping service para carpetas + // ID mapping service for folders let folder_id_mapping_path = self.storage_path.join("folder_ids.json"); let folder_id_mapping_service = Arc::new( IdMappingService::new(folder_id_mapping_path).await? ); - // ID mapping service para archivos + // ID mapping service for files let file_id_mapping_path = self.storage_path.join("file_ids.json"); let file_id_mapping_service = Arc::new( IdMappingService::new(file_id_mapping_path).await? ); - // Optimizer con batch processing y caching + // Optimizer with batch processing and caching let id_mapping_optimizer = Arc::new( IdMappingOptimizer::new(folder_id_mapping_service.clone()) ); - // Iniciar tarea de limpieza del optimizer + // Start optimizer cleanup task IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone()); - // Thumbnail service para generación de miniaturas + // Thumbnail service for thumbnail generation let thumbnail_service = Arc::new( crate::infrastructure::services::thumbnail_service::ThumbnailService::new( &self.storage_path, - 5000, // max 5000 thumbnails en cache - 100 * 1024 * 1024, // max 100MB de cache + 5000, // max 5000 thumbnails in cache + 100 * 1024 * 1024, // max 100MB cache ) ); - // Inicializar directorios de thumbnails + // Initialize thumbnail directories thumbnail_service.initialize().await?; - // Write-behind cache para uploads instantáneos de archivos pequeños + // Write-behind cache for instant uploads of small files let write_behind_cache = crate::infrastructure::services::write_behind_cache::WriteBehindCache::new(); - // Chunked upload service para archivos grandes (>10MB) + // Chunked upload service for large files (>10MB) let chunked_temp_dir = std::path::PathBuf::from(&self.storage_path).join(".uploads"); let chunked_upload_service = Arc::new( crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(chunked_temp_dir) ); - // Image transcoding service para conversión automática a WebP + // Image transcoding service for automatic WebP conversion let image_transcode_service = Arc::new( crate::infrastructure::services::image_transcode_service::ImageTranscodeService::new( &self.storage_path, - 2000, // max 2000 imágenes transcodificadas en cache - 50 * 1024 * 1024, // max 50MB de cache en memoria + 2000, // max 2000 transcoded images in cache + 50 * 1024 * 1024, // max 50MB in-memory cache ) ); image_transcode_service.initialize().await?; - // Deduplication service para eliminar archivos duplicados + // Deduplication service for removing duplicate files let dedup_service = Arc::new( crate::infrastructure::services::dedup_service::DedupService::new(&self.storage_path) ); @@ -189,7 +189,7 @@ impl AppServiceFactory { }) } - /// Inicializa los servicios de repositorio + /// Initializes the repository services pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices { // Storage mediator - uses stub initially, will be replaced after folder repo is ready let storage_mediator_stub: Arc = Arc::new( @@ -216,13 +216,13 @@ impl AppServiceFactory { FileMetadataCache::default_with_config(core.config.clone()) ); - // Iniciar tarea de limpieza de metadata cache + // Start metadata cache cleanup task let cache_clone = metadata_cache.clone(); tokio::spawn(async move { FileMetadataCache::start_cleanup_task(cache_clone).await; }); - // Buffer pool para optimización de memoria + // Buffer pool for memory optimization let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL BufferPool::start_cleaner(buffer_pool.clone()); @@ -232,7 +232,7 @@ impl AppServiceFactory { buffer_pool.clone() )); - // File repositories separados para lectura y escritura + // Separate file repositories for reading and writing let file_read_repository = Arc::new(FileFsReadRepository::new( self.storage_path.clone(), storage_mediator.clone(), @@ -281,19 +281,19 @@ impl AppServiceFactory { } } - /// Inicializa los servicios de aplicación + /// Initializes the application services pub fn create_application_services( &self, core: &CoreServices, repos: &RepositoryServices, trash_service: Option>, ) -> ApplicationServices { - // Servicios principales + // Main services let folder_service = Arc::new(FolderService::new( repos.folder_repository.clone() )); - // Servicios refactorizados con todos los puertos de infraestructura + // Refactored services with all infrastructure ports let file_upload_service = Arc::new(FileUploadService::new_full( repos.file_write_repository.clone(), repos.file_read_repository.clone(), @@ -308,7 +308,7 @@ impl AppServiceFactory { core.image_transcode_service.clone(), )); - // FileManagementService con dedup y trash + // FileManagementService with dedup and trash let file_management_service = Arc::new(FileManagementService::new_full( repos.file_write_repository.clone(), repos.file_read_repository.clone(), @@ -325,7 +325,7 @@ impl AppServiceFactory { repos.i18n_repository.clone() )); - // Search service con caché + // Search service with cache let search_service: Option> = Some(Arc::new(SearchService::new( repos.file_read_repository.clone(), repos.folder_repository.clone(), @@ -336,9 +336,9 @@ impl AppServiceFactory { tracing::info!("Application services initialized"); ApplicationServices { - // Tipos concretos para handlers que los necesitan + // Concrete types for handlers that need them folder_service_concrete: folder_service.clone(), - // Traits para abstracción + // Traits for abstraction folder_service, file_upload_service, file_retrieval_service, @@ -347,13 +347,13 @@ impl AppServiceFactory { i18n_service, trash_service, // Already set via parameter search_service, - share_service: None, // Se configura después con create_share_service - favorites_service: None, // Se configura después con create_favorites_service - recent_service: None, // Se configura después con create_recent_service + share_service: None, // Configured later with create_share_service + favorites_service: None, // Configured later with create_favorites_service + recent_service: None, // Configured later with create_recent_service } } - /// Crea el servicio de papelera + /// Creates the trash service pub async fn create_trash_service( &self, repos: &RepositoryServices, @@ -374,7 +374,7 @@ impl AppServiceFactory { self.config.storage.trash_retention_days, )); - // Inicializar servicio de limpieza + // Initialize cleanup service let cleanup_service = TrashCleanupService::new( service.clone(), trash_repo.clone(), @@ -387,7 +387,7 @@ impl AppServiceFactory { Some(service as Arc) } - /// Crea el servicio de compartición + /// Creates the sharing service pub fn create_share_service( &self, repos: &RepositoryServices, @@ -417,7 +417,7 @@ impl AppServiceFactory { Some(service) } - /// Crea el servicio de favoritos (requiere base de datos) + /// Creates the favorites service (requires database) pub fn create_favorites_service( &self, db_pool: &Arc, @@ -430,7 +430,7 @@ impl AppServiceFactory { service } - /// Crea el servicio de elementos recientes (requiere base de datos) + /// Creates the recent items service (requires database) pub fn create_recent_service( &self, db_pool: &Arc, @@ -446,7 +446,7 @@ impl AppServiceFactory { service } - /// Precarga traducciones + /// Preloads translations pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) { use crate::domain::services::i18n_service::Locale; @@ -468,7 +468,7 @@ impl AppServiceFactory { tracing::info!("Translations preloaded"); } - /// Precarga directorios en caché + /// Preloads directories into cache pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) { tracing::info!("Preloading common directories to warm up cache..."); if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await { @@ -476,7 +476,7 @@ impl AppServiceFactory { } } - /// Crea el servicio de uso de almacenamiento (requiere base de datos) + /// Creates the storage usage service (requires database) pub fn create_storage_usage_service( &self, repos: &RepositoryServices, @@ -495,9 +495,9 @@ impl AppServiceFactory { service } - /// Construye el AppState completo usando todos los servicios de la fábrica. + /// Builds the complete AppState using all factory services. /// - /// Este es el punto de entrada principal que reemplaza toda la lógica manual de `main.rs`. + /// This is the main entry point that replaces all manual logic in `main.rs`. pub async fn build_app_state( &self, db_pool: Option>, @@ -677,7 +677,7 @@ impl AppServiceFactory { } } -/// Contenedor para servicios base +/// Container for core services #[derive(Clone)] pub struct CoreServices { pub path_service: Arc, @@ -695,7 +695,7 @@ pub struct CoreServices { pub config: AppConfig, } -/// Contenedor para servicios de repositorio +/// Container for repository services #[derive(Clone)] pub struct RepositoryServices { pub folder_repository: Arc, @@ -707,12 +707,12 @@ pub struct RepositoryServices { pub trash_repository: Option>, } -/// Contenedor para servicios de aplicación +/// Container for application services #[derive(Clone)] pub struct ApplicationServices { - // Tipos concretos para compatibilidad con handlers existentes + // Concrete types for compatibility with existing handlers pub folder_service_concrete: Arc, - // Traits para abstracción + // Traits for abstraction pub folder_service: Arc, pub file_upload_service: Arc, pub file_retrieval_service: Arc, @@ -726,14 +726,14 @@ pub struct ApplicationServices { pub recent_service: Option>, } -/// Contenedor para servicios de autenticación +/// Container for authentication services #[derive(Clone)] pub struct AuthServices { pub token_service: Arc, pub auth_application_service: Arc, } -/// Estado global de la aplicación para dependency injection +/// Global application state for dependency injection #[derive(Clone)] pub struct AppState { pub core: CoreServices, diff --git a/src/common/errors.rs b/src/common/errors.rs index 62c8f832..f49f1396 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -1,21 +1,21 @@ -//! Errores de la aplicación +//! Application errors //! -//! Este módulo re-exporta los errores del dominio para compatibilidad. -//! Las conversiones de errores de infraestructura (sqlx, serde_json, etc.) -//! se encuentran en infrastructure/adapters/error_adapters.rs, siguiendo -//! los principios de Clean Architecture donde el dominio no debe conocer -//! detalles de infraestructura. +//! This module re-exports domain errors for compatibility. +//! Infrastructure error conversions (sqlx, serde_json, etc.) +//! are located in infrastructure/adapters/error_adapters.rs, following +//! Clean Architecture principles where the domain should not know about +//! infrastructure details. -// Re-exportar errores del dominio para compatibilidad +// Re-export domain errors for compatibility pub use crate::domain::errors::{DomainError, ErrorKind, Result}; -// Re-exportar AppError desde interfaces para compatibilidad hacia atrás -// NOTA: El lugar canónico de AppError es ahora crate::interfaces::errors +// Re-export AppError from interfaces for backward compatibility +// NOTE: The canonical location of AppError is now crate::interfaces::errors -// Las conversiones de errores de infraestructura se han movido a: +// Infrastructure error conversions have been moved to: // crate::infrastructure::adapters::error_adapters // -// Para convertir errores de infraestructura a DomainError, use: -// - El trait IntoDomainError para conversiones explícitas con contexto -// - O maneje los errores en los repositorios/servicios de infraestructura -// usando map_err() con DomainError::internal_error() o métodos similares +// To convert infrastructure errors to DomainError, use: +// - The IntoDomainError trait for explicit conversions with context +// - Or handle errors in infrastructure repositories/services +// using map_err() with DomainError::internal_error() or similar methods diff --git a/src/domain/entities/calendar.rs b/src/domain/entities/calendar.rs index 4b35390f..db6ea40c 100644 --- a/src/domain/entities/calendar.rs +++ b/src/domain/entities/calendar.rs @@ -14,7 +14,7 @@ use chrono::{DateTime, Utc}; use crate::common::errors::{Result, DomainError, ErrorKind}; -// Re-exportar errores de entidad desde el módulo centralizado +// Re-export entity errors from the centralized module pub use super::entity_errors::CalendarError; /** diff --git a/src/domain/entities/calendar_event.rs b/src/domain/entities/calendar_event.rs index 9970d681..4384591a 100644 --- a/src/domain/entities/calendar_event.rs +++ b/src/domain/entities/calendar_event.rs @@ -14,7 +14,7 @@ use chrono::{DateTime, Utc, Duration, TimeZone}; use crate::common::errors::{Result, DomainError, ErrorKind}; -// Re-exportar errores de entidad desde el módulo centralizado +// Re-export entity errors from the centralized module pub use super::entity_errors::CalendarEventError; /** diff --git a/src/domain/entities/entity_errors.rs b/src/domain/entities/entity_errors.rs index 6f3f563c..a4d72620 100644 --- a/src/domain/entities/entity_errors.rs +++ b/src/domain/entities/entity_errors.rs @@ -1,11 +1,11 @@ -//! Errores puros de entidades de dominio +//! Pure domain entity errors //! -//! Este módulo define los errores específicos de las entidades de dominio -//! sin dependencias de frameworks externos, siguiendo los principios de -//! Clean Architecture. +//! This module defines domain entity-specific errors +//! without external framework dependencies, following +//! Clean Architecture principles. //! -//! Los errores implementan manualmente `std::error::Error` y `std::fmt::Display` -//! para mantener el dominio libre de dependencias externas. +//! Errors manually implement `std::error::Error` and `std::fmt::Display` +//! to keep the domain free of external dependencies. use std::error::Error; use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -14,12 +14,12 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; // FILE ERRORS // ============================================================================ -/// Errores que pueden ocurrir durante operaciones con entidades File +/// Errors that can occur during File entity operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum FileError { - /// Ocurre cuando el nombre de archivo contiene caracteres inválidos o está vacío + /// Occurs when the file name contains invalid characters or is empty InvalidFileName(String), - /// Ocurre cuando falla la validación de cualquier atributo de la entidad + /// Occurs when validation of any entity attribute fails ValidationError(String), } @@ -34,19 +34,19 @@ impl Display for FileError { impl Error for FileError {} -/// Alias de tipo para resultados de operaciones con entidades File +/// Type alias for File entity operation results pub type FileResult = Result; // ============================================================================ // FOLDER ERRORS // ============================================================================ -/// Errores que pueden ocurrir durante operaciones con entidades Folder +/// Errors that can occur during Folder entity operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum FolderError { - /// Ocurre cuando el nombre de carpeta contiene caracteres inválidos o está vacío + /// Occurs when the folder name contains invalid characters or is empty InvalidFolderName(String), - /// Ocurre cuando falla la validación de cualquier atributo de la entidad + /// Occurs when validation of any entity attribute fails ValidationError(String), } @@ -61,54 +61,54 @@ impl Display for FolderError { impl Error for FolderError {} -/// Alias de tipo para resultados de operaciones con entidades Folder +/// Type alias for Folder entity operation results pub type FolderResult = Result; // ============================================================================ // USER ERRORS // ============================================================================ -/// Errores que pueden ocurrir durante operaciones con entidades User +/// Errors that can occur during User entity operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum UserError { - /// Nombre de usuario inválido + /// Invalid username InvalidUsername(String), - /// Contraseña inválida + /// Invalid password InvalidPassword(String), - /// Error de validación general + /// General validation error ValidationError(String), - /// Error de autenticación + /// Authentication error AuthenticationError(String), } impl Display for UserError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { - UserError::InvalidUsername(msg) => write!(f, "Username inválido: {}", msg), - UserError::InvalidPassword(msg) => write!(f, "Password inválido: {}", msg), - UserError::ValidationError(msg) => write!(f, "Error en la validación: {}", msg), - UserError::AuthenticationError(msg) => write!(f, "Error en la autenticación: {}", msg), + UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg), + UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg), + UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg), + UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg), } } } impl Error for UserError {} -/// Alias de tipo para resultados de operaciones con entidades User +/// Type alias for User entity operation results pub type UserResult = Result; // ============================================================================ // SHARE ERRORS // ============================================================================ -/// Errores que pueden ocurrir durante operaciones con entidades Share +/// Errors that can occur during Share entity operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum ShareError { - /// Token de compartición inválido + /// Invalid share token InvalidToken(String), - /// Fecha de expiración inválida + /// Invalid expiration date InvalidExpiration(String), - /// Error de validación general + /// General validation error ValidationError(String), } @@ -124,21 +124,21 @@ impl Display for ShareError { impl Error for ShareError {} -/// Alias de tipo para resultados de operaciones con entidades Share +/// Type alias for Share entity operation results pub type ShareResult = Result; // ============================================================================ // CALENDAR ERRORS // ============================================================================ -/// Errores que pueden ocurrir durante operaciones con entidades Calendar +/// Errors that can occur during Calendar entity operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum CalendarError { - /// Nombre de calendario inválido + /// Invalid calendar name InvalidName(String), - /// Código de color inválido + /// Invalid color code InvalidColor(String), - /// ID de propietario inválido + /// Invalid owner ID InvalidOwnerId(String), } @@ -154,23 +154,23 @@ impl Display for CalendarError { impl Error for CalendarError {} -/// Alias de tipo para resultados de operaciones con entidades Calendar +/// Type alias for Calendar entity operation results pub type CalendarResult = Result; // ============================================================================ // CALENDAR EVENT ERRORS // ============================================================================ -/// Errores que pueden ocurrir durante operaciones con entidades CalendarEvent +/// Errors that can occur during CalendarEvent entity operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum CalendarEventError { - /// Resumen/título de evento inválido + /// Invalid event summary/title InvalidSummary(String), - /// Fechas de evento inválidas + /// Invalid event dates InvalidDates(String), - /// Regla de recurrencia inválida + /// Invalid recurrence rule InvalidRecurrence(String), - /// Datos iCalendar inválidos + /// Invalid iCalendar data InvalidICalData(String), } @@ -187,7 +187,7 @@ impl Display for CalendarEventError { impl Error for CalendarEventError {} -/// Alias de tipo para resultados de operaciones con entidades CalendarEvent +/// Type alias for CalendarEvent entity operation results pub type CalendarEventResult = Result; // ============================================================================ @@ -216,10 +216,10 @@ mod tests { #[test] fn test_user_error_display() { let err = UserError::InvalidUsername("".to_string()); - assert_eq!(err.to_string(), "Username inválido: "); + assert_eq!(err.to_string(), "Invalid username: "); let err = UserError::AuthenticationError("invalid credentials".to_string()); - assert_eq!(err.to_string(), "Error en la autenticación: invalid credentials"); + assert_eq!(err.to_string(), "Authentication error: invalid credentials"); } #[test] diff --git a/src/domain/entities/file.rs b/src/domain/entities/file.rs index fe7a3461..172664bf 100644 --- a/src/domain/entities/file.rs +++ b/src/domain/entities/file.rs @@ -1,6 +1,6 @@ use crate::domain::services::path_service::StoragePath; -// Re-exportar errores de entidad desde el módulo centralizado +// Re-export entity errors from the centralized module pub use super::entity_errors::{FileError, FileResult}; /** @@ -366,6 +366,6 @@ mod tests { assert!(renamed.is_ok()); let renamed = renamed.unwrap(); assert_eq!(renamed.name(), "newname.txt"); - assert_eq!(renamed.id(), "123"); // El ID no cambia + assert_eq!(renamed.id(), "123"); // The ID does not change } } \ No newline at end of file diff --git a/src/domain/entities/folder.rs b/src/domain/entities/folder.rs index a94e14ce..a7565528 100644 --- a/src/domain/entities/folder.rs +++ b/src/domain/entities/folder.rs @@ -1,6 +1,6 @@ use crate::domain::services::path_service::StoragePath; -// Re-exportar errores de entidad desde el módulo centralizado +// Re-export entity errors from the centralized module pub use super::entity_errors::{FolderError, FolderResult}; /// Represents a folder entity in the domain diff --git a/src/domain/entities/share.rs b/src/domain/entities/share.rs index 5f8d0077..7d89b4e2 100644 --- a/src/domain/entities/share.rs +++ b/src/domain/entities/share.rs @@ -1,7 +1,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; -// Re-exportar errores de entidad desde el módulo centralizado +// Re-export entity errors from the centralized module pub use super::entity_errors::ShareError; #[derive(Debug, Clone, PartialEq)] diff --git a/src/domain/entities/user.rs b/src/domain/entities/user.rs index 9552dd3d..71f5821c 100644 --- a/src/domain/entities/user.rs +++ b/src/domain/entities/user.rs @@ -1,7 +1,7 @@ use uuid::Uuid; use chrono::{DateTime, Utc}; -// Re-exportar errores de entidad desde el módulo centralizado +// Re-export entity errors from the centralized module pub use super::entity_errors::{UserError, UserResult}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -57,22 +57,22 @@ impl User { role: UserRole, storage_quota_bytes: i64, ) -> UserResult { - // Validaciones + // Validations if username.is_empty() || username.len() < 3 || username.len() > 32 { return Err(UserError::InvalidUsername(format!( - "Username debe tener entre 3 y 32 caracteres" + "Username must be between 3 and 32 characters" ))); } if !email.contains('@') || email.len() < 5 { return Err(UserError::ValidationError(format!( - "Email inválido" + "Invalid email" ))); } if password_hash.is_empty() { return Err(UserError::InvalidPassword(format!( - "Password hash no puede estar vacío" + "Password hash cannot be empty" ))); } @@ -106,12 +106,12 @@ impl User { ) -> UserResult { if username.is_empty() || username.len() < 3 || username.len() > 32 { return Err(UserError::InvalidUsername( - "Username debe tener entre 3 y 32 caracteres".to_string(), + "Username must be between 3 and 32 characters".to_string(), )); } if !email.contains('@') || email.len() < 5 { return Err(UserError::ValidationError( - "Email inválido".to_string(), + "Invalid email".to_string(), )); } let now = Utc::now(); @@ -132,7 +132,7 @@ impl User { }) } - // Crear desde valores existentes (para reconstrucción desde BD) + // Create from existing values (for reconstruction from DB) pub fn from_data( id: String, username: String, @@ -263,26 +263,26 @@ impl User { self.updated_at = Utc::now(); } - // Actualizar uso de almacenamiento + // Update storage usage pub fn update_storage_used(&mut self, storage_used_bytes: i64) { self.storage_used_bytes = storage_used_bytes; self.updated_at = Utc::now(); } - // Registrar login + // Register login pub fn register_login(&mut self) { let now = Utc::now(); self.last_login_at = Some(now); self.updated_at = now; } - // Desactivar usuario + // Deactivate user pub fn deactivate(&mut self) { self.active = false; self.updated_at = Utc::now(); } - // Activar usuario + // Activate user pub fn activate(&mut self) { self.active = true; self.updated_at = Utc::now(); diff --git a/src/domain/errors.rs b/src/domain/errors.rs index 71b6ab9d..186cdbc5 100644 --- a/src/domain/errors.rs +++ b/src/domain/errors.rs @@ -1,35 +1,35 @@ -//! Errores del dominio +//! Domain errors //! -//! Este módulo contiene los tipos de error propios del dominio. -//! DomainError es el error base que se usa en toda la capa de dominio. +//! This module contains domain-specific error types. +//! DomainError is the base error used throughout the domain layer. use std::fmt::{Display, Formatter, Result as FmtResult}; use std::error::Error as StdError; use thiserror::Error; -/// Tipo Result común para el dominio con DomainError como error estándar +/// Common Result type for the domain with DomainError as the standard error pub type Result = std::result::Result; -/// Tipos de errores del dominio +/// Domain error types #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorKind { - /// Entidad no encontrada + /// Entity not found NotFound, - /// Entidad ya existe + /// Entity already exists AlreadyExists, - /// Entrada inválida o validación fallida + /// Invalid input or failed validation InvalidInput, - /// Error de acceso o permisos + /// Access or permissions error AccessDenied, - /// Tiempo de espera agotado + /// Timeout expired Timeout, - /// Error interno del sistema + /// Internal system error InternalError, - /// Funcionalidad no implementada + /// Functionality not implemented NotImplemented, - /// Operación no soportada + /// Unsupported operation UnsupportedOperation, - /// Error de base de datos + /// Database error DatabaseError, } @@ -49,25 +49,25 @@ impl Display for ErrorKind { } } -/// Error base de dominio que proporciona contexto detallado +/// Base domain error that provides detailed context #[derive(Error, Debug)] #[error("{kind}: {message}")] pub struct DomainError { - /// Tipo de error + /// Error type pub kind: ErrorKind, - /// Tipo de entidad afectada (ej: "File", "Folder") + /// Affected entity type (e.g.: "File", "Folder") pub entity_type: &'static str, - /// Identificador de la entidad si está disponible + /// Entity identifier if available pub entity_id: Option, - /// Mensaje descriptivo del error + /// Descriptive error message pub message: String, - /// Error fuente (opcional) + /// Source error (optional) #[source] pub source: Option>, } impl DomainError { - /// Crea un nuevo error de dominio + /// Creates a new domain error pub fn new>( kind: ErrorKind, entity_type: &'static str, @@ -82,7 +82,7 @@ impl DomainError { } } - /// Crea un error de entidad no encontrada + /// Creates an entity not found error pub fn not_found>(entity_type: &'static str, entity_id: S) -> Self { let id = entity_id.into(); Self { @@ -94,7 +94,7 @@ impl DomainError { } } - /// Crea un error de entidad ya existente + /// Creates an entity already exists error pub fn already_exists>(entity_type: &'static str, entity_id: S) -> Self { let id = entity_id.into(); Self { @@ -106,7 +106,7 @@ impl DomainError { } } - /// Crea un error para operaciones no soportadas + /// Creates an error for unsupported operations pub fn operation_not_supported>(entity_type: &'static str, message: S) -> Self { Self::new( ErrorKind::UnsupportedOperation, @@ -115,7 +115,7 @@ impl DomainError { ) } - /// Crea un error de tiempo agotado + /// Creates a timeout error pub fn timeout>(entity_type: &'static str, message: S) -> Self { Self { kind: ErrorKind::Timeout, @@ -126,7 +126,7 @@ impl DomainError { } } - /// Crea un error interno + /// Creates an internal error pub fn internal_error>(entity_type: &'static str, message: S) -> Self { Self { kind: ErrorKind::InternalError, @@ -137,7 +137,7 @@ impl DomainError { } } - /// Crea un error de acceso denegado + /// Creates an access denied error pub fn access_denied>(entity_type: &'static str, message: S) -> Self { Self { kind: ErrorKind::AccessDenied, @@ -159,7 +159,7 @@ impl DomainError { } } - /// Crea un error de base de datos + /// Creates a database error pub fn database_error>(message: S) -> Self { Self { kind: ErrorKind::DatabaseError, @@ -170,7 +170,7 @@ impl DomainError { } } - /// Crea un error de validación + /// Creates a validation error pub fn validation_error>(message: S) -> Self { Self { kind: ErrorKind::InvalidInput, @@ -181,7 +181,7 @@ impl DomainError { } } - /// Crea un error de funcionalidad no implementada + /// Creates a not implemented error pub fn not_implemented>(entity_type: &'static str, message: S) -> Self { Self { kind: ErrorKind::NotImplemented, @@ -192,20 +192,20 @@ impl DomainError { } } - /// Establece el ID de la entidad + /// Sets the entity ID pub fn with_id>(mut self, entity_id: S) -> Self { self.entity_id = Some(entity_id.into()); self } - /// Establece el error fuente + /// Sets the source error pub fn with_source(mut self, source: E) -> Self { self.source = Some(Box::new(source)); self } } -/// Trait para añadir contexto a los errores +/// Trait for adding context to errors pub trait ErrorContext { fn with_context(self, context: F) -> std::result::Result where @@ -245,7 +245,7 @@ impl ErrorContext for std::result: } } -// Implementaciones From para errores estándar (sin dependencias externas de infra) +// From implementations for standard errors (without external infrastructure dependencies) impl From for DomainError { fn from(err: std::io::Error) -> Self { DomainError { diff --git a/src/domain/repositories/file_repository.rs b/src/domain/repositories/file_repository.rs index 6e4508e5..81a00053 100644 --- a/src/domain/repositories/file_repository.rs +++ b/src/domain/repositories/file_repository.rs @@ -1,12 +1,12 @@ -//! Puerto de persistencia del dominio para la entidad File. +//! Domain persistence port for the File entity. //! -//! Define el contrato que cualquier implementación de almacenamiento de archivos -//! debe cumplir. Este trait vive en el dominio porque File es una entidad core -//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio, -//! siguiendo los principios de Clean/Hexagonal Architecture. +//! Defines the contract that any file storage implementation must fulfill. +//! This trait lives in the domain because File is a core entity of the system +//! and its persistence contracts belong to the domain layer, following +//! Clean/Hexagonal Architecture principles. //! -//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en -//! la capa de infraestructura. +//! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in +//! the infrastructure layer. use std::path::PathBuf; use std::pin::Pin; @@ -20,31 +20,31 @@ use crate::domain::services::path_service::StoragePath; use crate::common::errors::DomainError; // ───────────────────────────────────────────────────── -// FileReadRepository — operaciones de lectura/consulta +// FileReadRepository — read/query operations // ───────────────────────────────────────────────────── -/// Puerto del dominio para **lectura** de archivos. +/// Domain port for file **reading**. /// -/// Encapsula toda operación que consulta estado sin modificarlo: -/// obtener, listar, contenido, stream, mmap, rango, resolución de rutas. +/// Encapsulates every operation that queries state without modifying it: +/// get, list, content, stream, mmap, range, path resolution. #[async_trait] pub trait FileReadRepository: Send + Sync + 'static { - /// Obtiene un archivo por su ID. + /// Gets a file by its ID. async fn get_file(&self, id: &str) -> Result; - /// Lista archivos en una carpeta. + /// Lists files in a folder. async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError>; - /// Obtiene contenido completo como bytes (solo archivos pequeños/medianos). + /// Gets full content as bytes (only for small/medium files). async fn get_file_content(&self, id: &str) -> Result, DomainError>; - /// Obtiene contenido como stream (ideal para archivos grandes). + /// Gets content as a stream (ideal for large files). async fn get_file_stream( &self, id: &str, ) -> Result> + Send>, DomainError>; - /// Stream de un rango de bytes (HTTP Range Requests, video seek). + /// Stream of a byte range (HTTP Range Requests, video seek). async fn get_file_range_stream( &self, id: &str, @@ -52,27 +52,27 @@ pub trait FileReadRepository: Send + Sync + 'static { end: Option, ) -> Result> + Send>, DomainError>; - /// Memory-map de archivo para acceso zero-copy (10–100 MB). + /// Memory-mapped file for zero-copy access (10–100 MB). async fn get_file_mmap(&self, id: &str) -> Result; - /// Obtiene la ruta de almacenamiento lógica de un archivo. + /// Gets the logical storage path of a file. async fn get_file_path(&self, id: &str) -> Result; - /// Obtiene el ID de la carpeta padre a partir de una ruta (WebDAV). + /// Gets the parent folder ID from a path (WebDAV). async fn get_parent_folder_id(&self, path: &str) -> Result; } // ───────────────────────────────────────────────────── -// FileWriteRepository — operaciones de escritura/mutación +// FileWriteRepository — write/mutation operations // ───────────────────────────────────────────────────── -/// Puerto del dominio para **escritura** de archivos. +/// Domain port for file **writing**. /// -/// Cubre: upload (buffered + streaming), move, delete, update, -/// y el registro diferido para write-behind cache. +/// Covers: upload (buffered + streaming), move, delete, update, +/// and deferred registration for write-behind cache. #[async_trait] pub trait FileWriteRepository: Send + Sync + 'static { - /// Guarda un nuevo archivo desde bytes. + /// Saves a new file from bytes. async fn save_file( &self, name: String, @@ -81,7 +81,7 @@ pub trait FileWriteRepository: Send + Sync + 'static { content: Vec, ) -> Result; - /// Upload en streaming — escribe chunks a disco sin acumular en RAM. + /// Streaming upload — writes chunks to disk without accumulating in RAM. async fn save_file_from_stream( &self, name: String, @@ -90,30 +90,30 @@ pub trait FileWriteRepository: Send + Sync + 'static { stream: Pin> + Send>>, ) -> Result; - /// Mueve un archivo a otra carpeta. + /// Moves a file to another folder. async fn move_file( &self, file_id: &str, target_folder_id: Option, ) -> Result; - /// Renombra un archivo (same folder, different name). + /// Renames a file (same folder, different name). async fn rename_file( &self, file_id: &str, new_name: &str, ) -> Result; - /// Elimina un archivo. + /// Deletes a file. async fn delete_file(&self, id: &str) -> Result<(), DomainError>; - /// Actualiza el contenido de un archivo existente. + /// Updates the content of an existing file. async fn update_file_content(&self, file_id: &str, content: Vec) -> Result<(), DomainError>; - /// Registra metadatos de archivo SIN escribir contenido a disco (write-behind). + /// Registers file metadata WITHOUT writing content to disk (write-behind). /// - /// Devuelve `(File, PathBuf)` donde `PathBuf` es la ruta destino para la - /// escritura diferida que realizará el `WriteBehindCache`. + /// Returns `(File, PathBuf)` where `PathBuf` is the destination path for + /// the deferred write that the `WriteBehindCache` will perform. async fn register_file_deferred( &self, name: String, @@ -124,27 +124,27 @@ pub trait FileWriteRepository: Send + Sync + 'static { // ── Trash operations ── - /// Mueve un archivo a la papelera + /// Moves a file to the trash async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>; - /// Restaura un archivo desde la papelera a su ubicación original + /// Restores a file from the trash to its original location async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>; - /// Elimina un archivo permanentemente (usado por la papelera) + /// Permanently deletes a file (used by the trash) async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>; } // ───────────────────────────────────────────────────── -// FileRepository — supertrait unificado +// FileRepository — unified supertrait // ───────────────────────────────────────────────────── -/// Puerto unificado para persistencia de archivos. +/// Unified port for file persistence. /// -/// Es un supertrait de `FileReadRepository + FileWriteRepository`. -/// Cualquier tipo que implemente ambos ports obtiene `FileRepository` -/// automáticamente vía blanket impl. +/// It is a supertrait of `FileReadRepository + FileWriteRepository`. +/// Any type that implements both ports gets `FileRepository` +/// automatically via blanket impl. pub trait FileRepository: FileReadRepository + FileWriteRepository {} -/// Blanket implementation: cualquier tipo que implemente ambos ports -/// es automáticamente un FileRepository. +/// Blanket implementation: any type that implements both ports +/// is automatically a FileRepository. impl FileRepository for T {} diff --git a/src/domain/repositories/folder_repository.rs b/src/domain/repositories/folder_repository.rs index 3eb83610..420faea2 100644 --- a/src/domain/repositories/folder_repository.rs +++ b/src/domain/repositories/folder_repository.rs @@ -1,12 +1,12 @@ -//! Puerto de persistencia del dominio para la entidad Folder. +//! Domain persistence port for the Folder entity. //! -//! Define el contrato que cualquier implementación de almacenamiento de carpetas -//! debe cumplir. Este trait vive en el dominio porque Folder es una entidad core -//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio, -//! siguiendo los principios de Clean/Hexagonal Architecture. +//! Defines the contract that any folder storage implementation +//! must fulfill. This trait lives in the domain because Folder is a core entity +//! of the system and its persistence contracts belong to the domain layer, +//! following the principles of Clean/Hexagonal Architecture. //! -//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en -//! la capa de infraestructura. +//! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in +//! the infrastructure layer. use async_trait::async_trait; @@ -14,25 +14,25 @@ use crate::domain::entities::folder::Folder; use crate::domain::services::path_service::StoragePath; use crate::common::errors::DomainError; -/// Puerto del dominio para persistencia de carpetas. +/// Domain port for folder persistence. /// -/// Define las operaciones CRUD y de gestión necesarias para -/// la entidad Folder en el sistema de almacenamiento. +/// Defines the CRUD and management operations required for +/// the Folder entity in the storage system. #[async_trait] pub trait FolderRepository: Send + Sync + 'static { - /// Crea una nueva carpeta + /// Creates a new folder async fn create_folder(&self, name: String, parent_id: Option) -> Result; - /// Obtiene una carpeta por su ID + /// Gets a folder by its ID async fn get_folder(&self, id: &str) -> Result; - /// Obtiene una carpeta por su ruta de almacenamiento + /// Gets a folder by its storage path async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result; - /// Lista carpetas dentro de una carpeta padre + /// Lists folders within a parent folder async fn list_folders(&self, parent_id: Option<&str>) -> Result, DomainError>; - /// Lista carpetas con paginación + /// Lists folders with pagination async fn list_folders_paginated( &self, parent_id: Option<&str>, @@ -41,29 +41,29 @@ pub trait FolderRepository: Send + Sync + 'static { include_total: bool ) -> Result<(Vec, Option), DomainError>; - /// Renombra una carpeta + /// Renames a folder async fn rename_folder(&self, id: &str, new_name: String) -> Result; - /// Mueve una carpeta a otro padre + /// Moves a folder to another parent async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result; - /// Elimina una carpeta + /// Deletes a folder async fn delete_folder(&self, id: &str) -> Result<(), DomainError>; - /// Verifica si existe una carpeta en la ruta dada + /// Checks if a folder exists at the given path async fn folder_exists(&self, storage_path: &StoragePath) -> Result; - /// Obtiene la ruta de una carpeta + /// Gets the path of a folder async fn get_folder_path(&self, id: &str) -> Result; // ── Trash operations ── - /// Mueve una carpeta a la papelera + /// Moves a folder to the trash async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>; - /// Restaura una carpeta desde la papelera a su ubicación original + /// Restores a folder from the trash to its original location async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError>; - /// Elimina una carpeta permanentemente (usado por la papelera) + /// Permanently deletes a folder (used by the trash) async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>; } diff --git a/src/domain/repositories/session_repository.rs b/src/domain/repositories/session_repository.rs index bf913b8f..0f87ab5d 100644 --- a/src/domain/repositories/session_repository.rs +++ b/src/domain/repositories/session_repository.rs @@ -4,19 +4,19 @@ use crate::common::errors::DomainError; #[derive(Debug, thiserror::Error)] pub enum SessionRepositoryError { - #[error("Sesión no encontrada: {0}")] + #[error("Session not found: {0}")] NotFound(String), - #[error("Error de base de datos: {0}")] + #[error("Database error: {0}")] DatabaseError(String), - #[error("Error de tiempo de espera: {0}")] + #[error("Timeout error: {0}")] Timeout(String), } pub type SessionRepositoryResult = Result; -// Conversión de SessionRepositoryError a DomainError +// Conversion from SessionRepositoryError to DomainError impl From for DomainError { fn from(err: SessionRepositoryError) -> Self { match err { @@ -35,24 +35,24 @@ impl From for DomainError { #[async_trait] pub trait SessionRepository: Send + Sync + 'static { - /// Crea una nueva sesión + /// Creates a new session async fn create_session(&self, session: Session) -> SessionRepositoryResult; - /// Obtiene una sesión por ID + /// Gets a session by ID async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult; - /// Obtiene una sesión por token de actualización + /// Gets a session by refresh token async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult; - /// Obtiene todas las sesiones de un usuario + /// Gets all sessions for a user async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult>; - /// Revoca una sesión específica + /// Revokes a specific session async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>; - /// Revoca todas las sesiones de un usuario + /// Revokes all sessions for a user async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult; - /// Elimina sesiones expiradas + /// Deletes expired sessions async fn delete_expired_sessions(&self) -> SessionRepositoryResult; } \ No newline at end of file diff --git a/src/domain/repositories/user_repository.rs b/src/domain/repositories/user_repository.rs index d6b0b6c3..760375fa 100644 --- a/src/domain/repositories/user_repository.rs +++ b/src/domain/repositories/user_repository.rs @@ -4,28 +4,28 @@ use crate::common::errors::DomainError; #[derive(Debug, thiserror::Error)] pub enum UserRepositoryError { - #[error("Usuario no encontrado: {0}")] + #[error("User not found: {0}")] NotFound(String), - #[error("Usuario ya existe: {0}")] + #[error("User already exists: {0}")] AlreadyExists(String), - #[error("Error de base de datos: {0}")] + #[error("Database error: {0}")] DatabaseError(String), - #[error("Error de validación: {0}")] + #[error("Validation error: {0}")] ValidationError(String), - #[error("Error de tiempo de espera: {0}")] + #[error("Timeout error: {0}")] Timeout(String), - #[error("Operación no permitida: {0}")] + #[error("Operation not allowed: {0}")] OperationNotAllowed(String), } pub type UserRepositoryResult = Result; -// Conversión de UserRepositoryError a DomainError +// Conversion from UserRepositoryError to DomainError impl From for DomainError { fn from(err: UserRepositoryError) -> Self { match err { @@ -53,59 +53,59 @@ impl From for DomainError { #[async_trait] pub trait UserRepository: Send + Sync + 'static { - /// Crea un nuevo usuario + /// Creates a new user async fn create_user(&self, user: User) -> UserRepositoryResult; - /// Obtiene un usuario por ID + /// Gets a user by ID async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult; - /// Obtiene un usuario por nombre de usuario + /// Gets a user by username async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult; - /// Obtiene un usuario por correo electrónico + /// Gets a user by email async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult; - /// Actualiza un usuario existente + /// Updates an existing user async fn update_user(&self, user: User) -> UserRepositoryResult; - /// Actualiza solo el uso de almacenamiento de un usuario + /// Updates only a user's storage usage async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()>; - /// Actualiza la fecha de último inicio de sesión + /// Updates the last login date async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>; - /// Lista usuarios con paginación + /// Lists users with pagination async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult>; - /// Activa o desactiva un usuario + /// Activates or deactivates a user async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()>; - /// Cambia la contraseña de un usuario + /// Changes a user's password async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()>; - /// Cambia el rol de un usuario + /// Changes a user's role async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>; - /// Lista usuarios por rol (admin o user) + /// Lists users by role (admin or user) async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult>; - /// Elimina un usuario + /// Deletes a user async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>; /// Finds a user by OIDC provider + subject pair async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult; - /// Actualiza la cuota de almacenamiento de un usuario + /// Updates a user's storage quota async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()>; - /// Cuenta el número total de usuarios + /// Counts the total number of users async fn count_users(&self) -> UserRepositoryResult; - /// Obtiene estadísticas de almacenamiento agregadas + /// Gets aggregated storage statistics async fn get_storage_stats(&self) -> UserRepositoryResult; } -/// Estadísticas de almacenamiento agregadas +/// Aggregated storage statistics #[derive(Debug, Clone)] pub struct StorageStats { pub total_users: i64, diff --git a/src/domain/services/path_service.rs b/src/domain/services/path_service.rs index 0dd74534..c6b89b17 100644 --- a/src/domain/services/path_service.rs +++ b/src/domain/services/path_service.rs @@ -1,29 +1,29 @@ -//! StoragePath - Value Object del dominio para representar rutas de almacenamiento +//! StoragePath - Domain Value Object for representing storage paths //! -//! Este módulo contiene solo el Value Object StoragePath que es parte del dominio puro. -//! PathService (que implementa StoragePort y StorageMediator) fue movido a -//! infrastructure/services/path_service.rs porque tiene dependencias de sistema de archivos. +//! This module contains only the StoragePath Value Object which is part of the pure domain. +//! PathService (which implements StoragePort and StorageMediator) was moved to +//! infrastructure/services/path_service.rs because it has file system dependencies. use std::path::PathBuf; -/// Representa una ruta de almacenamiento en el dominio (Value Object) +/// Represents a storage path in the domain (Value Object) #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct StoragePath { segments: Vec, } impl StoragePath { - /// Crea una nueva ruta de almacenamiento + /// Creates a new storage path pub fn new(segments: Vec) -> Self { Self { segments } } - /// Crea una ruta vacía (raíz) + /// Creates an empty path (root) pub fn root() -> Self { Self { segments: Vec::new() } } - /// Crea una ruta a partir de una cadena con segmentos separados por / + /// Creates a path from a string with segments separated by / pub fn from_string(path: &str) -> Self { let segments = path .split('/') @@ -33,7 +33,7 @@ impl StoragePath { Self { segments } } - /// Crea una ruta a partir de un PathBuf + /// Creates a path from a PathBuf pub fn from(path_buf: PathBuf) -> Self { let segments = path_buf .components() @@ -45,19 +45,19 @@ impl StoragePath { Self { segments } } - /// Añade un segmento a la ruta + /// Appends a segment to the path pub fn join(&self, segment: &str) -> Self { let mut new_segments = self.segments.clone(); new_segments.push(segment.to_string()); Self { segments: new_segments } } - /// Obtiene el nombre del archivo (último segmento) + /// Gets the file name (last segment) pub fn file_name(&self) -> Option { self.segments.last().cloned() } - /// Obtiene la ruta del directorio padre + /// Gets the parent directory path pub fn parent(&self) -> Option { if self.segments.is_empty() { None @@ -67,12 +67,12 @@ impl StoragePath { } } - /// Verifica si la ruta está vacía (es la raíz) + /// Checks if the path is empty (is the root) pub fn is_empty(&self) -> bool { self.segments.is_empty() } - /// Convierte la ruta a una cadena con formato "/segment1/segment2/..." + /// Converts the path to a string with format "/segment1/segment2/..." pub fn to_string(&self) -> String { if self.segments.is_empty() { "/".to_string() @@ -81,15 +81,15 @@ impl StoragePath { } } - /// Devuelve la representación de la ruta como cadena + /// Returns the path representation as a string pub fn as_str(&self) -> &str { - // Nota: La implementación realmente debería almacenar la cadena, - // pero aquí hacemos una implementación temporal que siempre devuelve "/" - // Esto se usa solo para la implementación de get_folder_path_str + // Note: The implementation should really store the string, + // but here we do a temporary implementation that always returns "/" + // This is only used for the get_folder_path_str implementation "/" } - /// Obtiene los segmentos de la ruta + /// Gets the path segments pub fn segments(&self) -> &[String] { &self.segments } diff --git a/src/infrastructure/auth_factory.rs b/src/infrastructure/auth_factory.rs index a8ce0f87..cf260858 100644 --- a/src/infrastructure/auth_factory.rs +++ b/src/infrastructure/auth_factory.rs @@ -17,21 +17,21 @@ pub async fn create_auth_services( pool: Arc, folder_service: Option> ) -> Result { - // Crear servicio de tokens JWT (implementación de TokenServicePort) + // Create JWT token service (TokenServicePort implementation) let token_service: Arc = Arc::new(JwtTokenService::new( config.auth.jwt_secret.clone(), config.auth.access_token_expiry_secs, config.auth.refresh_token_expiry_secs, )); - // Crear servicio de hashing de contraseñas + // Create password hashing service let password_hasher = Arc::new(Argon2PasswordHasher::new()); - // Crear repositorios PostgreSQL + // Create PostgreSQL repositories let user_repository = Arc::new(UserPgRepository::new(pool.clone())); let session_repository = Arc::new(SessionPgRepository::new(pool.clone())); - // Crear servicio de aplicación de autenticación + // Create authentication application service let mut auth_app_service = AuthApplicationService::new( user_repository, session_repository, @@ -39,7 +39,7 @@ pub async fn create_auth_services( token_service.clone(), ); - // Configurar servicio de carpetas si está disponible + // Configure folder service if available if let Some(folder_svc) = folder_service { auth_app_service = auth_app_service.with_folder_service(folder_svc); } @@ -57,7 +57,7 @@ pub async fn create_auth_services( } } - // Empaquetar servicio en Arc + // Package service in Arc let auth_application_service = Arc::new(auth_app_service); Ok(AuthServices { diff --git a/src/infrastructure/db.rs b/src/infrastructure/db.rs index 3c7ad75d..26611954 100644 --- a/src/infrastructure/db.rs +++ b/src/infrastructure/db.rs @@ -4,7 +4,7 @@ use std::time::Duration; use crate::common::config::AppConfig; pub async fn create_database_pool(config: &AppConfig) -> Result { - tracing::info!("Inicializando conexión a PostgreSQL con URL: {}", + tracing::info!("Initializing PostgreSQL connection with URL: {}", config.database.connection_string.replace("postgres://", "postgres://[user]:[pass]@")); // Add a more robust connection attempt with retries @@ -13,9 +13,9 @@ pub async fn create_database_pool(config: &AppConfig) -> Result { while attempt < MAX_ATTEMPTS { attempt += 1; - tracing::info!("Intento de conexión a PostgreSQL #{}", attempt); + tracing::info!("PostgreSQL connection attempt #{}", attempt); - // Crear el pool de conexiones con las opciones de configuración + // Create the connection pool with configuration options match PgPoolOptions::new() .max_connections(config.database.max_connections) .min_connections(config.database.min_connections) @@ -25,10 +25,10 @@ pub async fn create_database_pool(config: &AppConfig) -> Result { .connect(&config.database.connection_string) .await { Ok(pool) => { - // Verificar la conexión + // Verify the connection match sqlx::query("SELECT 1").execute(&pool).await { Ok(_) => { - tracing::info!("Conexión a PostgreSQL establecida correctamente"); + tracing::info!("PostgreSQL connection established successfully"); // Verify if migrations have been applied let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')") @@ -39,34 +39,34 @@ pub async fn create_database_pool(config: &AppConfig) -> Result { Ok(row) => { let tables_exist: bool = row.get(0); if !tables_exist { - tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations"); + tracing::warn!("Database tables do not exist. Please run migrations with: cargo run --bin migrate --features migrations"); } }, Err(_) => { - tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations"); + tracing::warn!("Could not verify migration status. Please run migrations with: cargo run --bin migrate --features migrations"); } } return Ok(pool); }, Err(e) => { - tracing::error!("Error al verificar conexión: {}", e); - tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations"); + tracing::error!("Error verifying connection: {}", e); + tracing::warn!("The database appears to not be configured. Please run migrations with: cargo run --bin migrate --features migrations"); if attempt >= MAX_ATTEMPTS { - return Err(anyhow::anyhow!("Error al verificar la conexión a PostgreSQL: {}", e)); + return Err(anyhow::anyhow!("Error verifying PostgreSQL connection: {}", e)); } } } }, Err(e) => { - tracing::error!("Error al conectar a PostgreSQL: {}", e); + tracing::error!("Error connecting to PostgreSQL: {}", e); if attempt >= MAX_ATTEMPTS { - return Err(anyhow::anyhow!("Error en la conexión a PostgreSQL: {}", e)); + return Err(anyhow::anyhow!("Error in PostgreSQL connection: {}", e)); } tokio::time::sleep(Duration::from_secs(1)).await; } } } - Err(anyhow::anyhow!("No se pudo establecer la conexión a PostgreSQL después de {} intentos", MAX_ATTEMPTS)) + Err(anyhow::anyhow!("Could not establish PostgreSQL connection after {} attempts", MAX_ATTEMPTS)) } \ No newline at end of file diff --git a/src/infrastructure/repositories/composite_file_repository.rs b/src/infrastructure/repositories/composite_file_repository.rs index a19ab957..7d3cbdeb 100644 --- a/src/infrastructure/repositories/composite_file_repository.rs +++ b/src/infrastructure/repositories/composite_file_repository.rs @@ -10,11 +10,11 @@ use crate::common::errors::DomainError; use crate::domain::entities::file::File; use crate::domain::services::path_service::StoragePath; -/// Composite que envuelve `Arc` + `Arc` -/// y delega cada método al port correspondiente. +/// Composite that wraps `Arc` + `Arc` +/// and delegates each method to the corresponding port. /// -/// Gracias al blanket impl `impl FileStoragePort for T {}` -/// este tipo obtiene `FileStoragePort` automáticamente. +/// Thanks to the blanket impl `impl FileStoragePort for T {}` +/// this type automatically gets `FileStoragePort`. pub struct CompositeFileRepository { read: Arc, write: Arc, diff --git a/src/infrastructure/repositories/file_fs_read_repository.rs b/src/infrastructure/repositories/file_fs_read_repository.rs index 6d184a81..0c09a78b 100644 --- a/src/infrastructure/repositories/file_fs_read_repository.rs +++ b/src/infrastructure/repositories/file_fs_read_repository.rs @@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService; use crate::domain::services::path_service::StoragePath; use crate::common::config::AppConfig; -/// Implementación de repositorio para operaciones de **lectura** de archivos. +/// Repository implementation for file **read** operations. /// -/// Implementa `FileReadPort`: +/// Implements `FileReadPort`: /// get_file, list_files, get_file_content, get_file_stream, /// get_file_range_stream, get_file_mmap, get_file_path, get_parent_folder_id. pub struct FileFsReadRepository { @@ -37,7 +37,7 @@ pub struct FileFsReadRepository { } impl FileFsReadRepository { - /// Constructor completo con todas las dependencias de infraestructura. + /// Full constructor with all infrastructure dependencies. pub fn new( root_path: PathBuf, storage_mediator: Arc, @@ -58,7 +58,7 @@ impl FileFsReadRepository { } } - /// Stub para pruebas (no realiza I/O real). + /// Stub for testing (does not perform real I/O). pub fn default_stub() -> Self { Self { root_path: PathBuf::from("./storage"), @@ -75,7 +75,7 @@ impl FileFsReadRepository { } } - // ─── helpers internos ──────────────────────────────────── + // ─── internal helpers ───────────────────────────────────── fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf { self.path_service.resolve_path(storage_path) diff --git a/src/infrastructure/repositories/file_fs_write_repository.rs b/src/infrastructure/repositories/file_fs_write_repository.rs index 1e8617f8..15aadd8f 100644 --- a/src/infrastructure/repositories/file_fs_write_repository.rs +++ b/src/infrastructure/repositories/file_fs_write_repository.rs @@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService; use crate::domain::services::path_service::StoragePath; use crate::common::config::AppConfig; -/// Implementación de repositorio para operaciones de **escritura** de archivos. +/// Repository implementation for file **write** operations. /// -/// Implementa `FileWritePort`: +/// Implements `FileWritePort`: /// save_file, save_file_from_stream, move_file, delete_file, /// update_file_content, register_file_deferred. pub struct FileFsWriteRepository { @@ -37,7 +37,7 @@ pub struct FileFsWriteRepository { } impl FileFsWriteRepository { - /// Constructor completo con todas las dependencias. + /// Full constructor with all dependencies. pub fn new( root_path: PathBuf, storage_mediator: Arc, @@ -50,7 +50,7 @@ impl FileFsWriteRepository { Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor } } - /// Stub para pruebas (no realiza I/O real). + /// Stub for testing (does not perform real I/O). pub fn default_stub() -> Self { Self { root_path: PathBuf::from("./storage"), diff --git a/src/infrastructure/repositories/folder_fs_repository_trash.rs b/src/infrastructure/repositories/folder_fs_repository_trash.rs index 0f71be0a..3835beda 100644 --- a/src/infrastructure/repositories/folder_fs_repository_trash.rs +++ b/src/infrastructure/repositories/folder_fs_repository_trash.rs @@ -5,147 +5,147 @@ use tracing::{debug, error}; use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult; use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository; -// Este archivo contiene la implementación de los métodos relacionados con la papelera -// para el repositorio de carpetas FolderFsRepository +// This file contains the implementation of trash-related methods +// for the FolderFsRepository folder repository -// Implementación de métodos de papelera para el repositorio de carpetas +// Implementation of trash methods for the folder repository impl FolderFsRepository { - // Obtiene la ruta completa a la papelera + // Gets the full path to the trash directory fn get_trash_dir(&self) -> PathBuf { self.get_root_path().join(".trash").join("folders") } - // Crea una ruta única en la papelera para la carpeta + // Creates a unique path in the trash for the folder async fn create_trash_folder_path(&self, folder_id: &str) -> FolderRepositoryResult { let trash_dir = self.get_trash_dir(); - // Asegurarse que el directorio de la papelera existe + // Ensure the trash directory exists if !trash_dir.exists() { fs::create_dir_all(&trash_dir).await .map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?; } - // Crear una ruta única para la carpeta en la papelera + // Create a unique path for the folder in the trash Ok(trash_dir.join(folder_id)) } } -// Implementación de los métodos públicos del trait FolderRepository relacionados con la papelera +// Implementation of public FolderRepository trait methods related to trash // Implementation of internal methods for trash functionality // These will be enabled when the trash feature is re-enabled impl FolderFsRepository { /// Helper method that will be used for trash functionality pub(crate) async fn _trash_move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> { - debug!("Moviendo carpeta a la papelera: {}", folder_id); + debug!("Moving folder to trash: {}", folder_id); - // Obtener la ruta física de la carpeta + // Get the physical path of the folder let folder_path = match self.get_mapped_folder_path(folder_id).await { Ok(path) => path, Err(e) => { - error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e); + error!("Error getting folder path {}: {:?}", folder_id, e); return Err(e); } }; let folder_path_buf = PathBuf::from(folder_path.to_string()); - // Verificamos que la carpeta existe + // Verify the folder exists if !folder_path_buf.exists() { return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id))); } - // Crear directorio en la papelera + // Create directory in the trash let trash_folder_path = self.create_trash_folder_path(folder_id).await?; - // Mover la carpeta físicamente a la papelera + // Physically move the folder to the trash match fs::rename(&folder_path_buf, &trash_folder_path).await { Ok(_) => { - debug!("Carpeta movida a papelera: {} -> {}", folder_path_buf.display(), trash_folder_path.display()); + debug!("Folder moved to trash: {} -> {}", folder_path_buf.display(), trash_folder_path.display()); - // Actualizar el mapeo al nuevo path en la papelera + // Update the mapping to the new path in the trash if let Err(e) = self.update_mapped_folder_path(folder_id, &trash_folder_path).await { - error!("Error actualizando mapeo de carpeta en papelera: {}", e); + error!("Error updating folder mapping in trash: {}", e); return Err(e); } Ok(()) }, Err(e) => { - error!("Error moviendo carpeta a papelera: {}", e); + error!("Error moving folder to trash: {}", e); Err(FolderRepositoryError::StorageError(e.to_string())) } } } - /// Restaura una carpeta desde la papelera a su ubicación original + /// Restores a folder from the trash to its original location pub(crate) async fn _trash_restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> { - debug!("Restaurando carpeta {} a {}", folder_id, original_path); + debug!("Restoring folder {} to {}", folder_id, original_path); - // Obtener la ruta actual en la papelera + // Get the current path in the trash let current_path = match self.get_mapped_folder_path(folder_id).await { Ok(path) => PathBuf::from(path), Err(e) => { - error!("Error obteniendo ruta actual de la carpeta {}: {:?}", folder_id, e); + error!("Error getting current folder path {}: {:?}", folder_id, e); return Err(e); } }; - // Convertir la ruta original a PathBuf + // Convert the original path to PathBuf let original_path_buf = PathBuf::from(original_path); - // Asegurar que el directorio padre de destino existe + // Ensure the destination parent directory exists if let Some(parent) = original_path_buf.parent() { if !parent.exists() { fs::create_dir_all(parent).await .map_err(|e| { - error!("Error creando directorio padre para restauración: {}", e); + error!("Error creating parent directory for restoration: {}", e); FolderRepositoryError::StorageError(e.to_string()) })?; } } - // Mover la carpeta de la papelera a su ubicación original + // Move the folder from the trash to its original location match fs::rename(¤t_path, &original_path_buf).await { Ok(_) => { - debug!("Carpeta restaurada: {} -> {}", current_path.display(), original_path_buf.display()); + debug!("Folder restored: {} -> {}", current_path.display(), original_path_buf.display()); - // Actualizar el mapeo a la ruta original + // Update the mapping to the original path if let Err(e) = self.update_mapped_folder_path(folder_id, &original_path_buf).await { - error!("Error actualizando mapeo de carpeta restaurada: {}", e); + error!("Error updating restored folder mapping: {}", e); return Err(e); } Ok(()) }, Err(e) => { - error!("Error restaurando carpeta: {}", e); + error!("Error restoring folder: {}", e); Err(FolderRepositoryError::StorageError(e.to_string())) } } } - /// Elimina una carpeta permanentemente (usado por la papelera) + /// Permanently deletes a folder (used by the trash) pub(crate) async fn _trash_delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> { - debug!("Eliminando carpeta permanentemente: {}", folder_id); + debug!("Permanently deleting folder: {}", folder_id); - // Similar a delete_folder pero sin validaciones adicionales + // Similar to delete_folder but without additional validations let folder_path = match self.get_mapped_folder_path(folder_id).await { Ok(path) => PathBuf::from(path), Err(e) => { - error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e); + error!("Error getting folder path {}: {:?}", folder_id, e); return Err(e); } }; - // Eliminar la carpeta recursivamente + // Delete the folder recursively if folder_path.exists() { match fs::remove_dir_all(&folder_path).await { Ok(_) => { - debug!("Carpeta eliminada permanentemente: {}", folder_path.display()); + debug!("Folder permanently deleted: {}", folder_path.display()); }, Err(e) => { - error!("Error eliminando carpeta permanentemente: {}", e); - // No reportar error si la carpeta ya no existe + error!("Error permanently deleting folder: {}", e); + // Don't report error if the folder no longer exists if e.kind() != std::io::ErrorKind::NotFound { return Err(FolderRepositoryError::StorageError(e.to_string())); } @@ -153,16 +153,16 @@ impl FolderFsRepository { } } - // Eliminar el mapeo + // Remove the mapping if let Err(e) = self.remove_mapped_folder_id(folder_id).await { - error!("Error eliminando mapeo de la carpeta: {}", e); + error!("Error removing folder mapping: {}", e); return Err(e); } - debug!("Carpeta eliminada permanentemente con éxito: {}", folder_id); + debug!("Folder permanently deleted successfully: {}", folder_id); Ok(()) } } -// Re-exportaciones necesarias para el compilador +// Re-exports needed by the compiler use crate::infrastructure::repositories::repository_errors::FolderRepositoryError; \ No newline at end of file diff --git a/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs b/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs index 114338b0..0865d442 100644 --- a/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs +++ b/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs @@ -20,9 +20,9 @@ impl CalendarEventPgRepository { #[async_trait] impl CalendarEventRepository for CalendarEventPgRepository { async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult { - // Este método necesitaría una implementación completa que construya el CalendarEvent - // desde el resultado de la query, utilizando métodos del constructor - // Para esta demostración, vamos a retornar el mismo evento + // This method would need a full implementation that builds the CalendarEvent + // from the query result, using constructor methods + // For this demonstration, we return the same event sqlx::query( r#" @@ -50,7 +50,7 @@ impl CalendarEventRepository for CalendarEventPgRepository { .await .map_err(|e| DomainError::database_error(format!("Failed to create calendar event: {}", e)))?; - // Devolvemos el mismo evento en vez de un resultado + // We return the same event instead of a result Ok(event) } @@ -86,8 +86,8 @@ impl CalendarEventRepository for CalendarEventPgRepository { .await .map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?; - // En una implementación completa, recuperaríamos el evento actualizado - // Por simplicidad, devolvemos el mismo evento que recibimos + // In a full implementation, we would retrieve the updated event + // For simplicity, we return the same event we received Ok(event) } @@ -176,9 +176,9 @@ impl CalendarEventRepository for CalendarEventPgRepository { .map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))? .ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?; - // En una implementación real, construiríamos un objeto CalendarEvent completo - // Por simplicidad, creamos un objeto con valores predeterminados para - // demostrar el enfoque sin macros + // In a real implementation, we would build a complete CalendarEvent object + // For simplicity, we create an object with default values to + // demonstrate the approach without macros let event = CalendarEvent::with_id( row.get("id"), diff --git a/src/infrastructure/repositories/pg/calendar_pg_repository.rs b/src/infrastructure/repositories/pg/calendar_pg_repository.rs index 4690196f..3b9ec4bd 100644 --- a/src/infrastructure/repositories/pg/calendar_pg_repository.rs +++ b/src/infrastructure/repositories/pg/calendar_pg_repository.rs @@ -32,14 +32,14 @@ impl CalendarRepository for CalendarPgRepository { .bind(calendar.owner_id()) .bind(calendar.description()) .bind(calendar.color()) - .bind(false) // is_public no existe como campo + .bind(false) // is_public doesn't exist as a field .bind(calendar.created_at()) .bind(calendar.updated_at()) .fetch_one(&*self.pool) .await .map_err(|e| DomainError::database_error(format!("Failed to create calendar: {}", e)))?; - // Construir el objeto Calendar utilizando su constructor with_id + // Build the Calendar object using its with_id constructor let result = Calendar::with_id( row.get("id"), row.get("name"), @@ -66,14 +66,14 @@ impl CalendarRepository for CalendarPgRepository { .bind(calendar.name()) .bind(calendar.description()) .bind(calendar.color()) - .bind(false) // is_public no existe como campo + .bind(false) // is_public doesn't exist as a field .bind(now) .bind(calendar.id()) .fetch_one(&*self.pool) .await .map_err(|e| DomainError::database_error(format!("Failed to update calendar: {}", e)))?; - // Construir el objeto Calendar utilizando su constructor with_id + // Build the Calendar object using its with_id constructor let result = Calendar::with_id( row.get("id"), row.get("name"), diff --git a/src/infrastructure/repositories/pg/favorites_pg_repository.rs b/src/infrastructure/repositories/pg/favorites_pg_repository.rs index 0e78c3f2..a10151f2 100644 --- a/src/infrastructure/repositories/pg/favorites_pg_repository.rs +++ b/src/infrastructure/repositories/pg/favorites_pg_repository.rs @@ -8,7 +8,7 @@ use crate::application::dtos::favorites_dto::FavoriteItemDto; use crate::application::ports::favorites_ports::FavoritesRepositoryPort; use crate::common::errors::{Result, DomainError, ErrorKind}; -/// Implementación PostgreSQL del puerto de persistencia de favoritos. +/// PostgreSQL implementation of the favorites persistence port. pub struct FavoritesPgRepository { db_pool: Arc, } diff --git a/src/infrastructure/repositories/pg/recent_items_pg_repository.rs b/src/infrastructure/repositories/pg/recent_items_pg_repository.rs index 42ee3159..078f7db1 100644 --- a/src/infrastructure/repositories/pg/recent_items_pg_repository.rs +++ b/src/infrastructure/repositories/pg/recent_items_pg_repository.rs @@ -8,7 +8,7 @@ use crate::application::dtos::recent_dto::RecentItemDto; use crate::application::ports::recent_ports::RecentItemsRepositoryPort; use crate::common::errors::{Result, DomainError, ErrorKind}; -/// Implementación PostgreSQL del puerto de persistencia de elementos recientes. +/// PostgreSQL implementation of the recent items persistence port. pub struct RecentItemsPgRepository { db_pool: Arc, } diff --git a/src/infrastructure/repositories/pg/session_pg_repository.rs b/src/infrastructure/repositories/pg/session_pg_repository.rs index f22affdd..6e6bffd8 100644 --- a/src/infrastructure/repositories/pg/session_pg_repository.rs +++ b/src/infrastructure/repositories/pg/session_pg_repository.rs @@ -10,7 +10,7 @@ use crate::application::ports::auth_ports::SessionStoragePort; use crate::common::errors::DomainError; use crate::infrastructure::repositories::pg::transaction_utils::with_transaction; -// Implementar From para SessionRepositoryError para permitir conversiones automáticas +// Implement From for SessionRepositoryError to allow automatic conversions impl From for SessionRepositoryError { fn from(err: sqlx::Error) -> Self { SessionPgRepository::map_sqlx_error(err) @@ -26,14 +26,14 @@ impl SessionPgRepository { Self { pool } } - // Método auxiliar para mapear errores SQL a errores de dominio + // Helper method to map SQL errors to domain errors pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError { match err { sqlx::Error::RowNotFound => { - SessionRepositoryError::NotFound("Sesión no encontrada".to_string()) + SessionRepositoryError::NotFound("Session not found".to_string()) }, _ => SessionRepositoryError::DatabaseError( - format!("Error de base de datos: {}", err) + format!("Database error: {}", err) ), } } @@ -41,9 +41,9 @@ impl SessionPgRepository { #[async_trait] impl SessionRepository for SessionPgRepository { - /// Crea una nueva sesión utilizando una transacción + /// Creates a new session using a transaction async fn create_session(&self, session: Session) -> SessionRepositoryResult { - // Crear una copia de la sesión para el closure + // Create a copy of the session for the closure let session_clone = session.clone(); with_transaction( @@ -51,7 +51,7 @@ impl SessionRepository for SessionPgRepository { "create_session", |tx| { Box::pin(async move { - // Insertar la sesión + // Insert the session sqlx::query( r#" INSERT INTO auth.sessions ( @@ -74,8 +74,8 @@ impl SessionRepository for SessionPgRepository { .await .map_err(Self::map_sqlx_error)?; - // Opcionalmente, actualizar el último login del usuario - // dentro de la misma transacción + // Optionally, update the user's last login + // within the same transaction sqlx::query( r#" UPDATE auth.users @@ -87,12 +87,12 @@ impl SessionRepository for SessionPgRepository { .execute(&mut **tx) .await .map_err(|e| { - // Convertimos el error pero sin interrumpir la creación - // de la sesión si falla la actualización - tracing::warn!("No se pudo actualizar last_login_at para usuario {}: {}", + // Convert the error but without interrupting session + // creation if the update fails + tracing::warn!("Could not update last_login_at for user {}: {}", session_clone.user_id(), e); SessionRepositoryError::DatabaseError(format!( - "Sesión creada pero no se pudo actualizar last_login_at: {}", e + "Session created but could not update last_login_at: {}", e )) })?; @@ -104,7 +104,7 @@ impl SessionRepository for SessionPgRepository { Ok(session) } - /// Obtiene una sesión por ID + /// Gets a session by ID async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult { let row = sqlx::query( r#" @@ -132,7 +132,7 @@ impl SessionRepository for SessionPgRepository { )) } - /// Obtiene una sesión por token de actualización + /// Gets a session by refresh token async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult { let row = sqlx::query( r#" @@ -160,7 +160,7 @@ impl SessionRepository for SessionPgRepository { )) } - /// Obtiene todas las sesiones de un usuario + /// Gets all sessions for a user async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult> { let rows = sqlx::query( r#" @@ -195,16 +195,16 @@ impl SessionRepository for SessionPgRepository { Ok(sessions) } - /// Revoca una sesión específica utilizando una transacción + /// Revokes a specific session using a transaction async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> { - let id = session_id.to_string(); // Clone para uso en closure + let id = session_id.to_string(); // Clone for use in closure with_transaction( &self.pool, "revoke_session", |tx| { Box::pin(async move { - // Revocar la sesión + // Revoke the session let result = sqlx::query( r#" UPDATE auth.sessions @@ -218,14 +218,14 @@ impl SessionRepository for SessionPgRepository { .await .map_err(Self::map_sqlx_error)?; - // Si encontramos la sesión, podemos registrar un evento de seguridad + // If we found the session, we can log a security event if let Some(row) = result { let user_id: String = row.try_get("user_id").unwrap_or_default(); - // Registrar evento de seguridad (en una tabla de seguridad) - // Esto es opcional pero muestra cómo se puede realizar operaciones - // adicionales en la misma transacción - tracing::info!("Sesión con ID {} del usuario {} revocada", id, user_id); + // Log security event (in a security table) + // This is optional but shows how additional operations + // can be performed in the same transaction + tracing::info!("Session with ID {} for user {} revoked", id, user_id); } Ok(()) @@ -234,16 +234,16 @@ impl SessionRepository for SessionPgRepository { ).await } - /// Revoca todas las sesiones de un usuario utilizando una transacción + /// Revokes all sessions for a user using a transaction async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult { - let user_id_clone = user_id.to_string(); // Clone para uso en closure + let user_id_clone = user_id.to_string(); // Clone for use in closure with_transaction( &self.pool, "revoke_all_user_sessions", |tx| { Box::pin(async move { - // Revocar todas las sesiones del usuario + // Revoke all sessions for the user let result = sqlx::query( r#" UPDATE auth.sessions @@ -258,9 +258,9 @@ impl SessionRepository for SessionPgRepository { let affected = result.rows_affected(); - // Registrar evento de seguridad + // Log security event if affected > 0 { - tracing::info!("Revocadas {} sesiones del usuario {}", affected, user_id_clone); + tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone); } Ok(affected) @@ -269,7 +269,7 @@ impl SessionRepository for SessionPgRepository { ).await } - /// Elimina sesiones expiradas + /// Deletes expired sessions async fn delete_expired_sessions(&self) -> SessionRepositoryResult { let now = Utc::now(); @@ -288,7 +288,7 @@ impl SessionRepository for SessionPgRepository { } } -// Implementación del puerto de almacenamiento para la capa de aplicación +// Implementation of the storage port for the application layer #[async_trait] impl SessionStoragePort for SessionPgRepository { async fn create_session(&self, session: Session) -> Result { diff --git a/src/infrastructure/repositories/pg/user_pg_repository.rs b/src/infrastructure/repositories/pg/user_pg_repository.rs index bae15a06..8d8e8bd2 100644 --- a/src/infrastructure/repositories/pg/user_pg_repository.rs +++ b/src/infrastructure/repositories/pg/user_pg_repository.rs @@ -9,7 +9,7 @@ use crate::application::ports::auth_ports::UserStoragePort; use crate::common::errors::DomainError; use crate::infrastructure::repositories::pg::transaction_utils::with_transaction; -// Implementar From para UserRepositoryError para permitir conversiones automáticas +// Implement From for UserRepositoryError to allow automatic conversions impl From for UserRepositoryError { fn from(err: sqlx::Error) -> Self { UserPgRepository::map_sqlx_error(err) @@ -25,26 +25,26 @@ impl UserPgRepository { Self { pool } } - // Método auxiliar para mapear errores SQL a errores de dominio + // Helper method to map SQL errors to domain errors pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError { match err { sqlx::Error::RowNotFound => { - UserRepositoryError::NotFound("Usuario no encontrado".to_string()) + UserRepositoryError::NotFound("User not found".to_string()) }, sqlx::Error::Database(db_err) => { if db_err.code().map_or(false, |code| code == "23505") { - // Código para violación de unicidad en PostgreSQL + // PostgreSQL uniqueness violation code UserRepositoryError::AlreadyExists( - "Usuario o email ya existe".to_string() + "User or email already exists".to_string() ) } else { UserRepositoryError::DatabaseError( - format!("Error de base de datos: {}", db_err) + format!("Database error: {}", db_err) ) } }, _ => UserRepositoryError::DatabaseError( - format!("Error de base de datos: {}", err) + format!("Database error: {}", err) ), } } @@ -52,7 +52,7 @@ impl UserPgRepository { #[async_trait] impl UserRepository for UserPgRepository { - /// Crea un nuevo usuario utilizando una transacción + /// Creates a new user using a transaction async fn create_user(&self, user: User) -> UserRepositoryResult { // Creamos una copia del usuario para el closure let user_clone = user.clone(); @@ -61,14 +61,14 @@ impl UserRepository for UserPgRepository { &self.pool, "create_user", |tx| { - // Necesitamos mover el closure a un BoxFuture para devolver dentro - // de la llamada with_transaction + // We need to move the closure into a BoxFuture to return inside + // the with_transaction call Box::pin(async move { - // Usamos los getters para extraer los valores - // Convertimos user.role() a string para pasarlo como texto plano + // Use getters to extract the values + // Convert user.role() to string to pass it as plain text let role_str = user_clone.role().to_string(); - // Modificar el SQL para hacer un cast explícito al tipo auth.userrole + // Modify the SQL to do an explicit cast to the auth.userrole type let _result = sqlx::query( r#" INSERT INTO auth.users ( @@ -87,7 +87,7 @@ impl UserRepository for UserPgRepository { .bind(user_clone.username()) .bind(user_clone.email()) .bind(user_clone.password_hash()) - .bind(&role_str) // Convertir a string pero con cast explícito en SQL + .bind(&role_str) // Convert to string but with explicit cast in SQL .bind(user_clone.storage_quota_bytes()) .bind(user_clone.storage_used_bytes()) .bind(user_clone.created_at()) @@ -100,18 +100,18 @@ impl UserRepository for UserPgRepository { .await .map_err(Self::map_sqlx_error)?; - // Podríamos realizar operaciones adicionales aquí, - // como configurar permisos, roles, etc. + // We could perform additional operations here, + // such as configuring permissions, roles, etc. Ok(user_clone) }) as BoxFuture<'_, UserRepositoryResult> } ).await?; - Ok(user) // Devolvemos el usuario original por simplicidad + Ok(user) // Return the original user for simplicity } - /// Obtiene un usuario por ID + /// Gets a user by ID async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult { let row = sqlx::query( r#" @@ -153,7 +153,7 @@ impl UserRepository for UserPgRepository { )) } - /// Obtiene un usuario por nombre de usuario + /// Gets a user by username async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult { let row = sqlx::query( r#" @@ -195,7 +195,7 @@ impl UserRepository for UserPgRepository { )) } - /// Obtiene un usuario por correo electrónico + /// Gets a user by email async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult { let row = sqlx::query( r#" @@ -237,9 +237,9 @@ impl UserRepository for UserPgRepository { )) } - /// Actualiza un usuario existente utilizando una transacción + /// Updates an existing user using a transaction async fn update_user(&self, user: User) -> UserRepositoryResult { - // Creamos una copia del usuario para el closure + // Create a copy of the user for the closure let user_clone = user.clone(); with_transaction( @@ -247,7 +247,7 @@ impl UserRepository for UserPgRepository { "update_user", |tx| { Box::pin(async move { - // Actualizar el usuario + // Update the user sqlx::query( r#" UPDATE auth.users @@ -278,8 +278,8 @@ impl UserRepository for UserPgRepository { .await .map_err(Self::map_sqlx_error)?; - // Podríamos realizar operaciones adicionales aquí dentro - // de la misma transacción, como actualizar permisos, etc. + // We could perform additional operations here inside + // the same transaction, such as updating permissions, etc. Ok(user_clone) }) as BoxFuture<'_, UserRepositoryResult> @@ -289,7 +289,7 @@ impl UserRepository for UserPgRepository { Ok(user) } - /// Actualiza solo el uso de almacenamiento de un usuario + /// Updates only the storage usage of a user async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> { sqlx::query( r#" @@ -309,7 +309,7 @@ impl UserRepository for UserPgRepository { Ok(()) } - /// Actualiza la fecha de último inicio de sesión + /// Updates the last login date async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> { sqlx::query( r#" @@ -328,7 +328,7 @@ impl UserRepository for UserPgRepository { Ok(()) } - /// Lista usuarios con paginación + /// Lists users with pagination async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult> { let rows = sqlx::query( r#" @@ -378,7 +378,7 @@ impl UserRepository for UserPgRepository { Ok(users) } - /// Activa o desactiva un usuario + /// Activates or deactivates a user async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> { sqlx::query( r#" @@ -398,7 +398,7 @@ impl UserRepository for UserPgRepository { Ok(()) } - /// Cambia la contraseña de un usuario + /// Changes a user's password async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> { sqlx::query( r#" @@ -418,9 +418,9 @@ impl UserRepository for UserPgRepository { Ok(()) } - /// Cambia el rol de un usuario + /// Changes a user's role async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> { - // Convertir el rol a string para el binding + // Convert the role to string for the binding let role_str = role.to_string(); sqlx::query( @@ -441,7 +441,7 @@ impl UserRepository for UserPgRepository { Ok(()) } - /// Lista usuarios por rol + /// Lists users by role async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult> { let rows = sqlx::query( r#" @@ -490,7 +490,7 @@ impl UserRepository for UserPgRepository { Ok(users) } - /// Elimina un usuario + /// Deletes a user async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> { sqlx::query( r#" @@ -548,7 +548,7 @@ impl UserRepository for UserPgRepository { )) } - /// Actualiza la cuota de almacenamiento de un usuario + /// Updates a user's storage quota async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> { sqlx::query( r#" @@ -568,7 +568,7 @@ impl UserRepository for UserPgRepository { Ok(()) } - /// Cuenta el número total de usuarios + /// Counts the total number of users async fn count_users(&self) -> UserRepositoryResult { let row = sqlx::query( "SELECT COUNT(*) as count FROM auth.users" @@ -581,7 +581,7 @@ impl UserRepository for UserPgRepository { Ok(count) } - /// Obtiene estadísticas de almacenamiento agregadas + /// Gets aggregated storage statistics async fn get_storage_stats(&self) -> UserRepositoryResult { let row = sqlx::query( r#" @@ -610,7 +610,7 @@ impl UserRepository for UserPgRepository { } } -// Implementación del puerto de almacenamiento para la capa de aplicación +// Storage port implementation for the application layer #[async_trait] impl UserStoragePort for UserPgRepository { async fn create_user(&self, user: User) -> Result { diff --git a/src/infrastructure/repositories/share_fs_repository.rs b/src/infrastructure/repositories/share_fs_repository.rs index be6136fd..0abfcd59 100644 --- a/src/infrastructure/repositories/share_fs_repository.rs +++ b/src/infrastructure/repositories/share_fs_repository.rs @@ -12,7 +12,7 @@ use crate::{ }, }; -// Estructura para almacenar en el sistema de archivos +// Structure for storing in the file system #[derive(Debug, Clone, Serialize, Deserialize)] struct ShareRecord { id: String, @@ -38,12 +38,12 @@ impl ShareFsRepository { Self { config } } - /// Obtiene la ruta del archivo JSON donde se almacenan los enlaces compartidos + /// Gets the path to the JSON file where shared links are stored fn get_shares_path(&self) -> String { format!("{}/shares.json", self.config.storage_path.display()) } - /// Lee todos los enlaces compartidos del archivo JSON + /// Reads all shared links from the JSON file async fn read_shares(&self) -> Result, io::Error> { let path = self.get_shares_path(); let path = Path::new(&path); @@ -58,12 +58,12 @@ impl ShareFsRepository { Ok(shares) } - /// Guarda todos los enlaces compartidos en el archivo JSON + /// Saves all shared links to the JSON file async fn write_shares(&self, shares: &[ShareRecord]) -> Result<(), io::Error> { let path = self.get_shares_path(); let json = serde_json::to_string_pretty(shares)?; - // Asegúrate de que el directorio existe + // Make sure the directory exists let dir = Path::new(&path).parent().unwrap(); if !dir.exists() { fs::create_dir_all(dir).await? @@ -72,7 +72,7 @@ impl ShareFsRepository { fs::write(path, json).await } - /// Convierte un registro del sistema de archivos a una entidad de dominio + /// Converts a file system record to a domain entity fn to_entity(&self, record: &ShareRecord) -> Share { let item_type = ShareItemType::try_from(record.item_type.as_str()) .unwrap_or(ShareItemType::File); @@ -97,7 +97,7 @@ impl ShareFsRepository { ) } - /// Convierte una entidad de dominio a un registro para el sistema de archivos + /// Converts a domain entity to a file system record fn to_record(&self, share: &Share) -> ShareRecord { ShareRecord { id: share.id().to_string(), @@ -122,16 +122,16 @@ impl ShareStoragePort for ShareFsRepository { let mut shares = self.read_shares().await .map_err(|e| DomainError::internal_error("Share", e.to_string()))?; - // Verifica si el enlace ya existe + // Check if the link already exists let existing_index = shares.iter().position(|s| s.id == share.id()); let record = self.to_record(share); if let Some(index) = existing_index { - // Actualización + // Update shares[index] = record; } else { - // Inserción + // Insert shares.push(record); } @@ -190,16 +190,16 @@ impl ShareStoragePort for ShareFsRepository { let mut shares = self.read_shares().await .map_err(|e| DomainError::internal_error("Share", e.to_string()))?; - // Busca el índice del enlace a actualizar + // Find the index of the link to update let index = shares.iter().position(|s| s.id == share.id()) .ok_or_else(|| { DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id())) })?; - // Actualiza el registro + // Update the record shares[index] = self.to_record(share); - // Guarda los cambios + // Save changes self.write_shares(&shares).await .map_err(|e| DomainError::internal_error("Share", e.to_string()))?; @@ -210,16 +210,16 @@ impl ShareStoragePort for ShareFsRepository { let mut shares = self.read_shares().await .map_err(|e| DomainError::internal_error("Share", e.to_string()))?; - // Encuentra el índice del enlace a eliminar + // Find the index of the link to delete let initial_len = shares.len(); shares.retain(|s| s.id != id); - // Si no se eliminó ningún enlace, significa que no existía + // If no link was deleted, it means it didn't exist if shares.len() == initial_len { return Err(DomainError::not_found("Share", format!("Share with ID {} not found for deletion", id))); } - // Guarda los cambios + // Save changes self.write_shares(&shares).await .map_err(|e| DomainError::internal_error("Share", e.to_string()))?; @@ -230,15 +230,15 @@ impl ShareStoragePort for ShareFsRepository { let shares = self.read_shares().await .map_err(|e| DomainError::internal_error("Share", e.to_string()))?; - // Filtra los enlaces del usuario + // Filter the user's links let user_shares: Vec = shares.into_iter() .filter(|s| s.created_by == user_id) .collect(); - // Calcula el total + // Calculate the total let total = user_shares.len(); - // Aplica la paginación + // Apply pagination let paginated: Vec = user_shares.iter() .skip(offset) .take(limit) diff --git a/src/infrastructure/repositories/trash_fs_repository.rs b/src/infrastructure/repositories/trash_fs_repository.rs index ac32f7f6..3399949d 100644 --- a/src/infrastructure/repositories/trash_fs_repository.rs +++ b/src/infrastructure/repositories/trash_fs_repository.rs @@ -12,7 +12,7 @@ use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType}; use crate::domain::repositories::trash_repository::TrashRepository; use crate::application::ports::outbound::IdMappingPort; -/// Estructura para almacenar elementos en la papelera en formato JSON +/// Structure for storing trash items in JSON format #[derive(Debug, Serialize, Deserialize)] struct TrashedItemEntry { id: String, @@ -25,7 +25,7 @@ struct TrashedItemEntry { deletion_date: String, } -/// Implementación del repositorio de papelera usando el sistema de archivos +/// Trash repository implementation using the file system pub struct TrashFsRepository { trash_dir: PathBuf, trash_index_path: PathBuf, @@ -45,7 +45,7 @@ impl TrashFsRepository { } } - /// Asegura que existe el directorio de papelera + /// Ensures the trash directory exists async fn ensure_trash_dir(&self) -> Result<()> { debug!("Checking if trash directory exists: {}", self.trash_dir.display()); if !self.trash_dir.exists() { @@ -105,7 +105,7 @@ impl TrashFsRepository { Ok(()) } - /// Obtiene todas las entradas del índice de papelera + /// Gets all entries from the trash index async fn get_trash_entries(&self) -> Result> { self.ensure_trash_dir().await?; @@ -134,7 +134,7 @@ impl TrashFsRepository { Ok(entries) } - /// Guarda todas las entradas en el índice de papelera + /// Saves all entries to the trash index async fn save_trash_entries(&self, entries: Vec) -> Result<()> { self.ensure_trash_dir().await?; @@ -155,7 +155,7 @@ impl TrashFsRepository { Ok(()) } - /// Convierte una entrada JSON a entidad TrashedItem + /// Converts a JSON entry to a TrashedItem entity fn entry_to_trashed_item(&self, entry: TrashedItemEntry) -> Result { let item_type = match entry.item_type.as_str() { "file" => TrashedItemType::File, @@ -206,7 +206,7 @@ impl TrashFsRepository { )) } - /// Convierte una entidad TrashedItem a entrada JSON + /// Converts a TrashedItem entity to a JSON entry fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry { TrashedItemEntry { id: item.id().to_string(), @@ -228,9 +228,9 @@ impl TrashFsRepository { impl TrashRepository for TrashFsRepository { #[instrument(skip(self))] async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> { - debug!("Añadiendo elemento a la papelera: id={}, user={}", item.id(), item.user_id()); + debug!("Adding item to trash: id={}, user={}", item.id(), item.user_id()); - // Aseguramos que existe el directorio de la papelera para este usuario + // Ensure the trash directory exists for this user let user_trash_dir = self.trash_dir.join("files").join(item.user_id().to_string()); debug!("User trash directory path: {}", user_trash_dir.display()); @@ -268,7 +268,7 @@ impl TrashRepository for TrashFsRepository { #[instrument(skip(self))] async fn get_trash_items(&self, user_id: &Uuid) -> Result> { - debug!("Obteniendo elementos en papelera para usuario: {}", user_id); + debug!("Getting trash items for user: {}", user_id); let entries = self.get_trash_entries().await?; @@ -290,7 +290,7 @@ impl TrashRepository for TrashFsRepository { #[instrument(skip(self))] async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result> { - debug!("Buscando elemento en papelera: id={}, user={}", id, user_id); + debug!("Looking for item in trash: id={}, user={}", id, user_id); let entries = self.get_trash_entries().await?; @@ -311,7 +311,7 @@ impl TrashRepository for TrashFsRepository { #[instrument(skip(self))] async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { - debug!("Restaurando elemento de la papelera: id={}, user={}", id, user_id); + debug!("Restoring item from trash: id={}, user={}", id, user_id); let mut entries = self.get_trash_entries().await?; @@ -333,16 +333,16 @@ impl TrashRepository for TrashFsRepository { #[instrument(skip(self))] async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { - debug!("Eliminando permanentemente elemento de la papelera: id={}, user={}", id, user_id); + debug!("Permanently deleting item from trash: id={}, user={}", id, user_id); - // Simplemente eliminamos la entrada del índice - // Los archivos físicos se eliminarán a través del repositorio correspondiente + // Simply remove the entry from the index + // Physical files will be deleted through the corresponding repository self.restore_from_trash(id, user_id).await } #[instrument(skip(self))] async fn clear_trash(&self, user_id: &Uuid) -> Result<()> { - debug!("Limpiando papelera para usuario: {}", user_id); + debug!("Clearing trash for user: {}", user_id); let mut entries = self.get_trash_entries().await?; let user_id_str = user_id.to_string(); @@ -355,7 +355,7 @@ impl TrashRepository for TrashFsRepository { #[instrument(skip(self))] async fn get_expired_items(&self) -> Result> { - debug!("Buscando elementos de papelera expirados"); + debug!("Looking for expired trash items"); let entries = self.get_trash_entries().await?; let now = Utc::now(); diff --git a/src/infrastructure/services/buffer_pool.rs b/src/infrastructure/services/buffer_pool.rs index 0569ce15..07c5d0fa 100644 --- a/src/infrastructure/services/buffer_pool.rs +++ b/src/infrastructure/services/buffer_pool.rs @@ -5,71 +5,71 @@ use tokio::sync::{Mutex, Semaphore}; use std::time::{Duration, Instant}; use tracing::debug; -/// Tamaño por defecto de los buffers en el pool +/// Default buffer size in the pool pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB -/// Número máximo por defecto de buffers en el pool +/// Default maximum number of buffers in the pool pub const DEFAULT_MAX_BUFFERS: usize = 100; -/// Tiempo de vida por defecto de un buffer inactivo (en segundos) +/// Default time-to-live for an inactive buffer (in seconds) pub const DEFAULT_BUFFER_TTL: u64 = 60; -/// Buffer pooling para optimizar operaciones de lectura/escritura +/// Buffer pooling to optimize read/write operations pub struct BufferPool { - /// Pool de buffers disponibles + /// Pool of available buffers pool: Mutex>, - /// Semáforo para limitar el número máximo de buffers + /// Semaphore to limit the maximum number of buffers limit: Semaphore, - /// Tamaño de los buffers en el pool + /// Size of buffers in the pool buffer_size: usize, - /// Estadísticas del pool + /// Pool statistics stats: Mutex, - /// Tiempo de vida de un buffer inactivo + /// Time-to-live for an inactive buffer buffer_ttl: Duration, } -/// Estructura para tracking de estadísticas del pool +/// Structure for tracking pool statistics #[derive(Debug, Clone, Default)] pub struct BufferPoolStats { - /// Número total de operaciones de get + /// Total number of get operations pub gets: usize, - /// Número de hits del pool (reutilización exitosa) + /// Number of pool hits (successful reuse) pub hits: usize, - /// Número de misses (creación de nuevo buffer) + /// Number of misses (new buffer creation) pub misses: usize, - /// Número de retornos al pool + /// Number of returns to the pool pub returns: usize, - /// Número de eviction por TTL + /// Number of TTL evictions pub evictions: usize, - /// Número máximo de buffers alcanzado + /// Maximum number of buffers reached pub max_buffers_reached: usize, - /// Esperas por semáforo + /// Semaphore waits pub waits: usize, } -/// Buffer del pool con metadatos para gestión +/// Pool buffer with management metadata struct PooledBuffer { - /// Buffer real de bytes + /// Actual byte buffer buffer: Vec, - /// Timestamp de cuándo se añadió/retornó al pool + /// Timestamp of when it was added/returned to the pool last_used: Instant, } -/// Buffer prestado del pool con cleanup automático +/// Borrowed buffer from the pool with automatic cleanup #[derive(Clone)] pub struct BorrowedBuffer { - /// Buffer actual + /// Current buffer buffer: Vec, - /// Tamaño real utilizado del buffer + /// Actual used size of the buffer used_size: usize, - /// Referencia al pool para retornar + /// Reference to the pool for returning pool: Arc, - /// Si el buffer debe o no retornarse al pool + /// Whether the buffer should be returned to the pool or not return_to_pool: bool, } impl BufferPool { - /// Crea un nuevo pool de buffers + /// Creates a new buffer pool pub fn new(buffer_size: usize, max_buffers: usize, buffer_ttl_secs: u64) -> Arc { Arc::new(Self { pool: Mutex::new(VecDeque::with_capacity(max_buffers)), @@ -80,7 +80,7 @@ impl BufferPool { }) } - /// Crea un pool con configuración por defecto + /// Creates a pool with default configuration pub fn default() -> Arc { Self::new( DEFAULT_BUFFER_SIZE, @@ -89,25 +89,25 @@ impl BufferPool { ) } - /// Obtiene un buffer del pool o crea uno nuevo si es necesario. + /// Gets a buffer from the pool or creates a new one if needed. /// This version takes an Arc to ensure the BorrowedBuffer keeps a proper /// reference to the shared pool (not a clone). #[allow(unused_variables)] pub async fn get_buffer(self: &Arc) -> BorrowedBuffer { - // Incrementar contador de gets + // Increment get counter { let mut stats = self.stats.lock().await; stats.gets += 1; } - // Control de concurrencia + // Concurrency control // Acquire a semaphore permit. If none available, wait. // We forget() the permit so it doesn't auto-release on drop. // Instead, the permit is manually released in return_buffer/Drop via add_permits(1). match self.limit.try_acquire() { Ok(permit) => permit.forget(), Err(_) => { - // No hay permisos disponibles, esperamos + // No permits available, waiting { let mut stats = self.stats.lock().await; stats.waits += 1; @@ -121,15 +121,15 @@ impl BufferPool { } }; - // Intentar obtener un buffer existente del pool + // Try to get an existing buffer from the pool let mut pool_locked = self.pool.lock().await; let pool_arc = Arc::clone(self); if let Some(mut pooled_buffer) = pool_locked.pop_front() { - // Verificar si el buffer ha expirado + // Check if the buffer has expired if pooled_buffer.last_used.elapsed() > self.buffer_ttl { - // Buffer expirado, descartamos y creamos uno nuevo + // Expired buffer, discard and create a new one let mut stats = self.stats.lock().await; stats.evictions += 1; stats.misses += 1; @@ -137,8 +137,8 @@ impl BufferPool { debug!("Buffer pool: evicted expired buffer"); - // Crear nuevo buffer (reutilizando el permiso) - drop(pool_locked); // Liberar el lock antes de retornar + // Create new buffer (reusing the permit) + drop(pool_locked); // Release the lock before returning BorrowedBuffer { buffer: vec![0; self.buffer_size], @@ -147,15 +147,15 @@ impl BufferPool { return_to_pool: true, } } else { - // Buffer válido, lo reutilizamos + // Valid buffer, reuse it let mut stats = self.stats.lock().await; stats.hits += 1; drop(stats); - // Liberar el lock antes de retornar + // Release the lock before returning drop(pool_locked); - // Limpiar buffer por seguridad + // Clear buffer for security pooled_buffer.buffer.fill(0); BorrowedBuffer { @@ -166,12 +166,12 @@ impl BufferPool { } } } else { - // No hay buffers disponibles, creamos uno nuevo + // No buffers available, create a new one let mut stats = self.stats.lock().await; stats.misses += 1; drop(stats); - // Liberar el lock antes de retornar + // Release the lock before returning drop(pool_locked); debug!("Buffer pool: creating new buffer"); @@ -185,9 +185,9 @@ impl BufferPool { } } - /// Retorna un buffer al pool + /// Returns a buffer to the pool async fn return_buffer(&self, mut buffer: Vec) { - // Si el buffer es del tamaño incorrecto, lo descartamos + // If the buffer is the wrong size, discard it if buffer.capacity() != self.buffer_size { debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})", buffer.capacity(), self.buffer_size); @@ -196,10 +196,10 @@ impl BufferPool { return; } - // Resize para asegurar capacidad correcta + // Resize to ensure correct capacity buffer.resize(self.buffer_size, 0); - // Añadir al pool + // Add to the pool let mut pool_locked = self.pool.lock().await; pool_locked.push_back(PooledBuffer { @@ -207,7 +207,7 @@ impl BufferPool { last_used: Instant::now(), }); - // Actualizar estadísticas + // Update statistics let mut stats = self.stats.lock().await; stats.returns += 1; @@ -217,24 +217,24 @@ impl BufferPool { self.limit.add_permits(1); } - /// Limpia buffers expirados del pool + /// Cleans expired buffers from the pool pub async fn clean_expired_buffers(&self) { let _now = Instant::now(); let mut pool_locked = self.pool.lock().await; - // Contar expirados + // Count expired let count_before = pool_locked.len(); - // Filtrar manteniendo solo los no expirados + // Filter keeping only non-expired pool_locked.retain(|buffer| { buffer.last_used.elapsed() <= self.buffer_ttl }); - // Contar cuántos se eliminaron + // Count how many were removed let removed = count_before - pool_locked.len(); if removed > 0 { - // Actualizar estadísticas + // Update statistics let mut stats = self.stats.lock().await; stats.evictions += removed; @@ -242,21 +242,21 @@ impl BufferPool { } } - /// Obtiene estadísticas actuales del pool + /// Gets current pool statistics pub async fn get_stats(&self) -> BufferPoolStats { self.stats.lock().await.clone() } - /// Inicia la tarea periódica de limpieza + /// Starts the periodic cleanup task pub fn start_cleaner(pool: Arc) { tokio::spawn(async move { - let interval = Duration::from_secs(30); // Limpiar cada 30 segundos + let interval = Duration::from_secs(30); // Clean every 30 seconds loop { tokio::time::sleep(interval).await; pool.clean_expired_buffers().await; - // Loguear estadísticas periódicamente + // Log statistics periodically let stats = pool.get_stats().await; debug!("Buffer pool stats: gets={}, hits={}, misses={}, hit_ratio={:.2}%, returns={}, \ evictions={}, max_reached={}, waits={}", @@ -286,31 +286,31 @@ impl Clone for BufferPool { } impl BorrowedBuffer { - /// Accede al buffer interno + /// Accesses the internal buffer pub fn as_mut_slice(&mut self) -> &mut [u8] { &mut self.buffer } - /// Obtiene una referencia a los datos utilizados + /// Gets a reference to the used data pub fn as_slice(&self) -> &[u8] { &self.buffer[..self.used_size] } - /// Establece cuántos bytes se utilizaron realmente + /// Sets how many bytes were actually used pub fn set_used(&mut self, size: usize) { self.used_size = min(size, self.buffer.len()); } - /// Convierte en un Vec que incluye solo los datos utilizados + /// Converts into a Vec that includes only the used data pub fn into_vec(mut self) -> Vec { - // Marcar para no devolver al pool + // Mark to not return to pool self.return_to_pool = false; - // Crear un nuevo vector solo con los datos utilizados + // Create a new vector with only the used data self.buffer[..self.used_size].to_vec() } - /// Copia datos a este buffer y actualiza el tamaño usado + /// Copies data to this buffer and updates the used size pub fn copy_from_slice(&mut self, data: &[u8]) -> usize { let copy_size = min(data.len(), self.buffer.len()); self.buffer[..copy_size].copy_from_slice(&data[..copy_size]); @@ -318,32 +318,32 @@ impl BorrowedBuffer { copy_size } - /// Impide que el buffer se devuelva al pool al destruirse + /// Prevents the buffer from being returned to the pool on destruction pub fn do_not_return(mut self) -> Self { self.return_to_pool = false; self } - /// Obtiene el tamaño total del buffer + /// Gets the total buffer size pub fn capacity(&self) -> usize { self.buffer.len() } - /// Obtiene el tamaño usado del buffer + /// Gets the used buffer size pub fn used_size(&self) -> usize { self.used_size } } -// Cuando se hace drop de un BorrowedBuffer, lo devuelve al pool +// When a BorrowedBuffer is dropped, it is returned to the pool impl Drop for BorrowedBuffer { fn drop(&mut self) { if self.return_to_pool { - // Tomar posesión del buffer y crear un clone del pool + // Take ownership of the buffer and create a clone of the pool let buffer = std::mem::take(&mut self.buffer); let pool = self.pool.clone(); - // Spawn del return para que el drop no bloquee + // Spawn the return so that drop doesn't block // return_buffer will release the semaphore permit tokio::spawn(async move { pool.return_buffer(buffer).await; @@ -361,39 +361,39 @@ mod tests { #[tokio::test] async fn test_buffer_pooling() { - // Crear pool pequeño para testing + // Create small pool for testing let pool = BufferPool::new(1024, 5, 60); - // Obtener un buffer + // Get a buffer let mut buffer1 = pool.get_buffer().await; buffer1.copy_from_slice(b"test data"); assert_eq!(buffer1.as_slice(), b"test data"); - // Obtener otro buffer + // Get another buffer let buffer2 = pool.get_buffer().await; - // Verificar stats + // Verify stats let stats = pool.get_stats().await; assert_eq!(stats.gets, 2); - assert_eq!(stats.hits, 0); // sin hits todavía - assert_eq!(stats.misses, 2); // todos son misses + assert_eq!(stats.hits, 0); // no hits yet + assert_eq!(stats.misses, 2); // all are misses - // Devolver buffer1 al pool (implícitamente por drop) + // Return buffer1 to pool (implicitly via drop) drop(buffer1); - // Permitir que el return asíncrono ocurra + // Allow the async return to occur tokio::time::sleep(Duration::from_millis(10)).await; - // Obtener otro buffer (debería reutilizar el retornado) + // Get another buffer (should reuse the returned one) let buffer3 = pool.get_buffer().await; - // Verificar stats actualizados + // Verify updated stats let stats = pool.get_stats().await; assert_eq!(stats.gets, 3); - assert_eq!(stats.hits, 1); // ahora debería haber un hit - assert_eq!(stats.returns, 1); // un buffer retornado + assert_eq!(stats.hits, 1); // now there should be a hit + assert_eq!(stats.returns, 1); // one buffer returned - // Limpiar + // Cleanup drop(buffer2); drop(buffer3); } @@ -402,19 +402,19 @@ mod tests { async fn test_buffer_operations() { let pool = BufferPool::new(1024, 10, 60); - // Obtener buffer + // Get buffer let mut buffer = pool.get_buffer().await; - // Escribir datos + // Write data buffer.copy_from_slice(b"Hello, world!"); assert_eq!(buffer.used_size(), 13); assert_eq!(buffer.as_slice(), b"Hello, world!"); - // Convertir a vec y verificar - let vec = buffer.into_vec(); // Esto impide retornar al pool + // Convert to vec and verify + let vec = buffer.into_vec(); // This prevents returning to pool assert_eq!(vec, b"Hello, world!"); - // Verificar que no se incrementan los returns (buffer no retornado) + // Verify that returns are not incremented (buffer not returned) tokio::time::sleep(Duration::from_millis(10)).await; let stats = pool.get_stats().await; assert_eq!(stats.returns, 0); @@ -422,76 +422,76 @@ mod tests { #[tokio::test] async fn test_pool_limit() { - // Pool con solo 3 buffers + // Pool with only 3 buffers let pool = BufferPool::new(1024, 3, 60); - // Obtener 3 buffers (alcanza el límite) + // Get 3 buffers (reaches the limit) let buffer1 = pool.get_buffer().await; let buffer2 = pool.get_buffer().await; let buffer3 = pool.get_buffer().await; - // Verificar stats + // Verify stats let stats = pool.get_stats().await; assert_eq!(stats.gets, 3); - assert_eq!(stats.waits, 0); // sin esperas todavía + assert_eq!(stats.waits, 0); // no waits yet - // Intentar obtener un 4º buffer en una tarea separada (debería esperar) + // Try to get a 4th buffer in a separate task (should wait) let pool_clone = pool.clone(); let handle = tokio::spawn(async move { let _buffer4 = pool_clone.get_buffer().await; true }); - // Dar tiempo para que la tarea intente tomar el buffer + // Give time for the task to try to take the buffer tokio::time::sleep(Duration::from_millis(50)).await; - // Verificar que hay una espera + // Verify there is a wait let stats = pool.get_stats().await; assert_eq!(stats.waits, 1); - // Liberar un buffer + // Release a buffer drop(buffer1); - // Dar tiempo para el retorno asíncrono y para que la tarea en espera obtenga su buffer + // Give time for the async return and for the waiting task to get its buffer tokio::time::sleep(Duration::from_millis(50)).await; - // Verificar que la tarea pudo continuar + // Verify the task was able to continue assert!(handle.await.unwrap()); - // Limpiar + // Cleanup drop(buffer2); drop(buffer3); } #[tokio::test] async fn test_ttl_expiration() { - // Pool con TTL muy corto para testing - let pool = BufferPool::new(1024, 5, 1); // 1 segundo TTL + // Pool with very short TTL for testing + let pool = BufferPool::new(1024, 5, 1); // 1 second TTL - // Obtener y devolver un buffer + // Get and return a buffer let buffer = pool.get_buffer().await; drop(buffer); - // Permitir que el return asíncrono ocurra + // Allow the async return to occur tokio::time::sleep(Duration::from_millis(50)).await; - // Verificar que hay un buffer en el pool + // Verify there is a buffer in the pool let stats = pool.get_stats().await; assert_eq!(stats.returns, 1); - // Esperar a que expire el TTL + // Wait for the TTL to expire tokio::time::sleep(Duration::from_secs(2)).await; - // Limpiar expirados + // Clean expired pool.clean_expired_buffers().await; - // Obtener otro buffer (debería ser un miss ya que el anterior expiró) + // Get another buffer (should be a miss since the previous one expired) let _buffer2 = pool.get_buffer().await; - // Verificar stats + // Verify stats let stats = pool.get_stats().await; - assert_eq!(stats.evictions, 1); // un buffer expirado - assert_eq!(stats.hits, 0); // sin hits (el buffer expiró) - assert_eq!(stats.misses, 2); // dos misses (1er y 3er get) + assert_eq!(stats.evictions, 1); // one expired buffer + assert_eq!(stats.hits, 0); // no hits (the buffer expired) + assert_eq!(stats.misses, 2); // two misses (1st and 3rd get) } } \ No newline at end of file diff --git a/src/infrastructure/services/compression_service.rs b/src/infrastructure/services/compression_service.rs index eb6f4d19..96a9f4cc 100644 --- a/src/infrastructure/services/compression_service.rs +++ b/src/infrastructure/services/compression_service.rs @@ -16,16 +16,16 @@ use crate::application::ports::compression_ports::{ use crate::domain::errors::DomainError; use crate::infrastructure::services::buffer_pool::BufferPool; -/// Nivel de compresión para ficheros +/// Compression level for files #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CompressionLevel { - /// Sin compresión (solo para transferencia) + /// No compression (transfer only) None = 0, - /// Compresión rápida con menor ratio + /// Fast compression with lower ratio Fast = 1, - /// Compresión balanceada (por defecto) + /// Balanced compression (default) Default = 6, - /// Compresión máxima (más lenta) + /// Maximum compression (slower) Best = 9, } @@ -40,49 +40,49 @@ impl From for Compression { } } -/// Umbral de tamaño para decidir si se comprime o no +/// Size threshold to decide whether to compress or not const COMPRESSION_SIZE_THRESHOLD: u64 = 1024 * 50; // 50KB -/// Interfaz para servicios de compresión +/// Interface for compression services #[async_trait] pub trait CompressionService: Send + Sync { - /// Comprime datos en memoria + /// Compresses data in memory async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result>; - /// Descomprime datos en memoria + /// Decompresses data in memory async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result>; - /// Comprime un stream de datos + /// Compresses a data stream fn compress_stream(&self, stream: S, level: CompressionLevel) -> impl Stream> + Send where S: Stream> + Send + 'static + Unpin; - /// Descomprime un stream de datos + /// Decompresses a data stream fn decompress_stream(&self, compressed_stream: S) -> impl Stream> + Send where S: Stream> + Send + 'static + Unpin; - /// Determina si un archivo debe ser comprimido basado en su tipo MIME y tamaño + /// Determines whether a file should be compressed based on its MIME type and size fn should_compress(&self, mime_type: &str, size: u64) -> bool; } -/// Implementación de servicios de compresión usando Gzip +/// Gzip compression service implementation pub struct GzipCompressionService { - /// Pool de buffers para optimización de memoria + /// Buffer pool for memory optimization buffer_pool: Option>, } impl GzipCompressionService { - /// Crea una nueva instancia del servicio + /// Creates a new service instance pub fn new() -> Self { Self { buffer_pool: None, } } - /// Crea una nueva instancia del servicio con buffer pool + /// Creates a new service instance with buffer pool pub fn new_with_buffer_pool(buffer_pool: Arc) -> Self { Self { buffer_pool: Some(buffer_pool), @@ -92,64 +92,64 @@ impl GzipCompressionService { #[async_trait] impl CompressionService for GzipCompressionService { - /// Comprime datos en memoria usando Gzip + /// Compresses data in memory using Gzip async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result> { - // Si tenemos un buffer pool, usar un buffer prestado para la compresión + // If we have a buffer pool, use a borrowed buffer for compression if let Some(pool) = &self.buffer_pool { - // Estimar el tamaño de la compresión (aproximadamente 80% del original para casos típicos) + // Estimate the compression size (approximately 80% of original for typical cases) let estimated_size = (data.len() as f64 * 0.8) as usize; - // Obtener un buffer del pool + // Get a buffer from the pool let buffer = pool.get_buffer().await; - // Comprobar si el buffer es suficientemente grande + // Check if the buffer is large enough if buffer.capacity() >= estimated_size { - // Ejecutar la compresión en un worker thread usando el buffer + // Run compression in a worker thread using the buffer let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer)); let buffer_clone = buffer_ptr.clone(); - // Comprimir datos - // Clonar los datos para evitar problemas de lifetime + // Compress data + // Clone the data to avoid lifetime issues let data_owned = data.to_vec(); let result = tokio::task::spawn_blocking(move || { let mut encoder = GzEncoderRead::new(&data_owned[..], level.into()); - // Intentar bloquear el mutex (no debería fallar ya que estamos en un hilo separado) + // Try to lock the mutex (should not fail since we are in a separate thread) let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) { buffer => buffer, }; - // Leer directamente en el buffer + // Read directly into the buffer let read_bytes = encoder.read(buffer_guard.as_mut_slice())?; buffer_guard.set_used(read_bytes); Ok(()) as io::Result<()> }).await; - // Verificar resultado + // Verify result match result { Ok(Ok(())) => { - // Obtener el buffer y convertirlo a Vec + // Get the buffer and convert it to Vec let buffer = buffer_ptr.lock().await; let cloned_buffer = buffer.clone(); - drop(buffer); // Liberar el mutex primero + drop(buffer); // Release the mutex first return Ok(cloned_buffer.into_vec()); }, Ok(Err(e)) => { - error!("Error en compresión con buffer pool: {}", e); - // Continuar con implementación estándar + error!("Compression error with buffer pool: {}", e); + // Fall back to standard implementation }, Err(e) => { - error!("Error en task de compresión con buffer pool: {}", e); - // Continuar con implementación estándar + error!("Compression task error with buffer pool: {}", e); + // Fall back to standard implementation } } } } - // Implementación estándar si no hay buffer pool o el buffer es insuficiente - // Clonar los datos para evitar problemas de lifetime + // Standard implementation if there is no buffer pool or the buffer is insufficient + // Clone the data to avoid lifetime issues let data_owned = data.to_vec(); tokio::task::spawn_blocking(move || { @@ -158,79 +158,79 @@ impl CompressionService for GzipCompressionService { encoder.read_to_end(&mut compressed)?; Ok(compressed) }).await.unwrap_or_else(|e| { - error!("Error en task de compresión: {}", e); + error!("Compression task error: {}", e); Err(io::Error::new(io::ErrorKind::Other, e.to_string())) }) } - /// Descomprime datos en memoria + /// Decompresses data in memory async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result> { - // Si tenemos un buffer pool, usar un buffer prestado para la descompresión + // If we have a buffer pool, use a borrowed buffer for decompression if let Some(pool) = &self.buffer_pool { - // Estimar el tamaño de la descompresión (aproximadamente 5x del comprimido para casos típicos) + // Estimate the decompression size (approximately 5x of compressed for typical cases) let estimated_size = compressed_data.len() * 5; - // Obtener un buffer del pool + // Get a buffer from the pool let buffer = pool.get_buffer().await; - // Comprobar si el buffer es suficientemente grande + // Check if the buffer is large enough if buffer.capacity() >= estimated_size { - // Clonar datos comprimidos para mover al worker + // Clone compressed data to move to the worker let data = compressed_data.to_vec(); let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer)); let buffer_clone = buffer_ptr.clone(); - // Descomprimir datos + // Decompress data let result = tokio::task::spawn_blocking(move || { let mut decoder = GzDecoder::new(&data[..]); - // Intentar bloquear el mutex + // Try to lock the mutex let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) { buffer => buffer, }; - // Leer directamente en el buffer + // Read directly into the buffer let read_bytes = decoder.read(buffer_guard.as_mut_slice())?; buffer_guard.set_used(read_bytes); Ok(()) as io::Result<()> }).await; - // Verificar resultado + // Verify result match result { Ok(Ok(())) => { - // Obtener el buffer y convertirlo a Vec + // Get the buffer and convert it to Vec let buffer = buffer_ptr.lock().await; let cloned_buffer = buffer.clone(); - drop(buffer); // Liberar el mutex primero + drop(buffer); // Release the mutex first return Ok(cloned_buffer.into_vec()); }, Ok(Err(e)) => { - error!("Error en descompresión con buffer pool: {}", e); - // Continuar con implementación estándar + error!("Decompression error with buffer pool: {}", e); + // Fall back to standard implementation }, Err(e) => { - error!("Error en task de descompresión con buffer pool: {}", e); - // Continuar con implementación estándar + error!("Decompression task error with buffer pool: {}", e); + // Fall back to standard implementation } } } } - // Implementación estándar si no hay buffer pool o el buffer es insuficiente - let data = compressed_data.to_vec(); // Clonar para mover al worker + // Standard implementation if there is no buffer pool or the buffer is insufficient + let data = compressed_data.to_vec(); // Clone to move to the worker tokio::task::spawn_blocking(move || { let mut decoder = GzDecoder::new(&data[..]); let mut decompressed = Vec::new(); decoder.read_to_end(&mut decompressed)?; Ok(decompressed) }).await.unwrap_or_else(|e| { - error!("Error en task de descompresión: {}", e); + error!("Decompression task error: {}", e); Err(io::Error::new(io::ErrorKind::Other, e.to_string())) }) } - /// Comprime un stream de bytes + /// Compresses a byte stream fn compress_stream(&self, stream: S, level: CompressionLevel) -> impl Stream> + Send where @@ -271,7 +271,7 @@ impl CompressionService for GzipCompressionService { }) } - /// Descomprime un stream de bytes + /// Decompresses a byte stream fn decompress_stream(&self, compressed_stream: S) -> impl Stream> + Send where @@ -310,14 +310,14 @@ impl CompressionService for GzipCompressionService { }) } - /// Determina si un archivo debe ser comprimido basado en su tipo MIME y tamaño + /// Determines whether a file should be compressed based on its MIME type and size fn should_compress(&self, mime_type: &str, size: u64) -> bool { - // No comprimir archivos muy pequeños (overhead) + // Do not compress very small files (overhead) if size < COMPRESSION_SIZE_THRESHOLD { return false; } - // No comprimir archivos ya comprimidos + // Do not compress already compressed files if mime_type.starts_with("image/") && !mime_type.contains("svg") && !mime_type.contains("bmp") { @@ -345,7 +345,7 @@ impl CompressionService for GzipCompressionService { return false; } - // Comprimir archivos de texto, documentos, y otros tipos compresibles + // Compress text files, documents, and other compressible types true } } @@ -389,19 +389,19 @@ mod tests { async fn test_compress_decompress_data() { let service = GzipCompressionService::new(); - // Datos de prueba + // Test data let data = "Hello, world! ".repeat(1000).into_bytes(); - // Comprimir + // Compress let compressed = CompressionService::compress_data(&service, &data, CompressionLevel::Default).await.unwrap(); - // Verificar que la compresión reduce el tamaño + // Verify that compression reduces the size assert!(compressed.len() < data.len()); - // Descomprimir + // Decompress let decompressed = CompressionService::decompress_data(&service, &compressed).await.unwrap(); - // Verificar que los datos originales se recuperan correctamente + // Verify that the original data is recovered correctly assert_eq!(decompressed, data); } @@ -409,30 +409,30 @@ mod tests { async fn test_compress_decompress_stream() { let service = GzipCompressionService::new(); - // Crear datos de prueba + // Create test data let chunks = vec![ Ok(Bytes::from("Hello, ")), Ok(Bytes::from("world! ")), Ok(Bytes::from("This is a test of streaming compression.")), ]; - // Convertir a stream + // Convert to stream let input_stream = futures::stream::iter(chunks); - // Comprimir el stream + // Compress the stream let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default); - // Recolectar los bytes comprimidos + // Collect the compressed bytes let compressed_bytes = compressed_stream .try_fold(Vec::new(), |mut acc, chunk| async move { acc.extend_from_slice(&chunk); Ok(acc) }).await.unwrap(); - // Descomprimir los datos + // Decompress the data let decompressed = CompressionService::decompress_data(&service, &compressed_bytes).await.unwrap(); - // Verificar resultado + // Verify result let expected = "Hello, world! This is a test of streaming compression."; assert_eq!(String::from_utf8(decompressed).unwrap(), expected); } @@ -441,17 +441,17 @@ mod tests { fn test_should_compress() { let service = GzipCompressionService::new(); - // Casos que no deberían comprimirse + // Cases that should not be compressed assert!(!CompressionService::should_compress(&service, "image/jpeg", 100 * 1024)); assert!(!CompressionService::should_compress(&service, "video/mp4", 10 * 1024 * 1024)); assert!(!CompressionService::should_compress(&service, "application/zip", 5 * 1024 * 1024)); - // Casos que sí deberían comprimirse + // Cases that should be compressed assert!(CompressionService::should_compress(&service, "text/html", 100 * 1024)); assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024)); assert!(CompressionService::should_compress(&service, "text/plain", 1024 * 1024)); - // Archivos pequeños no deberían comprimirse independientemente del tipo + // Small files should not be compressed regardless of type assert!(!CompressionService::should_compress(&service, "text/html", 10 * 1024)); } } \ No newline at end of file diff --git a/src/infrastructure/services/file_metadata_cache.rs b/src/infrastructure/services/file_metadata_cache.rs index 421cc495..0bab8897 100644 --- a/src/infrastructure/services/file_metadata_cache.rs +++ b/src/infrastructure/services/file_metadata_cache.rs @@ -13,61 +13,61 @@ use crate::domain::entities::file::File; use crate::common::config::AppConfig; -/// Tipos de entradas en caché +/// Cache entry types #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CacheEntryType { - /// Archivo + /// File File, - /// Directorio + /// Directory Directory, - /// Tipo desconocido + /// Unknown type Unknown, } -/// Estadísticas de caché para monitoreo +/// Cache statistics for monitoring #[derive(Debug, Clone, Default)] pub struct CacheStats { - /// Número de hits en caché + /// Number of cache hits pub hits: usize, - /// Número de misses en caché + /// Number of cache misses pub misses: usize, - /// Número de invalidaciones manuales + /// Number of manual invalidations pub invalidations: usize, - /// Número de expiraciones automáticas + /// Number of automatic expirations pub expirations: usize, - /// Número de inserciones en caché + /// Number of cache inserts pub inserts: usize, - /// Tiempo total ahorrado (milisegundos) + /// Total time saved (milliseconds) pub time_saved_ms: u64, } -/// Metadatos completos de archivo en caché +/// Complete cached file metadata #[derive(Debug, Clone)] pub struct FileMetadata { - /// Ruta absoluta del archivo + /// Absolute file path pub path: PathBuf, - /// Si el archivo existe físicamente + /// Whether the file physically exists pub exists: bool, - /// Tipo de entrada (archivo, directorio) + /// Entry type (file, directory) pub entry_type: CacheEntryType, - /// Tamaño en bytes (para archivos) + /// Size in bytes (for files) pub size: Option, - /// Tipo MIME (para archivos) + /// MIME type (for files) pub mime_type: Option, - /// Timestamp de creación (UNIX epoch seconds) + /// Creation timestamp (UNIX epoch seconds) pub created_at: Option, - /// Timestamp de modificación (UNIX epoch seconds) + /// Modification timestamp (UNIX epoch seconds) pub modified_at: Option, - /// Acceso previo (usado para LRU) + /// Previous access (used for LRU) pub last_access: Instant, - /// Tiempo de expiración de la caché + /// Cache expiration time pub expires_at: Instant, - /// Número de accesos a esta entrada + /// Number of accesses to this entry pub access_count: usize, } impl FileMetadata { - /// Crea una nueva entrada de metadatos + /// Creates a new metadata entry pub fn new( path: PathBuf, exists: bool, @@ -94,56 +94,56 @@ impl FileMetadata { } } - /// Actualiza el tiempo de último acceso + /// Updates the last access time pub fn touch(&mut self) { self.last_access = Instant::now(); self.access_count += 1; } - /// Verifica si la entrada ha expirado + /// Checks if the entry has expired pub fn is_expired(&self) -> bool { Instant::now() > self.expires_at } - /// Actualiza el tiempo de expiración con un nuevo TTL + /// Updates the expiration time with a new TTL pub fn update_expiry(&mut self, ttl: Duration) { self.expires_at = Instant::now() + ttl; } } -/// Caché avanzada de metadatos de archivos +/// Advanced file metadata cache pub struct FileMetadataCache { - /// Caché principal de metadatos + /// Main metadata cache metadata_cache: RwLock>, - /// Cola LRU para administración de caché + /// LRU queue for cache management lru_queue: RwLock>, - /// Estadísticas de uso del caché + /// Cache usage statistics stats: RwLock, - /// Configuración global de la aplicación + /// Global application configuration config: AppConfig, - /// TTL adaptativo para entradas populares + /// Adaptive TTL for popular entries ttl_multiplier: f64, - /// Umbral de popularidad para TTL extendido + /// Popularity threshold for extended TTL popularity_threshold: usize, - /// Tamaño máximo de caché + /// Maximum cache size max_entries: usize, } impl FileMetadataCache { - /// Crea una nueva instancia de caché de metadatos + /// Creates a new metadata cache instance pub fn new(config: AppConfig, max_entries: usize) -> Self { Self { metadata_cache: RwLock::new(HashMap::with_capacity(max_entries)), lru_queue: RwLock::new(VecDeque::with_capacity(max_entries)), stats: RwLock::new(CacheStats::default()), config, - ttl_multiplier: 5.0, // Entradas populares tienen 5x TTL - popularity_threshold: 10, // Después de 10 accesos se considera popular + ttl_multiplier: 5.0, // Popular entries have 5x TTL + popularity_threshold: 10, // After 10 accesses it's considered popular max_entries, } } - /// Crea un objeto FileMetadata a partir de un objeto File + /// Creates a FileMetadata object from a File object pub fn create_metadata_from_file(file: &File, abs_path: PathBuf) -> FileMetadata { let entry_type = CacheEntryType::File; let size = Some(file.size()); @@ -151,8 +151,8 @@ impl FileMetadataCache { let created_at = Some(file.created_at()); let modified_at = Some(file.modified_at()); - // Usar un TTL estándar - let ttl = Duration::from_secs(60); // 1 minuto + // Use a standard TTL + let ttl = Duration::from_secs(60); // 1 minute FileMetadata::new( abs_path, @@ -166,28 +166,28 @@ impl FileMetadataCache { ) } - /// Crea una instancia por defecto + /// Creates a default instance pub fn default() -> Self { Self::new(AppConfig::default(), 10_000) } - /// Crea una instancia de caché con configuración por defecto + /// Creates a cache instance with default configuration pub fn default_with_config(config: AppConfig) -> Self { - Self::new(config, 50_000) // Caché más grande para sistema en producción + Self::new(config, 50_000) // Larger cache for production system } - /// Obtiene los metadatos de un archivo si están en caché + /// Gets file metadata if cached pub async fn get_metadata(&self, path: &Path) -> Option { let start_time = Instant::now(); let mut cache = self.metadata_cache.write().await; if let Some(metadata) = cache.get_mut(path) { - // Verificar si ha expirado + // Check if expired if metadata.is_expired() { - // Eliminar de caché si expiró + // Remove from cache if expired cache.remove(path); - // Actualizar estadísticas + // Update statistics let mut stats = self.stats.write().await; stats.misses += 1; stats.expirations += 1; @@ -197,10 +197,10 @@ impl FileMetadataCache { return None; } - // Actualizar tiempo de acceso + // Update access time metadata.touch(); - // Para entradas populares, extender TTL + // For popular entries, extend TTL if metadata.access_count >= self.popularity_threshold { let new_ttl = match metadata.entry_type { CacheEntryType::File => Duration::from_millis( @@ -209,33 +209,33 @@ impl FileMetadataCache { CacheEntryType::Directory => Duration::from_millis( (self.config.timeouts.dir_operation_ms as f64 * self.ttl_multiplier) as u64 ), - _ => Duration::from_secs(60), // 1 minuto por defecto + _ => Duration::from_secs(60), // 1 minute by default }; metadata.update_expiry(new_ttl); debug!("Extended TTL for popular entry: {}", path.display()); } - // Calcular tiempo ahorrado aproximado + // Calculate approximate time saved let elapsed = start_time.elapsed().as_millis() as u64; - let estimated_io_time: u64 = 10; // Asumimos 10ms mínimo para operación de IO + let estimated_io_time: u64 = 10; // We assume 10ms minimum for IO operation let time_saved = estimated_io_time.saturating_sub(elapsed); - // Actualizar estadísticas + // Update statistics let mut stats = self.stats.write().await; stats.hits += 1; stats.time_saved_ms += time_saved; debug!("Cache hit for: {}", path.display()); - // Mantener también la cola LRU actualizada + // Also keep the LRU queue updated self.update_lru(path.to_path_buf()).await; - // Clonar para retornar + // Clone to return return Some(metadata.clone()); } - // No encontrado en caché + // Not found in cache let mut stats = self.stats.write().await; stats.misses += 1; @@ -243,20 +243,20 @@ impl FileMetadataCache { None } - /// Actualiza la cola LRU + /// Updates the LRU queue async fn update_lru(&self, path: PathBuf) { let mut lru = self.lru_queue.write().await; - // Eliminar si ya existe + // Remove if already exists if let Some(pos) = lru.iter().position(|p| p == &path) { lru.remove(pos); } - // Agregar al final (más reciente) + // Add to the end (most recent) lru.push_back(path); } - /// Verifica si un archivo existe + /// Checks if a file exists pub async fn exists(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return Some(metadata.exists); @@ -265,7 +265,7 @@ impl FileMetadataCache { None } - /// Verifica si un path es un directorio + /// Checks if a path is a directory pub async fn is_dir(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return Some(metadata.entry_type == CacheEntryType::Directory); @@ -274,7 +274,7 @@ impl FileMetadataCache { None } - /// Verifica si un path es un archivo + /// Checks if a path is a file pub async fn is_file(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return Some(metadata.entry_type == CacheEntryType::File); @@ -283,7 +283,7 @@ impl FileMetadataCache { None } - /// Obtiene el tamaño de un archivo + /// Gets the size of a file pub async fn get_size(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return metadata.size; @@ -292,7 +292,7 @@ impl FileMetadataCache { None } - /// Obtiene el tipo MIME de un archivo + /// Gets the MIME type of a file pub async fn get_mime_type(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return metadata.mime_type; @@ -301,12 +301,12 @@ impl FileMetadataCache { None } - /// Refresca los metadatos de un path + /// Refreshes metadata for a path pub async fn refresh_metadata(&self, path: &Path) -> Result { - // Realizar lectura real del sistema de archivos + // Perform actual filesystem read let metadata = fs::metadata(path).await?; - // Determinar tipo de entrada + // Determine entry type let entry_type = if metadata.is_dir() { CacheEntryType::Directory } else if metadata.is_file() { @@ -315,21 +315,21 @@ impl FileMetadataCache { CacheEntryType::Unknown }; - // Obtener tamaño para archivos + // Get size for files let size = if metadata.is_file() { Some(metadata.len()) } else { None }; - // Obtener tipo MIME para archivos + // Get MIME type for files let mime_type = if metadata.is_file() { Some(from_path(path).first_or_octet_stream().to_string()) } else { None }; - // Obtener timestamps + // Get timestamps let created_at = metadata.created() .map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()) .ok(); @@ -338,14 +338,14 @@ impl FileMetadataCache { .map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()) .ok(); - // Determinar TTL apropiado + // Determine appropriate TTL let ttl = if metadata.is_dir() { Duration::from_millis(self.config.timeouts.dir_operation_ms) } else { Duration::from_millis(self.config.timeouts.file_operation_ms) }; - // Crear entrada de metadatos + // Create metadata entry let file_metadata = FileMetadata::new( path.to_path_buf(), true, @@ -357,34 +357,34 @@ impl FileMetadataCache { ttl, ); - // Actualizar caché + // Update cache self.update_cache(file_metadata.clone()).await; Ok(file_metadata) } - /// Actualiza la caché con nuevos metadatos + /// Updates the cache with new metadata pub async fn update_cache(&self, metadata: FileMetadata) { - // Evitar caché llena antes de insertar + // Avoid full cache before inserting self.ensure_capacity().await; let path = metadata.path.clone(); - // Insertar en caché + // Insert into cache { let mut cache = self.metadata_cache.write().await; cache.insert(path.clone(), metadata); - // Actualizar estadísticas + // Update statistics let mut stats = self.stats.write().await; stats.inserts += 1; } - // Actualizar la cola LRU + // Update the LRU queue self.update_lru(path).await; } - /// Asegura que hay espacio en la caché + /// Ensures there is space in the cache async fn ensure_capacity(&self) { let cache_size = { let cache = self.metadata_cache.read().await; @@ -392,15 +392,15 @@ impl FileMetadataCache { }; if cache_size >= self.max_entries { - self.evict_lru_entries(cache_size / 10).await; // Liberar 10% + self.evict_lru_entries(cache_size / 10).await; // Free up 10% } } - /// Elimina entradas menos recientemente usadas + /// Removes least recently used entries async fn evict_lru_entries(&self, count: usize) { let mut paths_to_remove = Vec::with_capacity(count); - // Obtener entries a eliminar de la cola LRU + // Get entries to remove from the LRU queue { let mut lru = self.lru_queue.write().await; for _ in 0..count { @@ -412,7 +412,7 @@ impl FileMetadataCache { } } - // Eliminar de la caché principal + // Remove from the main cache { let mut cache = self.metadata_cache.write().await; for path in paths_to_remove { @@ -423,19 +423,19 @@ impl FileMetadataCache { debug!("Evicted {} LRU entries from cache", count); } - /// Invalidar una entrada específica de caché + /// Invalidate a specific cache entry pub async fn invalidate(&self, path: &Path) { - // Eliminar de la caché principal + // Remove from the main cache { let mut cache = self.metadata_cache.write().await; cache.remove(path); - // Actualizar estadísticas + // Update statistics let mut stats = self.stats.write().await; stats.invalidations += 1; } - // Eliminar de la cola LRU + // Remove from the LRU queue let path_buf = path.to_path_buf(); { let mut lru = self.lru_queue.write().await; @@ -447,12 +447,12 @@ impl FileMetadataCache { debug!("Invalidated cache entry for: {}", path.display()); } - /// Invalidar recursivamente entradas bajo un directorio + /// Recursively invalidate entries under a directory pub async fn invalidate_directory(&self, dir_path: &Path) { let dir_str = dir_path.to_string_lossy().to_string(); let mut paths_to_remove = Vec::new(); - // Encontrar todos los paths que comienzan con el directorio + // Find all paths that start with the directory { let cache = self.metadata_cache.read().await; for path in cache.keys() { @@ -463,13 +463,13 @@ impl FileMetadataCache { } } - // Actualizar estadísticas + // Update statistics { let mut stats = self.stats.write().await; stats.invalidations += paths_to_remove.len(); } - // Eliminar cada path encontrado + // Remove each found path for path in paths_to_remove { self.invalidate(&path).await; } @@ -477,18 +477,18 @@ impl FileMetadataCache { debug!("Invalidated directory and contents: {}", dir_path.display()); } - /// Obtener estadísticas actuales de la caché + /// Get current cache statistics pub async fn get_stats(&self) -> CacheStats { let stats = self.stats.read().await; stats.clone() } - /// Limpia todas las entradas expiradas de la caché + /// Clears all expired entries from the cache pub async fn clear_expired(&self) { let now = Instant::now(); let mut paths_to_remove = Vec::new(); - // Encontrar entradas expiradas + // Find expired entries { let cache = self.metadata_cache.read().await; for (path, metadata) in cache.iter() { @@ -498,16 +498,16 @@ impl FileMetadataCache { } } - // Actualizar estadísticas + // Update statistics { let mut stats = self.stats.write().await; stats.expirations += paths_to_remove.len(); } - // Guardar la cantidad de entradas para el logging + // Save the number of entries for logging let num_paths = paths_to_remove.len(); - // Eliminar entradas expiradas + // Remove expired entries for path in paths_to_remove { self.invalidate(&path).await; } @@ -515,19 +515,19 @@ impl FileMetadataCache { debug!("Cleared {} expired entries from cache", num_paths); } - /// Inicia el proceso de limpieza periódica + /// Starts the periodic cleanup process pub fn start_cleanup_task(cache: Arc) -> BoxFuture<'static, ()> { Box::pin(async move { - let cleanup_interval = Duration::from_secs(60); // Cada minuto + let cleanup_interval = Duration::from_secs(60); // Every minute loop { - // Esperar el intervalo + // Wait for the interval time::sleep(cleanup_interval).await; - // Limpiar entradas expiradas + // Clean expired entries cache.clear_expired().await; - // Registrar estadísticas + // Log statistics let stats = cache.get_stats().await; let cache_size = { let cache_map = cache.metadata_cache.read().await; @@ -550,12 +550,12 @@ impl FileMetadataCache { }) } - /// Precarga metadatos de directorios completos (útil para inicialización) + /// Preloads metadata for entire directories (useful for initialization) pub async fn preload_directory(&self, dir_path: &Path, recursive: bool, max_depth: usize) -> Result { self._preload_directory_internal(dir_path, recursive, max_depth, 0).await } - /// Implementación interna de precarga con seguimiento de profundidad + /// Internal preload implementation with depth tracking async fn _preload_directory_internal( &self, dir_path: &Path, @@ -568,20 +568,20 @@ impl FileMetadataCache { return Ok(0); } - // Obtener entradas del directorio + // Get directory entries let mut entries = fs::read_dir(dir_path).await?; let mut count = 0; - // Procesar cada entrada + // Process each entry while let Some(entry) = entries.next_entry().await? { let path = entry.path(); let metadata = fs::metadata(&path).await?; - // Refrescar metadatos de esta entrada + // Refresh metadata for this entry self.refresh_metadata(&path).await?; count += 1; - // Recursivamente procesar subdirectorios si es necesario + // Recursively process subdirectories if needed if recursive && metadata.is_dir() { // Box to break recursion count += self._preload_directory_internal( @@ -657,37 +657,37 @@ mod tests { #[tokio::test] async fn test_cache_operations() { - // Crear directorio temporal para pruebas + // Create temporary directory for tests let temp_dir = tempdir().unwrap(); let file_path = temp_dir.path().join("test_file.txt"); - // Crear un archivo de prueba + // Create a test file let mut file = File::create(&file_path).await.unwrap(); file.write_all(b"test content").await.unwrap(); file.flush().await.unwrap(); drop(file); - // Crear caché + // Create cache let config = AppConfig::default(); let cache = FileMetadataCache::new(config, 1000); - // Verificar miss inicial + // Verify initial miss assert!(cache.exists(&file_path).await.is_none()); - // Refrescar y verificar hit + // Refresh and verify hit let metadata = cache.refresh_metadata(&file_path).await.unwrap(); assert_eq!(metadata.entry_type, CacheEntryType::File); assert_eq!(metadata.size, Some(12)); // "test content" = 12 bytes - // Verificar que ahora existe en caché + // Verify it now exists in cache assert_eq!(cache.exists(&file_path).await, Some(true)); assert_eq!(cache.is_file(&file_path).await, Some(true)); - // Invalidar y verificar que ya no existe en caché + // Invalidate and verify it no longer exists in cache cache.invalidate(&file_path).await; assert!(cache.exists(&file_path).await.is_none()); - // Verificar estadísticas + // Verify statistics let stats = cache.get_stats().await; assert_eq!(stats.inserts, 1); assert_eq!(stats.invalidations, 1); @@ -696,7 +696,7 @@ mod tests { #[tokio::test] async fn test_directory_operations() { - // Crear estructura de directorios para pruebas + // Create directory structure for tests let temp_dir = tempdir().unwrap(); // Canonicalize to handle macOS /var -> /private/var symlinks let base_path = temp_dir.path().canonicalize().unwrap(); @@ -709,24 +709,24 @@ mod tests { File::create(&file1).await.unwrap(); File::create(&file2).await.unwrap(); - // Crear caché + // Create cache let config = AppConfig::default(); let cache = FileMetadataCache::new(config, 1000); - // Precargar directorio recursivamente + // Preload directory recursively // preload_directory caches the *contents* of the directory, not the root itself let count = cache.preload_directory(&base_path, true, 2).await.unwrap(); assert_eq!(count, 3); // subdir, file1, file2 - // Verificar existencia en caché (solo contenido, no la raíz) + // Verify existence in cache (only contents, not the root) assert_eq!(cache.is_dir(&sub_dir).await, Some(true)); assert_eq!(cache.is_file(&file1).await, Some(true)); assert_eq!(cache.is_file(&file2).await, Some(true)); - // Invalidar directorio y contenido + // Invalidate directory and contents cache.invalidate_directory(&base_path).await; - // Verificar que nada existe en caché + // Verify nothing exists in cache assert!(cache.exists(&sub_dir).await.is_none()); assert!(cache.exists(&file1).await.is_none()); assert!(cache.exists(&file2).await.is_none()); diff --git a/src/infrastructure/services/id_mapping_optimizer.rs b/src/infrastructure/services/id_mapping_optimizer.rs index 5713111a..cecea106 100644 --- a/src/infrastructure/services/id_mapping_optimizer.rs +++ b/src/infrastructure/services/id_mapping_optimizer.rs @@ -179,7 +179,7 @@ impl IdMappingOptimizer { { let cache = self.path_to_id_cache.read().await; if let Some((id, _)) = cache.get(&path_str) { - // Actualizar estadísticas + // Update statistics { let mut stats = self.stats.write().await; stats.get_id_hits += 1; @@ -189,19 +189,19 @@ impl IdMappingOptimizer { } } - // Si no está en caché, agregar a la cola de batch + // If not in cache, add to batch queue { let mut batch_queue = self.pending_batch.lock().await; batch_queue.path_to_id_requests.insert(path_str); } - // No encontrado en caché, debe procesarse en batch + // Not found in cache, must be processed in batch Ok(None) } - /// Procesa las solicitudes pendientes en batch + /// Processes pending requests in batch async fn process_batch(&self) -> Result { - // Adquirir permiso para operación batch + // Acquire permit for batch operation let _permit = self.batch_limiter.acquire().await.unwrap(); // Get pending requests @@ -214,13 +214,13 @@ impl IdMappingOptimizer { (paths, ids) }; - // Crear resultados + // Create results let mut result = BatchResult { path_to_id: HashMap::with_capacity(path_requests.len()), id_to_path: HashMap::with_capacity(id_requests.len()), }; - // Procesar solicitudes path->id en batch + // Process path->id requests in batch for path_str in path_requests { let path = StoragePath::from_string(&path_str); match self.base_service.get_or_create_id(&path).await { @@ -230,12 +230,12 @@ impl IdMappingOptimizer { }, Err(e) => { error!("Error batch-processing path {}: {}", path_str, e); - // Continuar con las demás solicitudes + // Continue with remaining requests } } } - // Procesar solicitudes id->path en batch + // Process id->path requests in batch for id in id_requests { match self.base_service.get_path_by_id(&id).await { Ok(path) => { @@ -245,7 +245,7 @@ impl IdMappingOptimizer { }, Err(e) => { error!("Error batch-processing ID {}: {}", id, e); - // Continuar con las demás solicitudes + // Continue with remaining requests } } } @@ -266,14 +266,14 @@ impl IdMappingOptimizer { } } - // Actualizar estadísticas + // Update statistics { let mut stats = self.stats.write().await; stats.batch_operations += 1; stats.batch_items_processed += result.path_to_id.len() + result.id_to_path.len(); } - // Guardar los cambios al disco en segundo plano + // Save changes to disk in the background let service_clone = self.base_service.clone(); tokio::spawn(async move { if let Err(e) = service_clone.save_pending_changes().await { @@ -284,15 +284,15 @@ impl IdMappingOptimizer { Ok(result) } - /// Fuerza el procesamiento de solicitudes pendientes si hay suficientes + /// Forces processing of pending requests if there are enough async fn trigger_batch_if_needed(&self, min_batch_size: usize) -> Result<(), IdMappingError> { - // Verificar si hay suficientes solicitudes pendientes + // Check if there are enough pending requests let should_process = { let batch_queue = self.pending_batch.lock().await; batch_queue.path_to_id_requests.len() + batch_queue.id_to_path_requests.len() >= min_batch_size }; - // Procesar si es necesario + // Process if necessary if should_process { self.process_batch().await?; } @@ -300,17 +300,17 @@ impl IdMappingOptimizer { Ok(()) } - /// Precargar un conjunto de rutas para obtener sus IDs en batch + /// Preload a set of paths to get their IDs in batch pub async fn preload_paths(&self, paths: Vec) -> Result<(), IdMappingError> { - // Solo proceder si hay rutas para cargar + // Only proceed if there are paths to load if paths.is_empty() { return Ok(()); } - // Rutas que debemos cargar (las que no están en caché) + // Paths we need to load (those not in cache) let mut paths_to_load = Vec::new(); - // Verificar primero el caché + // Check cache first { let cache = self.path_to_id_cache.read().await; for path in paths { @@ -321,12 +321,12 @@ impl IdMappingOptimizer { } } - // Si todos estaban en caché, terminar + // If all were in cache, finish if paths_to_load.is_empty() { return Ok(()); } - // Agregar rutas a la cola para procesamiento batch + // Add paths to queue for batch processing { let mut batch_queue = self.pending_batch.lock().await; for path in paths_to_load { @@ -334,23 +334,23 @@ impl IdMappingOptimizer { } } - // Ejecutar procesamiento batch inmediatamente + // Execute batch processing immediately self.process_batch().await?; Ok(()) } - /// Precargar un conjunto de IDs para obtener sus rutas en batch + /// Preload a set of IDs to get their paths in batch pub async fn preload_ids(&self, ids: Vec) -> Result<(), IdMappingError> { - // Solo proceder si hay IDs para cargar + // Only proceed if there are IDs to load if ids.is_empty() { return Ok(()); } - // IDs que debemos cargar (los que no están en caché) + // IDs we need to load (those not in cache) let mut ids_to_load = Vec::new(); - // Verificar primero el caché + // Check cache first { let cache = self.id_to_path_cache.read().await; for id in ids { @@ -360,12 +360,12 @@ impl IdMappingOptimizer { } } - // Si todos estaban en caché, terminar + // If all were in cache, finish if ids_to_load.is_empty() { return Ok(()); } - // Agregar IDs a la cola para procesamiento batch + // Add IDs to queue for batch processing { let mut batch_queue = self.pending_batch.lock().await; for id in ids_to_load { @@ -373,7 +373,7 @@ impl IdMappingOptimizer { } } - // Ejecutar procesamiento batch inmediatamente + // Execute batch processing immediately self.process_batch().await?; Ok(()) @@ -383,7 +383,7 @@ impl IdMappingOptimizer { #[async_trait] impl IdMappingPort for IdMappingOptimizer { async fn get_or_create_id(&self, path: &StoragePath) -> Result { - // Actualizar estadísticas + // Update statistics { let mut stats = self.stats.write().await; stats.get_id_queries += 1; @@ -391,7 +391,7 @@ impl IdMappingPort for IdMappingOptimizer { let path_str = path.to_string(); - // Verificar primero en el caché + // Check cache first { let cache = self.path_to_id_cache.read().await; if let Some((id, _)) = cache.get(&path_str) { @@ -443,7 +443,7 @@ impl IdMappingPort for IdMappingOptimizer { } async fn get_path_by_id(&self, id: &str) -> Result { - // Actualizar estadísticas + // Update statistics { let mut stats = self.stats.write().await; stats.path_by_id_queries += 1; @@ -493,21 +493,21 @@ impl IdMappingPort for IdMappingOptimizer { } async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> { - // Invalidar caché para este ID + // Invalidate cache for this ID { let mut id_cache = self.id_to_path_cache.write().await; let mut path_cache = self.path_to_id_cache.write().await; - // Eliminar la entrada del ID + // Remove the ID entry if let Some((old_path, _)) = id_cache.remove(id) { path_cache.remove(&old_path); } } - // Actualizar en el servicio base + // Update in the base service let result = self.base_service.update_path(id, new_path).await?; - // Actualizar caché con el nuevo mapeo + // Update cache with new mapping { let mut id_cache = self.id_to_path_cache.write().await; let mut path_cache = self.path_to_id_cache.write().await; @@ -523,25 +523,25 @@ impl IdMappingPort for IdMappingOptimizer { } async fn remove_id(&self, id: &str) -> Result<(), DomainError> { - // Invalidar caché para este ID + // Invalidate cache for this ID { let mut id_cache = self.id_to_path_cache.write().await; let mut path_cache = self.path_to_id_cache.write().await; - // Eliminar la entrada del ID + // Remove the ID entry if let Some((path, _)) = id_cache.remove(id) { path_cache.remove(&path); } } - // Eliminar en el servicio base + // Remove from the base service self.base_service.remove_id(id).await?; Ok(()) } async fn save_changes(&self) -> Result<(), DomainError> { - // Delegar al servicio base + // Delegate to the base service self.base_service.save_changes().await?; Ok(()) @@ -569,15 +569,15 @@ mod tests { let path = StoragePath::from_string("/test/file.txt"); - // Primera llamada debería usar el servicio base + // First call should use the base service let id = optimizer.get_or_create_id(&path).await.unwrap(); assert!(!id.is_empty(), "ID should not be empty"); - // Segunda llamada debería usar caché + // Second call should use cache let id2 = optimizer.get_or_create_id(&path).await.unwrap(); assert_eq!(id, id2, "Same path should return same ID"); - // Verificar estadísticas de caché + // Verify cache statistics let stats = optimizer.get_stats().await; assert_eq!(stats.get_id_queries, 2, "Should have 2 queries"); assert_eq!(stats.get_id_hits, 1, "Should have 1 hit"); @@ -587,27 +587,27 @@ mod tests { async fn test_batch_processing() { let (_, optimizer) = create_test_service().await; - // Crear un lote de rutas + // Create a batch of paths let mut paths = Vec::new(); for i in 0..50 { paths.push(StoragePath::from_string(&format!("/test/batch/file{}.txt", i))); } - // Precargar las rutas + // Preload the paths optimizer.preload_paths(paths.clone()).await.unwrap(); - // Verificar que todas están en caché + // Verify all are in cache for path in &paths { let id = optimizer.get_or_create_id(path).await.unwrap(); assert!(!id.is_empty(), "ID should be available for path"); } - // Verificar estadísticas + // Verify statistics let stats = optimizer.get_stats().await; assert_eq!(stats.batch_operations, 1, "Should have 1 batch operation"); assert!(stats.batch_items_processed >= 50, "Should have processed at least 50 items"); - // Verificar que todas las consultas posteriores son hits en caché + // Verify all subsequent queries are cache hits assert_eq!(stats.get_id_hits, 50, "All subsequente queries should be cache hits"); } @@ -615,21 +615,21 @@ mod tests { async fn test_cache_cleanup() { let (_, optimizer) = create_test_service().await; - // Crear algunas entradas + // Create some entries let path = StoragePath::from_string("/test/cleanup.txt"); let id = optimizer.get_or_create_id(&path).await.unwrap(); - // Verificar estadísticas iniciales + // Verify initial statistics { let stats = optimizer.get_stats().await; assert_eq!(stats.get_id_queries, 1, "Should have 1 query"); assert_eq!(stats.get_id_hits, 0, "Should have 0 hits"); } - // Ejecutar limpieza (no debería eliminar nada todavía) + // Run cleanup (should not remove anything yet) optimizer.cleanup_cache().await; - // Verificar que el caché sigue funcionando + // Verify cache is still working let id2 = optimizer.get_or_create_id(&path).await.unwrap(); assert_eq!(id, id2, "Cache should still work after cleanup"); diff --git a/src/infrastructure/services/id_mapping_service.rs b/src/infrastructure/services/id_mapping_service.rs index 4ecdbaea..eb0ea812 100644 --- a/src/infrastructure/services/id_mapping_service.rs +++ b/src/infrastructure/services/id_mapping_service.rs @@ -12,7 +12,7 @@ use crate::common::errors::{DomainError, ErrorKind}; use crate::application::ports::outbound::IdMappingPort; use crate::common::config::TimeoutConfig; -/// Error específico para el servicio de mapeo de IDs +/// Specific error for the ID mapping service #[derive(Debug, thiserror::Error)] pub enum IdMappingError { #[error("ID not found: {0}")] @@ -31,7 +31,7 @@ pub enum IdMappingError { Other(String), } -// Implementar conversión de IdMappingError a DomainError +// Implement conversion from IdMappingError to DomainError impl From for DomainError { fn from(err: IdMappingError) -> Self { match err { @@ -59,25 +59,25 @@ impl From for DomainError { } } -/// Estructura para almacenar IDs mapeados a sus rutas +/// Structure to store IDs mapped to their paths #[derive(Serialize, Deserialize, Debug, Default)] struct IdMap { path_to_id: HashMap, - id_to_path: HashMap, // Campo para búsqueda bidireccional eficiente - version: u32, // Versión para detectar cambios + id_to_path: HashMap, // Field for efficient bidirectional lookup + version: u32, // Version to detect changes } -/// Servicio para gestionar mapeos entre rutas y IDs únicos +/// Service to manage mappings between paths and unique IDs pub struct IdMappingService { map_path: PathBuf, id_map: RwLock, - save_mutex: Mutex<()>, // Para evitar múltiples guardados concurrentes + save_mutex: Mutex<()>, // To prevent multiple concurrent saves timeouts: TimeoutConfig, - pending_save: RwLock, // Indica si hay cambios pendientes + pending_save: RwLock, // Indicates if there are pending changes } impl IdMappingService { - /// Crea un nuevo servicio de mapeo de IDs + /// Creates a new ID mapping service pub async fn new(map_path: PathBuf) -> Result { let timeouts = TimeoutConfig::default(); let id_map = Self::load_id_map(&map_path, &timeouts).await?; @@ -91,7 +91,7 @@ impl IdMappingService { }) } - /// Crea un servicio de mapeo de IDs en memoria (para pruebas) + /// Creates an in-memory ID mapping service (for testing) /// /// Similar functionality as new_in_memory but with a simpler signature for dummy use pub fn dummy() -> Self { @@ -104,7 +104,7 @@ impl IdMappingService { } } - /// Crea un servicio de mapeo de IDs en memoria (para pruebas - versión original) + /// Creates an in-memory ID mapping service (for testing - original version) pub fn new_in_memory() -> Self { Self { map_path: PathBuf::from("memory"), @@ -115,10 +115,10 @@ impl IdMappingService { } } - /// Carga el mapa de IDs desde disco con manejo robusto de errores + /// Loads the ID map from disk with robust error handling async fn load_id_map(map_path: &PathBuf, timeouts: &TimeoutConfig) -> Result { if map_path.exists() { - // Intentar leer con timeout para evitar bloqueos indefinidos + // Try to read with timeout to avoid indefinite blocking let read_result = time::timeout( timeouts.lock_timeout(), fs::read_to_string(map_path) @@ -127,10 +127,10 @@ impl IdMappingService { let content = read_result.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to read ID map from {}: {}", map_path.display(), e)))?; - // Parsear el JSON + // Parse the JSON match serde_json::from_str::(&content) { Ok(mut map) => { - // Reconstruir el mapa inverso si es necesario + // Rebuild the inverse map if necessary if map.id_to_path.is_empty() && !map.path_to_id.is_empty() { let mut rebuild_count = 0; for (path, id) in &map.path_to_id { @@ -146,7 +146,7 @@ impl IdMappingService { }, Err(e) => { tracing::error!("Error parsing ID map: {}", e); - // Intentar hacer un respaldo del archivo corrupto + // Try to backup the corrupted file let backup_path = map_path.with_extension("json.bak"); if let Err(copy_err) = tokio::fs::copy(map_path, &backup_path).await { tracing::error!("Failed to backup corrupted map file: {}", copy_err); @@ -158,18 +158,18 @@ impl IdMappingService { return Ok(IdMap { path_to_id: HashMap::new(), id_to_path: HashMap::new(), - version: 1, // Iniciar con versión 1 + version: 1, // Start with version 1 }); } } } - // Devolver un mapa vacío si el archivo no existe y crear el archivo + // Return an empty map if the file doesn't exist and create the file tracing::info!("No existing ID map found, creating new empty map"); let empty_map = IdMap { path_to_id: HashMap::new(), id_to_path: HashMap::new(), - version: 1, // Iniciar con versión 1 + version: 1, // Start with version 1 }; // Ensure directory exists @@ -198,16 +198,16 @@ impl IdMappingService { Ok(empty_map) } - /// Guarda el mapa de IDs en disco de manera segura + /// Saves the ID map to disk safely async fn save_id_map(&self) -> Result<(), DomainError> { - // Adquirir bloqueo exclusivo para salvar + // Acquire exclusive lock for saving let _lock = time::timeout( self.timeouts.lock_timeout(), self.save_mutex.lock() ).await .map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring save lock for ID mapping"))?; - // Crear JSON con el lock de lectura para minimizar el tiempo de bloqueo + // Create JSON with read lock to minimize lock hold time let json = { let mut map = time::timeout( self.timeouts.lock_timeout(), @@ -215,7 +215,7 @@ impl IdMappingService { ).await .map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring write lock for ID mapping"))?; - // Incrementar versión sólo si hay cambios por guardar + // Increment version only if there are pending changes to save let pending = *self.pending_save.read().await; if pending { map.version += 1; @@ -227,16 +227,16 @@ impl IdMappingService { .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to serialize ID map to JSON: {}", e)))? }; - // Escribir a un archivo temporal primero para evitar corrupción + // Write to a temporary file first to avoid corruption let temp_path = self.map_path.with_extension("json.tmp"); fs::write(&temp_path, &json).await .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to write temporary ID map to {}: {}", temp_path.display(), e)))?; - // Realizar el rename atómico + // Perform the atomic rename fs::rename(&temp_path, &self.map_path).await .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to rename temporary ID map to {}: {}", self.map_path.display(), e)))?; - // Resetear flag de pendientes + // Reset pending flag { let mut pending = self.pending_save.write().await; *pending = false; @@ -246,22 +246,22 @@ impl IdMappingService { Ok(()) } - /// Genera un ID único + /// Generates a unique ID fn generate_id(&self) -> String { Uuid::new_v4().to_string() } - /// Marca cambios como pendientes + /// Marks changes as pending async fn mark_pending(&self) { let mut pending = self.pending_save.write().await; *pending = true; } - /// Obtiene el ID para una ruta o genera uno nuevo si no existe + /// Gets the ID for a path or generates a new one if it doesn't exist pub async fn get_or_create_id(&self, path: &StoragePath) -> Result { let path_str = path.to_string(); - // Primer intento con lock de lectura (más eficiente) + // First attempt with read lock (more efficient) { let read_result = match time::timeout( self.timeouts.lock_timeout(), @@ -276,7 +276,7 @@ impl IdMappingService { } } - // Si no se encuentra, adquirir lock de escritura + // If not found, acquire write lock let write_result = match time::timeout( self.timeouts.lock_timeout(), self.id_map.write() @@ -287,18 +287,18 @@ impl IdMappingService { let mut map = write_result; - // Verificar nuevamente (podría haberse agregado mientras esperábamos el lock) + // Check again (it could have been added while we were waiting for the lock) if let Some(id) = map.path_to_id.get(&path_str) { return Ok(id.clone()); } - // Generar un nuevo ID y almacenarlo + // Generate a new ID and store it let id = self.generate_id(); map.path_to_id.insert(path_str.clone(), id.clone()); map.id_to_path.insert(id.clone(), path_str); - // Marcar como pendiente para guardar - drop(map); // Liberar el write lock antes de adquirir otro + // Mark as pending for saving + drop(map); // Release the write lock before acquiring another self.mark_pending().await; tracing::debug!("Created new ID mapping: {} -> {}", path.to_string(), id); @@ -306,7 +306,7 @@ impl IdMappingService { Ok(id) } - /// Obtiene una ruta por su ID con manejo de timeout + /// Gets a path by its ID with timeout handling pub async fn get_path_by_id(&self, id: &str) -> Result { let read_result = match time::timeout( self.timeouts.lock_timeout(), @@ -323,7 +323,7 @@ impl IdMappingService { Err(IdMappingError::NotFound(id.to_string())) } - /// Actualiza el mapeo de un ID existente a una nueva ruta + /// Updates the mapping of an existing ID to a new path pub async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), IdMappingError> { let write_result = match time::timeout( self.timeouts.lock_timeout(), @@ -335,17 +335,17 @@ impl IdMappingService { let mut map = write_result; - // Buscar la ruta anterior para eliminarla + // Find the previous path to remove it if let Some(old_path) = map.id_to_path.get(id).cloned() { map.path_to_id.remove(&old_path); - // Registrar la nueva ruta + // Register the new path let new_path_str = new_path.to_string(); map.path_to_id.insert(new_path_str.clone(), id.to_string()); map.id_to_path.insert(id.to_string(), new_path_str); - // Marcar como pendiente - drop(map); // Liberar el write lock antes de adquirir otro + // Mark as pending + drop(map); // Release the write lock before acquiring another self.mark_pending().await; tracing::debug!("Updated path mapping for ID {}: {} -> {}", @@ -357,7 +357,7 @@ impl IdMappingService { } } - /// Elimina un ID del mapa + /// Removes an ID from the map pub async fn remove_id(&self, id: &str) -> Result<(), IdMappingError> { let write_result = match time::timeout( self.timeouts.lock_timeout(), @@ -369,12 +369,12 @@ impl IdMappingService { let mut map = write_result; - // Buscar la ruta para eliminarla + // Find the path to remove it if let Some(path) = map.id_to_path.remove(id) { map.path_to_id.remove(&path); - // Marcar como pendiente - drop(map); // Liberar el write lock antes de adquirir otro + // Mark as pending + drop(map); // Release the write lock before acquiring another self.mark_pending().await; tracing::debug!("Removed ID mapping: {} -> {}", id, path); @@ -384,9 +384,9 @@ impl IdMappingService { } } - /// Guarda cambios pendientes al disco inmediatamente, sin debounce + /// Saves pending changes to disk immediately, without debounce pub async fn save_pending_changes(&self) -> Result<(), IdMappingError> { - // Verificar si hay cambios pendientes + // Check if there are pending changes { let pending = self.pending_save.read().await; if !*pending { @@ -394,12 +394,12 @@ impl IdMappingService { } } - // Guardar inmediatamente (sin debounce ni spawn) + // Save immediately (without debounce or spawn) match self.save_id_map().await { Ok(_) => { tracing::info!("ID mappings saved successfully to disk at {}", self.map_path.display()); - // Verificar explícitamente que el archivo existe y tiene tamaño + // Explicitly verify that the file exists and has size match std::fs::metadata(&self.map_path) { Ok(metadata) => { if metadata.len() > 0 { @@ -410,7 +410,7 @@ impl IdMappingService { }, Err(e) => { tracing::error!("Failed to verify saved map file: {}", e); - // Intentar un segundo guardado si la verificación falla + // Try a second save if verification fails if let Err(retry_err) = self.save_id_map().await { tracing::error!("Second save attempt also failed: {}", retry_err); return Err(IdMappingError::IoError(std::io::Error::new( @@ -426,7 +426,7 @@ impl IdMappingService { }, Err(e) => { tracing::error!("Failed to save ID map to {}: {}", self.map_path.display(), e); - // Intentar un segundo guardado con retraso en caso de error + // Try a second save with delay in case of error tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; match self.save_id_map().await { Ok(_) => { @@ -448,31 +448,31 @@ impl IdMappingService { #[async_trait] impl IdMappingPort for IdMappingService { - /// Obtiene el ID para una ruta o genera uno nuevo si no existe + /// Gets the ID for a path or generates a new one if it doesn't exist async fn get_or_create_id(&self, path: &StoragePath) -> Result { self.get_or_create_id(path).await .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get or create ID for path: {}: {}", path.to_string(), e))) } - /// Obtiene una ruta por su ID con manejo de timeout + /// Gets a path by its ID with timeout handling async fn get_path_by_id(&self, id: &str) -> Result { self.get_path_by_id(id).await .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get path for ID: {}: {}", id, e))) } - /// Actualiza el mapeo de un ID existente a una nueva ruta + /// Updates the mapping of an existing ID to a new path async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> { self.update_path(id, new_path).await .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to update path for ID: {} to {}: {}", id, new_path.to_string(), e))) } - /// Elimina un ID del mapa + /// Removes an ID from the map async fn remove_id(&self, id: &str) -> Result<(), DomainError> { self.remove_id(id).await .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to remove ID: {}: {}", id, e))) } - /// Guarda cambios pendientes al disco + /// Saves pending changes to disk async fn save_changes(&self) -> Result<(), DomainError> { self.save_pending_changes().await .map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to save pending ID mapping changes: {}", e))) @@ -481,7 +481,7 @@ impl IdMappingPort for IdMappingService { // The extension methods were moved to the IdMappingPort trait as default implementations -// Implementar Clone para poder usar en tokio::spawn +// Implement Clone to allow use in tokio::spawn /// Synchronous helper for contexts where we can't use async impl IdMappingService { /// Create a new service synchronously (only for stubs and initialization) @@ -499,13 +499,13 @@ impl IdMappingService { impl Clone for IdMappingService { fn clone(&self) -> Self { - // No podemos clonar directamente los RwLock/Mutex, - // pero podemos crear nuevas instancias que apunten al mismo Arc interno - // Sin embargo, en este caso simplemente necesitamos la map_path + // We cannot directly clone the RwLock/Mutex, + // but we can create new instances that point to the same internal Arc + // However, in this case we simply need the map_path Self { map_path: self.map_path.clone(), - id_map: RwLock::new(IdMap::default()), // Esto no se usa en el task asíncrono - save_mutex: Mutex::new(()), // Esto tampoco + id_map: RwLock::new(IdMap::default()), // This is not used in the async task + save_mutex: Mutex::new(()), // Neither is this timeouts: self.timeouts.clone(), pending_save: RwLock::new(false), } @@ -530,7 +530,7 @@ mod tests { assert!(!id.is_empty(), "ID should not be empty"); - // Verificar que el mismo ID se devuelve para la misma ruta + // Verify that the same ID is returned for the same path let id2 = service.get_or_create_id(&path).await.unwrap(); assert_eq!(id, id2, "Same path should return same ID"); } @@ -557,7 +557,7 @@ mod tests { let temp_dir = tempdir().unwrap(); let map_path = temp_dir.path().join("id_map.json"); - // Crear y poblar el servicio + // Create and populate the service let service = IdMappingService::new(map_path.clone()).await.unwrap(); let path1 = StoragePath::from_string("/test/file1.txt"); @@ -565,16 +565,16 @@ mod tests { let id1 = service.get_or_create_id(&path1).await.unwrap(); let id2 = service.get_or_create_id(&path2).await.unwrap(); - // Guardar cambios + // Save changes service.save_pending_changes().await.unwrap(); - // Esperar para asegurar que el guardado asíncrono termine + // Wait to ensure the async save completes tokio::time::sleep(Duration::from_millis(500)).await; - // Crear un nuevo servicio que debería cargar el mismo mapa + // Create a new service that should load the same map let service2 = IdMappingService::new(map_path).await.unwrap(); - // Verificar que los IDs coinciden + // Verify that the IDs match let loaded_id1 = service2.get_or_create_id(&path1).await.unwrap(); let loaded_id2 = service2.get_or_create_id(&path2).await.unwrap(); @@ -591,7 +591,7 @@ mod tests { let service = std::sync::Arc::new(IdMappingService::new(map_path).await.unwrap()); - // Crear múltiples tareas que intentan acceder simultáneamente + // Create multiple tasks that attempt simultaneous access let mut tasks = Vec::new(); for i in 0..100 { let path = StoragePath::from_string(&format!("/test/concurrent/file{}.txt", i)); @@ -602,15 +602,15 @@ mod tests { })); } - // Esperar a que todas terminen + // Wait for all to finish let results = join_all(tasks).await; - // Verificar que todas tuvieron éxito + // Verify that all succeeded for result in results { assert!(result.unwrap().is_ok(), "Concurrent operations should succeed"); } - // Guardar cambios + // Save changes service.save_pending_changes().await.unwrap(); } } \ No newline at end of file diff --git a/src/infrastructure/services/jwt_service.rs b/src/infrastructure/services/jwt_service.rs index 3f0a7aee..7634947b 100644 --- a/src/infrastructure/services/jwt_service.rs +++ b/src/infrastructure/services/jwt_service.rs @@ -110,7 +110,7 @@ impl TokenServicePort for JwtTokenService { DomainError::new( ErrorKind::InternalError, "TokenService", - format!("Error al generar token: {}", e) + format!("Error generating token: {}", e) ) }) } @@ -126,12 +126,12 @@ impl TokenServicePort for JwtTokenService { .map_err(|e| { match e.kind() { jsonwebtoken::errors::ErrorKind::ExpiredSignature => { - DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado") + DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expired") }, _ => DomainError::new( ErrorKind::AccessDenied, "TokenService", - format!("Token inválido: {}", e) + format!("Invalid token: {}", e) ), } })?; diff --git a/src/infrastructure/services/path_service.rs b/src/infrastructure/services/path_service.rs index b661b743..0f413fca 100644 --- a/src/infrastructure/services/path_service.rs +++ b/src/infrastructure/services/path_service.rs @@ -1,9 +1,9 @@ -//! PathService - Servicio de infraestructura para manejo de rutas de almacenamiento +//! PathService - Infrastructure service for storage path management //! -//! Este servicio fue movido desde domain/services porque implementa traits de application -//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs). +//! This service was moved from domain/services because it implements application traits +//! (StoragePort, StorageMediator) and has file system dependencies (tokio::fs). //! -//! StoragePath (Value Object) permanece en domain/services/path_service.rs +//! StoragePath (Value Object) remains in domain/services/path_service.rs use std::path::{Path, PathBuf}; use async_trait::async_trait; @@ -15,18 +15,18 @@ use crate::application::services::storage_mediator::{StorageMediator, StorageMed use crate::domain::entities::folder::Folder; use crate::domain::services::path_service::StoragePath; -/// Servicio de infraestructura para manejar operaciones con rutas de almacenamiento +/// Infrastructure service for handling storage path operations pub struct PathService { root_path: PathBuf, } impl PathService { - /// Crea un nuevo servicio de rutas con una raíz específica + /// Creates a new path service with a specific root pub fn new(root_path: PathBuf) -> Self { Self { root_path } } - /// Convierte una ruta del dominio a una ruta física absoluta + /// Converts a domain path to an absolute physical path pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf { let mut path = self.root_path.clone(); for segment in storage_path.segments() { @@ -35,7 +35,7 @@ impl PathService { path } - /// Convierte una ruta física a una ruta de dominio + /// Converts a physical path to a domain path pub fn to_storage_path(&self, physical_path: &Path) -> Option { physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| { let segments: Vec = rel_path @@ -49,12 +49,12 @@ impl PathService { }) } - /// Crea una ruta de archivo dentro de una carpeta + /// Creates a file path within a folder pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath { folder_path.join(file_name) } - /// Verifica si una ruta es directamente hija de otra + /// Checks if a path is a direct child of another pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool { if let Some(child_parent) = potential_child.parent() { &child_parent == parent_path @@ -63,7 +63,7 @@ impl PathService { } } - /// Verifica si una ruta está en la raíz + /// Checks if a path is at the root pub fn is_in_root(&self, path: &StoragePath) -> bool { path.parent().map_or(true, |p| p.is_empty()) } @@ -73,9 +73,9 @@ impl PathService { &self.root_path } - /// Valida una ruta para asegurar que no contiene componentes peligrosos + /// Validates a path to ensure it doesn't contain dangerous components pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> { - // Verificar que no haya segmentos vacíos + // Check for empty segments if path.segments().iter().any(|s| s.is_empty()) { return Err(DomainError::new( ErrorKind::InvalidInput, @@ -84,7 +84,7 @@ impl PathService { )); } - // Verificar que no haya caracteres peligrosos + // Check for dangerous characters let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|']; for segment in path.segments() { if segment.contains(&dangerous_chars[..]) { @@ -95,7 +95,7 @@ impl PathService { )); } - // Verificar que no empiece con . (oculto en Unix) + // Check that it doesn't start with . (hidden in Unix) if segment.starts_with('.') && segment != ".well-known" { return Err(DomainError::new( ErrorKind::InvalidInput, @@ -120,13 +120,13 @@ impl StoragePort for PathService { } async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> { - // Primero validar la ruta + // First validate the path self.validate_path(storage_path)?; - // Resolver a ruta física + // Resolve to physical path let physical_path = self.resolve_path(storage_path); - // Crear directorios si no existen + // Create directories if they don't exist if !physical_path.exists() { fs::create_dir_all(&physical_path).await .map_err(|e| DomainError::new( diff --git a/src/infrastructure/services/trash_cleanup_service.rs b/src/infrastructure/services/trash_cleanup_service.rs index 3484b0a8..50a6426a 100644 --- a/src/infrastructure/services/trash_cleanup_service.rs +++ b/src/infrastructure/services/trash_cleanup_service.rs @@ -7,7 +7,7 @@ use crate::common::errors::Result; use crate::domain::repositories::trash_repository::TrashRepository; use crate::application::ports::trash_ports::TrashUseCase; -/// Servicio para la limpieza automática de elementos expirados en la papelera +/// Service for automatic cleanup of expired items in the trash pub struct TrashCleanupService { trash_service: Arc, trash_repository: Arc, @@ -23,75 +23,75 @@ impl TrashCleanupService { Self { trash_service, trash_repository, - cleanup_interval_hours: cleanup_interval_hours.max(1), // Mínimo 1 hora + cleanup_interval_hours: cleanup_interval_hours.max(1), // Minimum 1 hour } } - /// Inicia el trabajo de limpieza periódica + /// Starts the periodic cleanup job #[instrument(skip(self))] pub async fn start_cleanup_job(&self) { let trash_repository = self.trash_repository.clone(); let trash_service = self.trash_service.clone(); let interval_hours = self.cleanup_interval_hours; - info!("Iniciando trabajo de limpieza de papelera con intervalo de {} horas", interval_hours); + info!("Starting trash cleanup job with interval of {} hours", interval_hours); tokio::spawn(async move { let interval_duration = Duration::from_secs(interval_hours * 60 * 60); let mut interval = time::interval(interval_duration); - // Primera ejecución inmediata + // First immediate execution Self::cleanup_expired_items(trash_repository.clone(), trash_service.clone()).await - .unwrap_or_else(|e| error!("Error en la limpieza inicial de la papelera: {:?}", e)); + .unwrap_or_else(|e| error!("Error in initial trash cleanup: {:?}", e)); loop { interval.tick().await; - debug!("Ejecutando tarea programada de limpieza de papelera"); + debug!("Running scheduled trash cleanup task"); if let Err(e) = Self::cleanup_expired_items( trash_repository.clone(), trash_service.clone() ).await { - error!("Error en la limpieza programada de la papelera: {:?}", e); + error!("Error in scheduled trash cleanup: {:?}", e); } } }); } - /// Limpia los elementos expirados en la papelera + /// Cleans up expired items in the trash #[instrument(skip(trash_repository, trash_service))] async fn cleanup_expired_items( trash_repository: Arc, trash_service: Arc, ) -> Result<()> { - debug!("Comenzando limpieza de elementos expirados en la papelera"); + debug!("Starting cleanup of expired items in the trash"); - // Obtener todos los elementos expirados + // Get all expired items let expired_items = trash_repository.get_expired_items().await?; if expired_items.is_empty() { - debug!("No hay elementos expirados para limpiar"); + debug!("No expired items to clean up"); return Ok(()); } - info!("Encontrados {} elementos expirados para eliminar", expired_items.len()); + info!("Found {} expired items to delete", expired_items.len()); - // Eliminar cada elemento expirado + // Delete each expired item for item in expired_items { let trash_id = item.id().to_string(); let user_id = item.user_id().to_string(); - debug!("Eliminando elemento expirado: id={}, user={}", trash_id, user_id); + debug!("Deleting expired item: id={}, user={}", trash_id, user_id); - // Si falla una eliminación, continuar con las demás + // If a deletion fails, continue with the rest if let Err(e) = trash_service.delete_permanently(&trash_id, &user_id).await { - error!("Error eliminando elemento expirado {}: {:?}", trash_id, e); + error!("Error deleting expired item {}: {:?}", trash_id, e); } else { - debug!("Elemento expirado eliminado correctamente: {}", trash_id); + debug!("Expired item deleted successfully: {}", trash_id); } } - info!("Limpieza de papelera completada"); + info!("Trash cleanup completed"); Ok(()) } } \ No newline at end of file diff --git a/src/infrastructure/services/zip_service.rs b/src/infrastructure/services/zip_service.rs index 09c600bf..9a243283 100644 --- a/src/infrastructure/services/zip_service.rs +++ b/src/infrastructure/services/zip_service.rs @@ -13,47 +13,47 @@ use crate::{ }; use std::sync::Arc; -/// Error relacionado con la creación de archivos ZIP +/// Error related to ZIP file creation #[derive(Debug, Error)] pub enum ZipError { - #[error("Error de IO: {0}")] + #[error("IO error: {0}")] IoError(#[from] std::io::Error), - #[error("Error de ZIP: {0}")] + #[error("ZIP error: {0}")] ZipError(#[from] zip::result::ZipError), - #[error("Error al leer el archivo: {0}")] + #[error("Error reading file: {0}")] FileReadError(String), - #[error("Error al obtener contenido de carpeta: {0}")] + #[error("Error getting folder contents: {0}")] FolderContentsError(String), - #[error("Carpeta no encontrada: {0}")] + #[error("Folder not found: {0}")] FolderNotFound(String), } -// Implementar From para DomainError para permitir el uso de ? +// Implement From for DomainError to allow the use of ? impl From for DomainError { fn from(err: ZipError) -> Self { DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string()) } } -// Implementar From para DomainError directamente +// Implement From for DomainError directly impl From for DomainError { fn from(err: zip::result::ZipError) -> Self { DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string()) } } -/// Servicio para crear archivos ZIP +/// Service for creating ZIP files pub struct ZipService { file_service: Arc, folder_service: Arc, } impl ZipService { - /// Crea una nueva instancia del servicio ZIP con una referencia al servicio de archivos + /// Creates a new instance of the ZIP service with a reference to the file service pub fn new(file_service: Arc, folder_service: Arc) -> Self { Self { file_service, @@ -61,33 +61,33 @@ impl ZipService { } } - /// Crea un archivo ZIP con el contenido de una carpeta y todas sus subcarpetas - /// Retorna los bytes del ZIP + /// Creates a ZIP file with the contents of a folder and all its subfolders + /// Returns the ZIP bytes pub async fn create_folder_zip(&self, folder_id: &str, folder_name: &str) -> Result> { - info!("Creando ZIP para carpeta: {} (ID: {})", folder_name, folder_id); + info!("Creating ZIP for folder: {} (ID: {})", folder_name, folder_id); - // Verificar si la carpeta existe + // Verify if the folder exists let folder = match self.folder_service.get_folder(folder_id).await { Ok(folder) => folder, Err(e) => { - error!("Error al obtener carpeta {}: {}", folder_id, e); + error!("Error getting folder {}: {}", folder_id, e); return Err(ZipError::FolderNotFound(folder_id.to_string()).into()); } }; - // Crear un buffer en memoria para el ZIP + // Create an in-memory buffer for the ZIP let buf = Cursor::new(Vec::new()); let mut zip = ZipWriter::new(buf); - // Establecer opciones de compresión + // Set compression options let options = SimpleFileOptions::default() .compression_method(zip::CompressionMethod::Deflated) .unix_permissions(0o755); - // Objeto para seguir las carpetas procesadas y evitar ciclos + // Object to track processed folders and avoid cycles let mut processed_folders = std::collections::HashSet::new(); - // Procesamos la carpeta raíz y construimos el ZIP + // Process the root folder and build the ZIP self.process_folder_recursively( &mut zip, &folder, @@ -96,20 +96,20 @@ impl ZipService { &mut processed_folders ).await?; - // Finalizar el ZIP y obtener los bytes + // Finalize the ZIP and get the bytes let mut zip_buf = zip.finish()?; let mut bytes = Vec::new(); match zip_buf.read_to_end(&mut bytes) { Ok(_) => Ok(bytes), Err(e) => { - error!("Error al leer ZIP finalizado: {}", e); + error!("Error reading finalized ZIP: {}", e); Err(ZipError::IoError(e).into()) } } } - // Implementación alternativa para evitar recursión en async + // Alternative implementation to avoid recursion in async async fn process_folder_recursively( &self, zip: &mut ZipWriter>>, @@ -118,63 +118,63 @@ impl ZipService { options: &SimpleFileOptions, processed_folders: &mut std::collections::HashSet ) -> Result<()> { - // Estructura para representar el trabajo pendiente + // Structure to represent pending work struct PendingFolder { folder: FolderDto, path: String, } - // Cola de trabajo para procesamiento iterativo + // Work queue for iterative processing let mut work_queue = vec![PendingFolder { folder: folder.clone(), path: path.to_string(), }]; - // Procesar la cola mientras haya elementos + // Process the queue while there are elements while let Some(current) = work_queue.pop() { let folder_id = current.folder.id.to_string(); - // Evitar ciclos + // Avoid cycles if processed_folders.contains(&folder_id) { continue; } processed_folders.insert(folder_id.clone()); - // Crear la entrada de directorio en el ZIP + // Create the directory entry in the ZIP let folder_path = format!("{}/", current.path); match zip.add_directory(&folder_path, *options) { - Ok(_) => debug!("Carpeta agregada al ZIP: {}", folder_path), + Ok(_) => debug!("Folder added to ZIP: {}", folder_path), Err(e) => { - warn!("No se pudo agregar carpeta al ZIP (puede que ya exista): {}", e); - // Continuamos aunque falle crear el directorio (podría estar duplicado) + warn!("Could not add folder to ZIP (it may already exist): {}", e); + // Continue even if creating the directory fails (it could be a duplicate) } } - // Agregar archivos de la carpeta al ZIP + // Add files from the folder to the ZIP let files = match self.file_service.list_files(Some(&folder_id)).await { Ok(files) => files, Err(e) => { - error!("Error al listar archivos en carpeta {}: {}", folder_id, e); - return Err(ZipError::FolderContentsError(format!("Error al listar archivos: {}", e)).into()); + error!("Error listing files in folder {}: {}", folder_id, e); + return Err(ZipError::FolderContentsError(format!("Error listing files: {}", e)).into()); } }; - // Agregar cada archivo al ZIP + // Add each file to the ZIP for file in files { self.add_file_to_zip(zip, &file, &folder_path, options).await?; } - // Procesar subcarpetas + // Process subfolders let subfolders = match self.folder_service.list_folders(Some(&folder_id)).await { Ok(folders) => folders, Err(e) => { - error!("Error al listar subcarpetas en {}: {}", folder_id, e); - return Err(ZipError::FolderContentsError(format!("Error al listar subcarpetas: {}", e)).into()); + error!("Error listing subfolders in {}: {}", folder_id, e); + return Err(ZipError::FolderContentsError(format!("Error listing subfolders: {}", e)).into()); } }; - // Agregar subcarpetas a la cola + // Add subfolders to the queue for subfolder in subfolders { let subfolder_path = format!("{}/{}", current.path, subfolder.name); work_queue.push(PendingFolder { @@ -187,7 +187,7 @@ impl ZipService { Ok(()) } - // Agrega un archivo al ZIP + // Adds a file to the ZIP async fn add_file_to_zip( &self, zip: &mut ZipWriter>>, @@ -196,34 +196,34 @@ impl ZipService { options: &SimpleFileOptions, ) -> Result<()> { let file_path = format!("{}{}", folder_path, file.name); - info!("Agregando archivo al ZIP: {}", file_path); + info!("Adding file to ZIP: {}", file_path); - // Obtener el contenido del archivo + // Get the file content let file_id = file.id.to_string(); let content = match self.file_service.get_file_content(&file_id).await { Ok(content) => content, Err(e) => { - error!("Error al leer contenido del archivo {}: {}", file_id, e); - return Err(ZipError::FileReadError(format!("Error al leer archivo {}: {}", file_id, e)).into()); + error!("Error reading file content {}: {}", file_id, e); + return Err(ZipError::FileReadError(format!("Error reading file {}: {}", file_id, e)).into()); } }; - // Escribir archivo al ZIP + // Write file to the ZIP match zip.start_file_from_path(std::path::Path::new(&file_path), *options) { Ok(_) => { match zip.write_all(&content) { Ok(_) => { - debug!("Archivo agregado al ZIP: {}", file_path); + debug!("File added to ZIP: {}", file_path); Ok(()) }, Err(e) => { - error!("Error al escribir contenido del archivo {}: {}", file_path, e); + error!("Error writing file content {}: {}", file_path, e); Err(ZipError::IoError(e).into()) } } }, Err(e) => { - error!("Error al iniciar archivo en ZIP {}: {}", file_path, e); + error!("Error starting file in ZIP {}: {}", file_path, e); Err(ZipError::ZipError(e).into()) } } diff --git a/src/interfaces/api/handlers/auth_handler.rs b/src/interfaces/api/handlers/auth_handler.rs index 286cfb17..00da24b2 100644 --- a/src/interfaces/api/handlers/auth_handler.rs +++ b/src/interfaces/api/handlers/auth_handler.rs @@ -15,7 +15,7 @@ use crate::application::dtos::user_dto::{ use crate::interfaces::errors::AppError; pub fn auth_routes() -> Router> { - // Rutas que NO requieren autenticación + // Routes that do NOT require authentication let public_routes = Router::new() .route("/register", post(register)) .route("/login", post(login)) @@ -27,14 +27,14 @@ pub fn auth_routes() -> Router> { .route("/oidc/callback", get(oidc_callback)) .route("/oidc/exchange", post(oidc_exchange)); - // Rutas que SÍ requieren autenticación - usamos route_layer para aplicar middleware - // El middleware usará el state que se pase con .with_state() desde main.rs + // Routes that DO require authentication - we use route_layer to apply middleware + // The middleware will use the state passed with .with_state() from main.rs let protected_routes = Router::new() .route("/me", get(get_current_user)) .route("/change-password", put(change_password)) .route("/logout", post(logout)); - // Combinar rutas públicas y protegidas + // Combine public and protected routes public_routes.merge(protected_routes) } @@ -53,7 +53,7 @@ async fn register( }, None => { tracing::error!("Auth service not configured"); - return Err(AppError::internal_error("Servicio de autenticación no configurado")); + return Err(AppError::internal_error("Authentication service not configured")); } }; @@ -153,7 +153,7 @@ async fn login( }, None => { tracing::error!("Auth service not configured"); - return Err(AppError::internal_error("Servicio de autenticación no configurado")); + return Err(AppError::internal_error("Authentication service not configured")); } }; @@ -174,7 +174,7 @@ async fn login( // Ensure the response has the expected fields if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() { tracing::error!("Login response contains empty tokens for user: {}", dto.username); - return Err(AppError::internal_error("Error generando tokens de autenticación")); + return Err(AppError::internal_error("Error generating authentication tokens")); } Ok((StatusCode::OK, Json(auth_response))) @@ -198,7 +198,7 @@ async fn refresh_token( // Normal process for real tokens let auth_service = state.auth_service.as_ref() - .ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?; + .ok_or_else(|| AppError::internal_error("Authentication service not configured"))?; let auth_response = auth_service.auth_application_service.refresh_token(dto).await?; @@ -214,37 +214,37 @@ async fn get_current_user( ) -> Result { // Normal process for all users let auth_service = state.auth_service.as_ref() - .ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?; + .ok_or_else(|| AppError::internal_error("Authentication service not configured"))?; - // Extraer y validar el token directamente + // Extract and validate the token directly let token = headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) .and_then(|value| value.strip_prefix("Bearer ")) - .ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?; + .ok_or_else(|| AppError::unauthorized("Authorization token not found"))?; - // Validar el token y obtener claims + // Validate the token and get claims let claims = auth_service.token_service.validate_token(token) - .map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?; + .map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?; let user_id = claims.sub; - // Primero, actualizar las estadísticas de uso de almacenamiento - // IMPORTANTE: Esperamos el cálculo para devolver datos actualizados + // First, update the storage usage statistics + // IMPORTANT: We await the calculation to return updated data if let Some(storage_usage_service) = state.storage_usage_service.as_ref() { - // Calcular storage de forma síncrona (esperamos el resultado) + // Calculate storage synchronously (we await the result) match storage_usage_service.update_user_storage_usage(&user_id).await { Ok(usage) => { tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage); }, Err(e) => { - // Solo log de warning, no fallar la petición completa + // Only log a warning, don't fail the entire request tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e); } } } - // Ahora obtener los datos del usuario CON el almacenamiento actualizado + // Now get the user data WITH the updated storage let user = auth_service.auth_application_service.get_user_by_id(&user_id).await?; Ok((StatusCode::OK, Json(user))) @@ -256,18 +256,18 @@ async fn change_password( Json(dto): Json, ) -> Result { let auth_service = state.auth_service.as_ref() - .ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?; + .ok_or_else(|| AppError::internal_error("Authentication service not configured"))?; - // Extraer y validar el token directamente + // Extract and validate the token directly let token = headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) .and_then(|value| value.strip_prefix("Bearer ")) - .ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?; + .ok_or_else(|| AppError::unauthorized("Authorization token not found"))?; - // Validar el token y obtener claims + // Validate the token and get claims let claims = auth_service.token_service.validate_token(token) - .map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?; + .map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?; auth_service.auth_application_service.change_password(&claims.sub, dto).await?; @@ -279,18 +279,18 @@ async fn logout( headers: HeaderMap, ) -> Result { let auth_service = state.auth_service.as_ref() - .ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?; + .ok_or_else(|| AppError::internal_error("Authentication service not configured"))?; - // Extraer y validar el token directamente + // Extract and validate the token directly let token = headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) .and_then(|value| value.strip_prefix("Bearer ")) - .ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?; + .ok_or_else(|| AppError::unauthorized("Authorization token not found"))?; - // Validar el token y obtener claims + // Validate the token and get claims let claims = auth_service.token_service.validate_token(token) - .map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?; + .map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?; // Use access token for logout (we don't have refresh token in headers) auth_service.auth_application_service.logout(&claims.sub, token).await?; @@ -314,7 +314,7 @@ async fn get_system_status( State(state): State>, ) -> Result { let auth_service = state.auth_service.as_ref() - .ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?; + .ok_or_else(|| AppError::internal_error("Authentication service not configured"))?; // Count admin users to determine if system is initialized let admin_count = auth_service.auth_application_service.count_admin_users().await diff --git a/src/interfaces/api/handlers/batch_handler.rs b/src/interfaces/api/handlers/batch_handler.rs index 134b8b9b..99ccb1d5 100644 --- a/src/interfaces/api/handlers/batch_handler.rs +++ b/src/interfaces/api/handlers/batch_handler.rs @@ -13,86 +13,86 @@ use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::folder_dto::FolderDto; use crate::interfaces::api::handlers::ApiResult; -/// Estado compartido para el handler de batch +/// Shared state for the batch handler #[derive(Clone)] pub struct BatchHandlerState { pub batch_service: Arc, } -/// DTO para las solicitudes de operaciones en lote de archivos +/// DTO for batch file operation requests #[derive(Debug, Deserialize)] pub struct BatchFileOperationRequest { - /// IDs de los archivos a procesar + /// IDs of the files to process pub file_ids: Vec, - /// ID de la carpeta destino (opcional) + /// Target folder ID (optional) #[serde(skip_serializing_if = "Option::is_none")] pub target_folder_id: Option, } -/// DTO para las solicitudes de operaciones en lote de carpetas +/// DTO for batch folder operation requests #[derive(Debug, Deserialize)] pub struct BatchFolderOperationRequest { - /// IDs de las carpetas a procesar + /// IDs of the folders to process pub folder_ids: Vec, - /// Si la operación debe ser recursiva + /// Whether the operation should be recursive #[serde(default)] pub recursive: bool, - /// ID de la carpeta destino (opcional) + /// Target folder ID (optional) #[serde(skip_serializing_if = "Option::is_none")] pub target_folder_id: Option, } -/// DTO para las solicitudes de creación en lote de carpetas +/// DTO for batch folder creation requests #[derive(Debug, Deserialize)] pub struct BatchCreateFoldersRequest { - /// Detalles de las carpetas a crear + /// Details of the folders to create pub folders: Vec, } -/// Detalle para creación de una carpeta +/// Detail for folder creation #[derive(Debug, Deserialize)] pub struct CreateFolderDetail { - /// Nombre de la carpeta + /// Folder name pub name: String, - /// ID de la carpeta padre (opcional) + /// Parent folder ID (optional) #[serde(skip_serializing_if = "Option::is_none")] pub parent_id: Option, } -/// DTO para los resultados de operaciones en lote +/// DTO for batch operation results #[derive(Debug, Serialize)] pub struct BatchOperationResponse { - /// Entidades procesadas exitosamente + /// Successfully processed entities pub successful: Vec, - /// Operaciones fallidas con sus mensajes de error + /// Failed operations with their error messages pub failed: Vec, - /// Estadísticas de la operación + /// Operation statistics pub stats: BatchOperationStats, } -/// Operación fallida en un lote +/// Failed operation in a batch #[derive(Debug, Serialize)] pub struct FailedOperation { - /// Identificador de la entidad que falló + /// Identifier of the entity that failed pub id: String, - /// Mensaje de error + /// Error message pub error: String, } -/// Estadísticas de una operación por lotes +/// Statistics for a batch operation #[derive(Debug, Serialize)] pub struct BatchOperationStats { - /// Número total de operaciones + /// Total number of operations pub total: usize, - /// Número de operaciones exitosas + /// Number of successful operations pub successful: usize, - /// Número de operaciones fallidas + /// Number of failed operations pub failed: usize, - /// Tiempo total de ejecución en milisegundos + /// Total execution time in milliseconds pub execution_time_ms: u128, } -/// Convierte BatchStats del dominio a DTO +/// Converts domain BatchStats to DTO impl From for BatchOperationStats { fn from(stats: BatchStats) -> Self { Self { @@ -104,7 +104,7 @@ impl From for BatchOperationStats { } } -/// Convierte BatchResult del dominio a DTO +/// Converts domain BatchResult to DTO impl From> for BatchOperationResponse where U: From, @@ -124,12 +124,12 @@ where } } -/// Handler para mover múltiples archivos en lote +/// Handler for moving multiple files in batch pub async fn move_files_batch( State(state): State, Json(request): Json, ) -> ApiResult { - // Verificar que hay archivos para procesar + // Verify there are files to process if request.file_ids.is_empty() { return Ok(( StatusCode::BAD_REQUEST, @@ -139,35 +139,35 @@ pub async fn move_files_batch( ).into_response()); } - // Ejecutar operación de lote + // Execute batch operation let result = state.batch_service .move_files(request.file_ids, request.target_folder_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Convertir resultado a DTO + // Convert result to DTO let response: BatchOperationResponse = result.into(); - // Determinar código de estado basado en los resultados + // Determine status code based on results let status_code = if response.stats.failed > 0 { if response.stats.successful > 0 { - StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas + StatusCode::PARTIAL_CONTENT // Some operations successful, others failed } else { - StatusCode::BAD_REQUEST // Todas fallaron + StatusCode::BAD_REQUEST // All failed } } else { - StatusCode::OK // Todas exitosas + StatusCode::OK // All successful }; Ok((status_code, Json(response)).into_response()) } -/// Handler para copiar múltiples archivos en lote +/// Handler for copying multiple files in batch pub async fn copy_files_batch( State(state): State, Json(request): Json, ) -> ApiResult { - // Verificar que hay archivos para procesar + // Verify there are files to process if request.file_ids.is_empty() { return Ok(( StatusCode::BAD_REQUEST, @@ -177,35 +177,35 @@ pub async fn copy_files_batch( ).into_response()); } - // Ejecutar operación de lote + // Execute batch operation let result = state.batch_service .copy_files(request.file_ids, request.target_folder_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Convertir resultado a DTO + // Convert result to DTO let response: BatchOperationResponse = result.into(); - // Determinar código de estado basado en los resultados + // Determine status code based on results let status_code = if response.stats.failed > 0 { if response.stats.successful > 0 { - StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas + StatusCode::PARTIAL_CONTENT // Some operations successful, others failed } else { - StatusCode::BAD_REQUEST // Todas fallaron + StatusCode::BAD_REQUEST // All failed } } else { - StatusCode::OK // Todas exitosas + StatusCode::OK // All successful }; Ok((status_code, Json(response)).into_response()) } -/// Handler para eliminar múltiples archivos en lote +/// Handler for deleting multiple files in batch pub async fn delete_files_batch( State(state): State, Json(request): Json, ) -> ApiResult { - // Verificar que hay archivos para procesar + // Verify there are files to process if request.file_ids.is_empty() { return Ok(( StatusCode::BAD_REQUEST, @@ -215,13 +215,13 @@ pub async fn delete_files_batch( ).into_response()); } - // Ejecutar operación de lote + // Execute batch operation let result = state.batch_service .delete_files(request.file_ids) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Crear respuesta personalizada para IDs de string + // Create custom response for string IDs let response = BatchOperationResponse { successful: result.successful, failed: result.failed.into_iter() @@ -230,26 +230,26 @@ pub async fn delete_files_batch( stats: result.stats.into(), }; - // Determinar código de estado basado en los resultados + // Determine status code based on results let status_code = if response.stats.failed > 0 { if response.stats.successful > 0 { - StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas + StatusCode::PARTIAL_CONTENT // Some operations successful, others failed } else { - StatusCode::BAD_REQUEST // Todas fallaron + StatusCode::BAD_REQUEST // All failed } } else { - StatusCode::OK // Todas exitosas + StatusCode::OK // All successful }; Ok((status_code, Json(response)).into_response()) } -/// Handler para eliminar múltiples carpetas en lote +/// Handler for deleting multiple folders in batch pub async fn delete_folders_batch( State(state): State, Json(request): Json, ) -> ApiResult { - // Verificar que hay carpetas para procesar + // Verify there are folders to process if request.folder_ids.is_empty() { return Ok(( StatusCode::BAD_REQUEST, @@ -259,13 +259,13 @@ pub async fn delete_folders_batch( ).into_response()); } - // Ejecutar operación de lote + // Execute batch operation let result = state.batch_service .delete_folders(request.folder_ids, request.recursive) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Crear respuesta personalizada para IDs de string + // Create custom response for string IDs let response = BatchOperationResponse { successful: result.successful, failed: result.failed.into_iter() @@ -274,26 +274,26 @@ pub async fn delete_folders_batch( stats: result.stats.into(), }; - // Determinar código de estado basado en los resultados + // Determine status code based on results let status_code = if response.stats.failed > 0 { if response.stats.successful > 0 { - StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas + StatusCode::PARTIAL_CONTENT // Some operations successful, others failed } else { - StatusCode::BAD_REQUEST // Todas fallaron + StatusCode::BAD_REQUEST // All failed } } else { - StatusCode::OK // Todas exitosas + StatusCode::OK // All successful }; Ok((status_code, Json(response)).into_response()) } -/// Handler para crear múltiples carpetas en lote +/// Handler for creating multiple folders in batch pub async fn create_folders_batch( State(state): State, Json(request): Json, ) -> ApiResult { - // Verificar que hay carpetas para procesar + // Verify there are folders to process if request.folders.is_empty() { return Ok(( StatusCode::BAD_REQUEST, @@ -303,41 +303,41 @@ pub async fn create_folders_batch( ).into_response()); } - // Transformar el formato para el servicio + // Transform the format for the service let folders = request.folders .into_iter() .map(|detail| (detail.name, detail.parent_id)) .collect(); - // Ejecutar operación de lote + // Execute batch operation let result = state.batch_service .create_folders(folders) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Convertir resultado a DTO + // Convert result to DTO let response: BatchOperationResponse = result.into(); - // Determinar código de estado basado en los resultados + // Determine status code based on results let status_code = if response.stats.failed > 0 { if response.stats.successful > 0 { - StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas + StatusCode::PARTIAL_CONTENT // Some operations successful, others failed } else { - StatusCode::BAD_REQUEST // Todas fallaron + StatusCode::BAD_REQUEST // All failed } } else { - StatusCode::CREATED // Todas exitosas + StatusCode::CREATED // All successful }; Ok((status_code, Json(response)).into_response()) } -/// Handler para obtener múltiples archivos en lote +/// Handler for getting multiple files in batch pub async fn get_files_batch( State(state): State, Json(request): Json, ) -> ApiResult { - // Verificar que hay archivos para procesar + // Verify there are files to process if request.file_ids.is_empty() { return Ok(( StatusCode::BAD_REQUEST, @@ -347,35 +347,35 @@ pub async fn get_files_batch( ).into_response()); } - // Ejecutar operación de lote + // Execute batch operation let result = state.batch_service .get_multiple_files(request.file_ids) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Convertir resultado a DTO + // Convert result to DTO let response: BatchOperationResponse = result.into(); - // Determinar código de estado basado en los resultados + // Determine status code based on results let status_code = if response.stats.failed > 0 { if response.stats.successful > 0 { - StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas + StatusCode::PARTIAL_CONTENT // Some operations successful, others failed } else { - StatusCode::BAD_REQUEST // Todas fallaron + StatusCode::BAD_REQUEST // All failed } } else { - StatusCode::OK // Todas exitosas + StatusCode::OK // All successful }; Ok((status_code, Json(response)).into_response()) } -/// Handler para obtener múltiples carpetas en lote +/// Handler for getting multiple folders in batch pub async fn get_folders_batch( State(state): State, Json(request): Json, ) -> ApiResult { - // Verificar que hay carpetas para procesar + // Verify there are folders to process if request.folder_ids.is_empty() { return Ok(( StatusCode::BAD_REQUEST, @@ -385,24 +385,24 @@ pub async fn get_folders_batch( ).into_response()); } - // Ejecutar operación de lote + // Execute batch operation let result = state.batch_service .get_multiple_folders(request.folder_ids) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Convertir resultado a DTO + // Convert result to DTO let response: BatchOperationResponse = result.into(); - // Determinar código de estado basado en los resultados + // Determine status code based on results let status_code = if response.stats.failed > 0 { if response.stats.successful > 0 { - StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas + StatusCode::PARTIAL_CONTENT // Some operations successful, others failed } else { - StatusCode::BAD_REQUEST // Todas fallaron + StatusCode::BAD_REQUEST // All failed } } else { - StatusCode::OK // Todas exitosas + StatusCode::OK // All successful }; Ok((status_code, Json(response)).into_response()) diff --git a/src/interfaces/api/handlers/recent_handler.rs b/src/interfaces/api/handlers/recent_handler.rs index e6d4e097..4629ddc7 100644 --- a/src/interfaces/api/handlers/recent_handler.rs +++ b/src/interfaces/api/handlers/recent_handler.rs @@ -11,14 +11,14 @@ use tracing::{error, info}; use crate::application::ports::recent_ports::RecentItemsUseCase; use crate::interfaces::middleware::auth::AuthUser; -/// Parámetros de consulta para obtener elementos recientes +/// Query parameters for getting recent items #[derive(Deserialize)] pub struct GetRecentParams { #[serde(default)] limit: Option, } -/// Obtener elementos recientes del usuario +/// Get user's recent items pub async fn get_recent_items( State(recent_service): State>, auth_user: AuthUser, @@ -28,22 +28,22 @@ pub async fn get_recent_items( match recent_service.get_recent_items(user_id, params.limit).await { Ok(items) => { - info!("Recuperados {} elementos recientes para usuario", items.len()); + info!("Retrieved {} recent items for user", items.len()); (StatusCode::OK, Json(items)).into_response() }, Err(err) => { - error!("Error al recuperar elementos recientes: {}", err); + error!("Error retrieving recent items: {}", err); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": format!("Fallo al recuperar elementos recientes: {}", err) + "error": format!("Failed to retrieve recent items: {}", err) })) ).into_response() } } } -/// Registrar acceso a un elemento +/// Record access to an item pub async fn record_item_access( State(recent_service): State>, auth_user: AuthUser, @@ -51,39 +51,39 @@ pub async fn record_item_access( ) -> impl IntoResponse { let user_id = &auth_user.id; - // Validar tipo de elemento + // Validate item type if item_type != "file" && item_type != "folder" { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "El tipo de elemento debe ser 'file' o 'folder'" + "error": "Item type must be 'file' or 'folder'" })) ).into_response(); } match recent_service.record_item_access(user_id, &item_id, &item_type).await { Ok(_) => { - info!("Registrado acceso a {} '{}' en recientes", item_type, item_id); + info!("Recorded access to {} '{}' in recents", item_type, item_id); ( StatusCode::OK, Json(serde_json::json!({ - "message": "Acceso registrado correctamente" + "message": "Access recorded successfully" })) ).into_response() }, Err(err) => { - error!("Error al registrar acceso en recientes: {}", err); + error!("Error recording access in recents: {}", err); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": format!("Fallo al registrar acceso: {}", err) + "error": format!("Failed to record access: {}", err) })) ).into_response() } } } -/// Eliminar un elemento de recientes +/// Remove an item from recents pub async fn remove_from_recent( State(recent_service): State>, auth_user: AuthUser, @@ -94,36 +94,36 @@ pub async fn remove_from_recent( match recent_service.remove_from_recent(user_id, &item_id, &item_type).await { Ok(removed) => { if removed { - info!("Eliminado {} '{}' de recientes", item_type, item_id); + info!("Removed {} '{}' from recents", item_type, item_id); ( StatusCode::OK, Json(serde_json::json!({ - "message": "Elemento eliminado de recientes" + "message": "Item removed from recents" })) ).into_response() } else { - info!("Elemento {} '{}' no estaba en recientes", item_type, item_id); + info!("Item {} '{}' was not in recents", item_type, item_id); ( StatusCode::NOT_FOUND, Json(serde_json::json!({ - "message": "Elemento no estaba en recientes" + "message": "Item was not in recents" })) ).into_response() } }, Err(err) => { - error!("Error al eliminar de recientes: {}", err); + error!("Error removing from recents: {}", err); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": format!("Fallo al eliminar de recientes: {}", err) + "error": format!("Failed to remove from recents: {}", err) })) ).into_response() } } } -/// Limpiar todos los elementos recientes +/// Clear all recent items pub async fn clear_recent_items( State(recent_service): State>, auth_user: AuthUser, @@ -132,20 +132,20 @@ pub async fn clear_recent_items( match recent_service.clear_recent_items(user_id).await { Ok(_) => { - info!("Limpiados todos los elementos recientes para usuario"); + info!("Cleared all recent items for user"); ( StatusCode::OK, Json(serde_json::json!({ - "message": "Elementos recientes limpiados correctamente" + "message": "Recent items cleared successfully" })) ).into_response() }, Err(err) => { - error!("Error al limpiar elementos recientes: {}", err); + error!("Error clearing recent items: {}", err); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": format!("Fallo al limpiar elementos recientes: {}", err) + "error": format!("Failed to clear recent items: {}", err) })) ).into_response() } diff --git a/src/interfaces/api/handlers/search_handler.rs b/src/interfaces/api/handlers/search_handler.rs index 03632700..802c494c 100644 --- a/src/interfaces/api/handlers/search_handler.rs +++ b/src/interfaces/api/handlers/search_handler.rs @@ -10,34 +10,34 @@ use crate::application::dtos::search_dto::SearchCriteriaDto; use crate::common::di::AppState; /** - * Manejador para las operaciones de búsqueda a través de la API. + * Handler for search operations through the API. * - * Este manejador expone endpoints relacionados con la funcionalidad de búsqueda, - * permitiendo a los usuarios buscar archivos y carpetas usando diversos criterios. + * This handler exposes endpoints related to search functionality, + * allowing users to search for files and folders using various criteria. */ pub struct SearchHandler; impl SearchHandler { /** - * Realiza una búsqueda basada en los criterios proporcionados como parámetros de consulta. + * Performs a search based on the criteria provided as query parameters. * - * Este endpoint permite búsquedas simples directamente con parámetros URL. + * This endpoint allows simple searches directly with URL parameters. * - * @param state Estado de la aplicación con servicios - * @param query_params Parámetros de búsqueda como query string - * @return Respuesta HTTP con los resultados de la búsqueda + * @param state Application state with services + * @param query_params Search parameters as query string + * @return HTTP response with the search results */ pub async fn search_files_get( State(state): State, Query(params): Query, ) -> impl IntoResponse { - info!("API: Búsqueda de archivos con parámetros: {:?}", params); + info!("API: File search with parameters: {:?}", params); - // Extraer el servicio de búsqueda o devolver error si no está disponible + // Extract the search service or return error if not available let search_service = match &state.applications.search_service { Some(service) => service, None => { - error!("Servicio de búsqueda no disponible"); + error!("Search service not available"); return ( StatusCode::SERVICE_UNAVAILABLE, Json(json!({ @@ -47,7 +47,7 @@ impl SearchHandler { } }; - // Convertir parámetros de búsqueda a DTO + // Convert search parameters to DTO let search_criteria = SearchCriteriaDto { name_contains: params.query, file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()), @@ -63,15 +63,15 @@ impl SearchHandler { offset: params.offset.unwrap_or(0), }; - // Realizar la búsqueda + // Perform the search match search_service.search(search_criteria).await { Ok(results) => { - info!("Búsqueda completada, {} archivos y {} carpetas encontrados", + info!("Search completed, {} files and {} folders found", results.files.len(), results.folders.len()); (StatusCode::OK, Json(results)).into_response() }, Err(err) => { - error!("Error en búsqueda: {}", err); + error!("Search error: {}", err); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ @@ -83,26 +83,26 @@ impl SearchHandler { } /** - * Realiza una búsqueda avanzada basada en un objeto de criterios JSON completo. + * Performs an advanced search based on a complete JSON criteria object. * - * Este endpoint permite búsquedas más complejas con todos los criterios posibles - * proporcionados en el cuerpo de la solicitud. + * This endpoint allows more complex searches with all possible criteria + * provided in the request body. * - * @param state Estado de la aplicación con servicios - * @param criteria Criterios de búsqueda completos - * @return Respuesta HTTP con los resultados de la búsqueda + * @param state Application state with services + * @param criteria Complete search criteria + * @return HTTP response with the search results */ pub async fn search_files_post( State(state): State, Json(criteria): Json, ) -> impl IntoResponse { - info!("API: Búsqueda avanzada de archivos"); + info!("API: Advanced file search"); - // Extraer el servicio de búsqueda o devolver error si no está disponible + // Extract the search service or return error if not available let search_service = match &state.applications.search_service { Some(service) => service, None => { - error!("Servicio de búsqueda no disponible"); + error!("Search service not available"); return ( StatusCode::SERVICE_UNAVAILABLE, Json(json!({ @@ -112,15 +112,15 @@ impl SearchHandler { } }; - // Realizar la búsqueda + // Perform the search match search_service.search(criteria).await { Ok(results) => { - info!("Búsqueda completada, {} archivos y {} carpetas encontrados", + info!("Search completed, {} files and {} folders found", results.files.len(), results.folders.len()); (StatusCode::OK, Json(results)).into_response() }, Err(err) => { - error!("Error en búsqueda: {}", err); + error!("Search error: {}", err); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ @@ -132,24 +132,24 @@ impl SearchHandler { } /** - * Limpia la caché de resultados de búsqueda. + * Clears the search results cache. * - * Este endpoint es útil para forzar búsquedas frescas después de cambios - * significativos en el sistema de archivos. + * This endpoint is useful for forcing fresh searches after significant + * changes in the file system. * - * @param state Estado de la aplicación con servicios - * @return Respuesta HTTP indicando éxito o error + * @param state Application state with services + * @return HTTP response indicating success or error */ pub async fn clear_search_cache( State(state): State, ) -> impl IntoResponse { - info!("API: Limpiando caché de búsqueda"); + info!("API: Clearing search cache"); - // Extraer el servicio de búsqueda o devolver error si no está disponible + // Extract the search service or return error if not available let search_service = match &state.applications.search_service { Some(service) => service, None => { - error!("Servicio de búsqueda no disponible"); + error!("Search service not available"); return ( StatusCode::SERVICE_UNAVAILABLE, Json(json!({ @@ -159,10 +159,10 @@ impl SearchHandler { } }; - // Limpiar la caché + // Clear the cache match search_service.clear_search_cache().await { Ok(_) => { - info!("Caché de búsqueda limpiada correctamente"); + info!("Search cache cleared successfully"); ( StatusCode::OK, Json(json!({ @@ -171,7 +171,7 @@ impl SearchHandler { ).into_response() }, Err(err) => { - error!("Error al limpiar caché de búsqueda: {}", err); + error!("Error clearing search cache: {}", err); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ @@ -183,43 +183,43 @@ impl SearchHandler { } } -/// Parámetros de búsqueda para el endpoint GET +/// Search parameters for the GET endpoint #[derive(Debug, serde::Deserialize)] pub struct SearchParams { - /// Texto a buscar en nombres de archivos y carpetas + /// Text to search for in file and folder names pub query: Option, - /// Filtro por tipos de archivo (extensiones separadas por comas) + /// Filter by file types (comma-separated extensions) #[serde(rename = "type")] pub type_filter: Option, - /// Filtrar elementos creados después de esta fecha (timestamp) + /// Filter items created after this date (timestamp) pub created_after: Option, - /// Filtrar elementos creados antes de esta fecha (timestamp) + /// Filter items created before this date (timestamp) pub created_before: Option, - /// Filtrar elementos modificados después de esta fecha (timestamp) + /// Filter items modified after this date (timestamp) pub modified_after: Option, - /// Filtrar elementos modificados antes de esta fecha (timestamp) + /// Filter items modified before this date (timestamp) pub modified_before: Option, - /// Tamaño mínimo en bytes + /// Minimum size in bytes pub min_size: Option, - /// Tamaño máximo en bytes + /// Maximum size in bytes pub max_size: Option, - /// ID de carpeta para limitar la búsqueda + /// Folder ID to limit the search scope pub folder_id: Option, - /// Búsqueda recursiva en subcarpetas + /// Recursive search in subfolders pub recursive: Option, - /// Límite de resultados para paginación + /// Result limit for pagination pub limit: Option, - /// Desplazamiento para paginación + /// Offset for pagination pub offset: Option, } \ No newline at end of file diff --git a/src/interfaces/api/handlers/trash_handler.rs b/src/interfaces/api/handlers/trash_handler.rs index 9810afb7..4716fc90 100644 --- a/src/interfaces/api/handlers/trash_handler.rs +++ b/src/interfaces/api/handlers/trash_handler.rs @@ -8,7 +8,7 @@ use tracing::{debug, error, warn, instrument}; use crate::common::di::AppState; use crate::interfaces::middleware::auth::AuthUser; -/// Obtiene todos los elementos en la papelera para el usuario actual +/// Gets all items in the trash for the current user #[instrument(skip_all)] pub async fn get_trash_items( State(state): State, @@ -19,7 +19,7 @@ pub async fn get_trash_items( // privilege escalation attacks. let effective_user = auth_user.id.clone(); - debug!("Solicitud para listar elementos en papelera para usuario {}", effective_user); + debug!("Request to list trash items for user {}", effective_user); let trash_service = match state.trash_service.as_ref() { Some(service) => service, @@ -34,11 +34,11 @@ pub async fn get_trash_items( match result { Ok(items) => { - debug!("Encontrados {} elementos en la papelera", items.len()); + debug!("Found {} items in trash", items.len()); (StatusCode::OK, Json(json!(items))) }, Err(e) => { - error!("Error al obtener elementos de la papelera: {:?}", e); + error!("Error retrieving trash items: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Error retrieving trash items: {}", e) }))) @@ -46,14 +46,14 @@ pub async fn get_trash_items( } } -/// Mueve un elemento (archivo o carpeta) a la papelera (función genérica, no usada directamente en rutas) +/// Moves an item (file or folder) to the trash (generic function, not used directly in routes) #[instrument(skip_all)] pub async fn move_to_trash( State(state): State, auth_user: AuthUser, Path((item_type, item_id)): Path<(String, String)>, ) -> (StatusCode, Json) { - debug!("Solicitud para mover a papelera: tipo={}, id={}, usuario={}", + debug!("Request to move to trash: type={}, id={}, user={}", item_type, item_id, auth_user.id); let trash_service = match state.trash_service.as_ref() { @@ -68,14 +68,14 @@ pub async fn move_to_trash( match result { Ok(_) => { - debug!("Elemento movido a papelera con éxito"); + debug!("Item moved to trash successfully"); (StatusCode::OK, Json(json!({ "success": true, "message": "Item moved to trash successfully" }))) }, Err(e) => { - error!("Error al mover elemento a papelera: {:?}", e); + error!("Error moving item to trash: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Error moving item to trash: {}", e) }))) @@ -83,14 +83,14 @@ pub async fn move_to_trash( } } -/// Mueve un archivo a la papelera +/// Moves a file to the trash #[instrument(skip_all)] pub async fn move_file_to_trash( State(state): State, auth_user: AuthUser, Path(item_id): Path, ) -> (StatusCode, Json) { - debug!("Solicitud para mover archivo a papelera: id={}, usuario={}", + debug!("Request to move file to trash: id={}, user={}", item_id, auth_user.id); let trash_service = match state.trash_service.as_ref() { @@ -102,19 +102,19 @@ pub async fn move_file_to_trash( } }; - // Especificar que es un archivo + // Specify that it is a file let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await; match result { Ok(_) => { - debug!("Archivo movido a papelera con éxito"); + debug!("File moved to trash successfully"); (StatusCode::OK, Json(json!({ "success": true, "message": "File moved to trash successfully" }))) }, Err(e) => { - error!("Error al mover archivo a papelera: {:?}", e); + error!("Error moving file to trash: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Error moving file to trash: {}", e) }))) @@ -122,14 +122,14 @@ pub async fn move_file_to_trash( } } -/// Mueve una carpeta a la papelera +/// Moves a folder to the trash #[instrument(skip_all)] pub async fn move_folder_to_trash( State(state): State, auth_user: AuthUser, Path(item_id): Path, ) -> (StatusCode, Json) { - debug!("Solicitud para mover carpeta a papelera: id={}, usuario={}", + debug!("Request to move folder to trash: id={}, user={}", item_id, auth_user.id); let trash_service = match state.trash_service.as_ref() { @@ -141,19 +141,19 @@ pub async fn move_folder_to_trash( } }; - // Especificar que es una carpeta + // Specify that it is a folder let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await; match result { Ok(_) => { - debug!("Carpeta movida a papelera con éxito"); + debug!("Folder moved to trash successfully"); (StatusCode::OK, Json(json!({ "success": true, "message": "Folder moved to trash successfully" }))) }, Err(e) => { - error!("Error al mover carpeta a papelera: {:?}", e); + error!("Error moving folder to trash: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Error moving folder to trash: {}", e) }))) @@ -161,14 +161,14 @@ pub async fn move_folder_to_trash( } } -/// Restaura un elemento desde la papelera a su ubicación original +/// Restores an item from the trash to its original location #[instrument(skip_all)] pub async fn restore_from_trash( State(state): State, auth_user: AuthUser, Path(trash_id): Path, ) -> (StatusCode, Json) { - debug!("Solicitud para restaurar elemento {} de papelera", trash_id); + debug!("Request to restore item {} from trash", trash_id); let trash_service = match state.trash_service.as_ref() { Some(service) => service, @@ -182,7 +182,7 @@ pub async fn restore_from_trash( match result { Ok(_) => { - debug!("Elemento restaurado con éxito"); + debug!("Item restored successfully"); (StatusCode::OK, Json(json!({ "success": true, "message": "Item restored successfully" @@ -199,7 +199,7 @@ pub async fn restore_from_trash( }))); } - error!("Error al restaurar elemento de papelera: {:?}", e); + error!("Error restoring item from trash: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Error restoring item from trash: {}", e) }))) @@ -207,14 +207,14 @@ pub async fn restore_from_trash( } } -/// Elimina permanentemente un elemento de la papelera +/// Permanently deletes an item from the trash #[instrument(skip_all)] pub async fn delete_permanently( State(state): State, auth_user: AuthUser, Path(trash_id): Path, ) -> (StatusCode, Json) { - debug!("Solicitud para eliminar permanentemente elemento {}", trash_id); + debug!("Request to permanently delete item {}", trash_id); let trash_service = match state.trash_service.as_ref() { Some(service) => service, @@ -228,7 +228,7 @@ pub async fn delete_permanently( match result { Ok(_) => { - debug!("Elemento eliminado permanentemente"); + debug!("Item permanently deleted"); (StatusCode::OK, Json(json!({ "success": true, "message": "Item deleted permanently" @@ -245,7 +245,7 @@ pub async fn delete_permanently( }))); } - error!("Error al eliminar permanentemente elemento: {:?}", e); + error!("Error permanently deleting item: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Error deleting item permanently: {}", e) }))) @@ -253,13 +253,13 @@ pub async fn delete_permanently( } } -/// Vacía la papelera completamente para el usuario actual +/// Empties the trash completely for the current user #[instrument(skip_all)] pub async fn empty_trash( State(state): State, auth_user: AuthUser, ) -> (StatusCode, Json) { - debug!("Solicitud para vaciar papelera del usuario {}", auth_user.id); + debug!("Request to empty trash for user {}", auth_user.id); let trash_service = match state.trash_service.as_ref() { Some(service) => service, @@ -273,14 +273,14 @@ pub async fn empty_trash( match result { Ok(_) => { - debug!("Papelera vaciada con éxito"); + debug!("Trash emptied successfully"); (StatusCode::OK, Json(json!({ "success": true, "message": "Trash emptied successfully" }))) }, Err(e) => { - error!("Error al vaciar papelera: {:?}", e); + error!("Error emptying trash: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Error emptying trash: {}", e) }))) diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index 1318c27e..94c6d834 100644 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -90,14 +90,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router { let favorites_service = app_state.favorites_service.clone(); let recent_service = app_state.recent_service.clone(); - // Inicializar el servicio de operaciones por lotes + // Initialize the batch operations service let batch_service = Arc::new(BatchOperationService::default( file_retrieval_service.clone(), file_management_service.clone(), folder_service.clone() )); - // Crear estado para el manejador de operaciones por lotes + // Create state for the batch operations handler let batch_handler_state = BatchHandlerState { batch_service: batch_service.clone(), }; @@ -154,14 +154,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router { // Merge the routers let files_router = basic_file_router.merge(file_operations_router); - // Crear rutas para operaciones por lotes + // Create routes for batch operations let batch_router = Router::new() - // Operaciones de archivos + // File operations .route("/files/move", post(batch_handler::move_files_batch)) .route("/files/copy", post(batch_handler::copy_files_batch)) .route("/files/delete", post(batch_handler::delete_files_batch)) .route("/files/get", post(batch_handler::get_files_batch)) - // Operaciones de carpetas + // Folder operations .route("/folders/delete", post(batch_handler::delete_folders_batch)) .route("/folders/create", post(batch_handler::create_folders_batch)) .route("/folders/get", post(batch_handler::get_folders_batch)) @@ -183,7 +183,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router { Router::new() }; - // Implementaciones directas de handlers para compartir, sin depender de ShareHandler + // Direct handler implementations for sharing, without depending on ShareHandler // Create routes for shared resources management (requires auth) let share_router = if let Some(share_service) = share_service.clone() { diff --git a/src/interfaces/middleware/auth.rs b/src/interfaces/middleware/auth.rs index fb7ba601..04721b71 100644 --- a/src/interfaces/middleware/auth.rs +++ b/src/interfaces/middleware/auth.rs @@ -11,24 +11,24 @@ use crate::common::di::AppState; // Re-export CurrentUser from application layer for use in handlers pub use crate::application::dtos::user_dto::CurrentUser; -// Estructura para usar en extractores de Axum +// Structure for use in Axum extractors #[derive(Clone, Debug)] pub struct AuthUser { pub id: String, pub username: String, } -/// Extractor reutilizable que obtiene el user_id del usuario autenticado. -/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware. +/// Reusable extractor that gets the user_id of the authenticated user. +/// Automatically extracted from the `CurrentUser` inserted by the auth middleware. /// -/// Uso en handlers: +/// Usage in handlers: /// ```ignore /// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... } /// ``` #[derive(Clone, Debug)] pub struct CurrentUserId(pub String); -// Implementar FromRequestParts para AuthUser — permite usar `auth_user: AuthUser` en handlers +// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers impl FromRequestParts for AuthUser where S: Send + Sync, @@ -47,7 +47,7 @@ where } } -// Implementar FromRequestParts para CurrentUserId — extractor ligero solo para el user_id +// Implement FromRequestParts for CurrentUserId — lightweight extractor for user_id only impl FromRequestParts for CurrentUserId where S: Send + Sync, @@ -63,37 +63,37 @@ where } } -// Error para las operaciones de autenticación +// Error for authentication operations #[derive(Debug, thiserror::Error)] pub enum AuthError { - #[error("Token no proporcionado")] + #[error("Token not provided")] TokenNotProvided, - #[error("Token inválido: {0}")] + #[error("Invalid token: {0}")] InvalidToken(String), - #[error("Token expirado")] + #[error("Token expired")] TokenExpired, - #[error("Usuario no encontrado")] + #[error("User not found")] UserNotFound, - #[error("Acceso denegado: {0}")] + #[error("Access denied: {0}")] AccessDenied(String), - #[error("Servicio de autenticación no disponible")] + #[error("Authentication service unavailable")] AuthServiceUnavailable, } impl IntoResponse for AuthError { fn into_response(self) -> Response { let (status, error_message) = match self { - AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token no proporcionado".to_string()), + AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token not provided".to_string()), AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg), - AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()), - AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()), + AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired".to_string()), + AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "User not found".to_string()), AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg), - AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Servicio de autenticación no disponible".to_string()), + AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Authentication service unavailable".to_string()), }; let body = axum::Json(serde_json::json!({ @@ -104,24 +104,24 @@ impl IntoResponse for AuthError { } } -/// Middleware de autenticación seguro. +/// Secure authentication middleware. /// -/// Valida el token JWT contra el servicio de autenticación configurado. -/// No acepta bypasses, tokens mock, ni parámetros de URL para saltar validación. +/// Validates the JWT token against the configured authentication service. +/// Does not accept bypasses, mock tokens, or URL parameters to skip validation. pub async fn auth_middleware( State(state): State>, headers: HeaderMap, mut request: Request, next: Next, ) -> Result { - // Extraer el token Bearer del header Authorization + // Extract the Bearer token from the Authorization header let token_str = headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) .and_then(|value| value.strip_prefix("Bearer ")) .ok_or(AuthError::TokenNotProvided)?; - // Validar que el token no esté vacío + // Validate that the token is not empty let token_str = token_str.trim(); if token_str.is_empty() { return Err(AuthError::TokenNotProvided); @@ -129,7 +129,7 @@ pub async fn auth_middleware( tracing::debug!("Processing authentication token"); - // Validar el token usando el servicio de autenticación + // Validate the token using the authentication service if let Some(auth_service) = state.auth_service.as_ref() { let token_service = &auth_service.token_service; match token_service.validate_token(token_str) { @@ -146,25 +146,25 @@ pub async fn auth_middleware( }, Err(e) => { tracing::warn!("Token validation failed: {}", e); - return Err(AuthError::InvalidToken(format!("Token inválido: {}", e))); + return Err(AuthError::InvalidToken(format!("Invalid token: {}", e))); } } } - // Si no hay servicio de autenticación disponible, denegar acceso + // If no authentication service is available, deny access tracing::error!("Auth middleware invoked but auth service is not configured"); Err(AuthError::AuthServiceUnavailable) } -/// Middleware para verificar que el usuario autenticado tiene rol de administrador. +/// Middleware to verify that the authenticated user has an admin role. /// -/// Debe aplicarse DESPUÉS del auth_middleware, ya que depende de que -/// `CurrentUser` esté presente en las extensiones de la request. +/// Must be applied AFTER auth_middleware, as it depends on +/// `CurrentUser` being present in the request extensions. pub async fn require_admin( request: Request, next: Next, ) -> Response { - // Obtener el CurrentUser insertado por auth_middleware + // Get the CurrentUser inserted by auth_middleware if let Some(current_user) = request.extensions().get::() { if current_user.role == "admin" { tracing::debug!("Admin access granted for user: {}", current_user.username); @@ -175,7 +175,7 @@ pub async fn require_admin( tracing::warn!("Admin check failed: no authenticated user in request"); } - // Acceso denegado - let error = AuthError::AccessDenied("Se requiere rol de administrador".to_string()); + // Access denied + let error = AuthError::AccessDenied("Admin role required".to_string()); error.into_response() } \ No newline at end of file diff --git a/src/interfaces/middleware/cache.rs b/src/interfaces/middleware/cache.rs index f22ac858..d983e642 100644 --- a/src/interfaces/middleware/cache.rs +++ b/src/interfaces/middleware/cache.rs @@ -17,39 +17,39 @@ use std::future::Future; use bytes::Bytes; use tracing::{debug, info}; -const MAX_CACHE_ENTRIES: usize = 1000; // Máximo número de entradas en caché -const DEFAULT_MAX_AGE: u64 = 60; // Tiempo de vida por defecto en segundos +const MAX_CACHE_ENTRIES: usize = 1000; // Maximum number of cache entries +const DEFAULT_MAX_AGE: u64 = 60; // Default time-to-live in seconds -// Definición de tipos para mayor claridad +// Type definitions for clarity type CacheKey = String; type EntityTag = String; -/// Un valor almacenado en caché +/// A cached value #[derive(Clone)] struct CacheEntry { - /// El ETag calculado para este valor + /// The ETag calculated for this value etag: EntityTag, - /// Los datos serializados en bytes + /// The serialized data in bytes data: Option, - /// Las cabeceras originales + /// The original headers headers: HeaderMap, - /// Timestamp de cuando fue almacenado + /// Timestamp of when it was stored timestamp: SystemTime, - /// Tiempo de vida en segundos + /// Time-to-live in seconds max_age: u64, } -/// Cache para respuestas HTTP con soporte para ETag +/// Cache for HTTP responses with ETag support #[derive(Clone)] pub struct HttpCache { - /// Almacenamiento de entradas en caché + /// Cache entry storage cache: Arc>>, - /// Tiempo de vida por defecto para las entradas + /// Default time-to-live for entries default_max_age: u64, } impl HttpCache { - /// Crea una nueva instancia del caché + /// Creates a new cache instance pub fn new() -> Self { Self { cache: Arc::new(Mutex::new(HashMap::with_capacity(100))), @@ -57,7 +57,7 @@ impl HttpCache { } } - /// Crea una nueva instancia con un tiempo de vida especificado + /// Creates a new instance with a specified time-to-live pub fn with_max_age(max_age: u64) -> Self { Self { cache: Arc::new(Mutex::new(HashMap::with_capacity(100))), @@ -65,12 +65,12 @@ impl HttpCache { } } - /// Obtiene estadísticas del caché + /// Gets cache statistics pub fn stats(&self) -> (usize, usize) { let lock = self.cache.lock().unwrap(); let total = lock.len(); - // Contar entradas válidas + // Count valid entries let _now = SystemTime::now(); let valid = lock.values().filter(|entry| { match entry.timestamp.elapsed() { @@ -82,12 +82,12 @@ impl HttpCache { (total, valid) } - /// Limpia entradas expiradas + /// Cleans up expired entries pub fn cleanup(&self) -> usize { let mut lock = self.cache.lock().unwrap(); let initial_count = lock.len(); - // Eliminar entradas expiradas + // Remove expired entries let _now = SystemTime::now(); lock.retain(|_, entry| { match entry.timestamp.elapsed() { @@ -102,18 +102,18 @@ impl HttpCache { removed } - /// Establece una entrada en el caché + /// Sets an entry in the cache fn set(&self, key: &str, etag: EntityTag, data: Option, headers: HeaderMap, max_age: Option) { let mut lock = self.cache.lock().unwrap(); - // Aplicar política de eviction si el caché está lleno + // Apply eviction policy if the cache is full if lock.len() >= MAX_CACHE_ENTRIES { debug!("Cache full, removing oldest entries"); - // Eliminar el 10% de las entradas más antiguas + // Remove the oldest 10% of entries self.evict_oldest(&mut lock, MAX_CACHE_ENTRIES / 10); } - // Almacenar la nueva entrada + // Store the new entry lock.insert(key.to_string(), CacheEntry { etag, data, @@ -123,30 +123,30 @@ impl HttpCache { }); } - /// Elimina las entradas más antiguas del caché + /// Removes the oldest entries from the cache fn evict_oldest(&self, cache: &mut HashMap, count: usize) { - // Ordenar por timestamp + // Sort by timestamp let mut entries: Vec<(CacheKey, SystemTime)> = cache .iter() .map(|(key, entry)| (key.clone(), entry.timestamp)) .collect(); - // Ordenar por timestamp (más antiguo primero) + // Sort by timestamp (oldest first) entries.sort_by(|a, b| a.1.cmp(&b.1)); - // Eliminar las entradas más antiguas + // Remove the oldest entries for (key, _) in entries.iter().take(count) { cache.remove(key); } } - /// Obtiene una entrada del caché + /// Gets an entry from the cache fn get(&self, key: &str) -> Option { let lock = self.cache.lock().unwrap(); - // Buscar la entrada + // Look up the entry if let Some(entry) = lock.get(key) { - // Verificar si ha expirado + // Check if it has expired match entry.timestamp.elapsed() { Ok(elapsed) if elapsed.as_secs() < entry.max_age => { // Entry is still valid @@ -162,9 +162,9 @@ impl HttpCache { None } - /// Genera un ETag simple para un bloque de bytes + /// Generates a simple ETag for a block of bytes fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag { - // Calcular hash + // Calculate hash let mut hasher = DefaultHasher::new(); bytes.hash(&mut hasher); let hash = hasher.finish(); @@ -173,7 +173,7 @@ impl HttpCache { } } -/// Middleware de caché HTTP +/// HTTP cache middleware pub async fn cache_middleware( cache: HttpCache, cache_key: &str, @@ -184,65 +184,65 @@ pub async fn cache_middleware( where T: Serialize { - // Solo aplicar caché para solicitudes GET + // Only apply cache for GET requests if req.method() != Method::GET { return Ok(next.run(req).await); } - // Verificar si la respuesta está en caché + // Check if the response is cached let if_none_match = req.headers() .get("if-none-match") .and_then(|v| v.to_str().ok()); - // Si hay una entrada en caché + // If there is a cache entry if let Some(cache_entry) = cache.get(cache_key) { - // Comprobar si el cliente ya tiene la versión actualizada + // Check if the client already has the updated version if let Some(client_etag) = if_none_match { if client_etag == cache_entry.etag { - // El cliente tiene la versión más reciente, enviar 304 Not Modified + // The client has the most recent version, send 304 Not Modified debug!("Cache hit (304) for key: {}", cache_key); return Ok(create_not_modified_response(&cache_entry)); } } - // El cliente necesita la versión actualizada + // The client needs the updated version if let Some(data) = &cache_entry.data { debug!("Cache hit (200) for key: {}", cache_key); - // Crear respuesta con los datos en caché + // Create response with cached data let mut response = Response::new(Body::from(data.clone())); - // Copiar cabeceras originales + // Copy original headers for (key, value) in &cache_entry.headers { if !key.as_str().eq_ignore_ascii_case("transfer-encoding") { response.headers_mut().insert(key.clone(), value.clone()); } } - // Añadir cabeceras de caché + // Add cache headers set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age)); return Ok(response); } } - // No está en caché o ha expirado, continuar con el middleware + // Not cached or expired, continue with the middleware debug!("Cache miss for key: {}", cache_key); let response = next.run(req).await; - // No cachear errores + // Don't cache errors if !response.status().is_success() { return Ok(response); } - // Convertir la respuesta para calcular el ETag + // Convert the response to calculate the ETag let (parts, _body) = response.into_parts(); let bytes = axum::body::to_bytes(_body, 1024 * 1024 * 10).await.unwrap_or_default(); - // Calcular ETag + // Calculate ETag let etag = cache.calculate_etag_for_bytes(&bytes); - // Guardar en caché + // Save to cache cache.set( cache_key, etag.clone(), @@ -251,26 +251,26 @@ where max_age ); - // Crear la respuesta con ETag + // Create the response with ETag let mut response = Response::from_parts(parts, Body::from(bytes)); set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age)); Ok(response) } -/// Crea una respuesta 304 Not Modified +/// Creates a 304 Not Modified response fn create_not_modified_response(entry: &CacheEntry) -> Response { let mut response = Response::builder() .status(StatusCode::NOT_MODIFIED) .body(Body::empty()) .unwrap(); - // Copiar cabeceras de caché + // Copy cache headers if let Some(cache_control) = entry.headers.get("cache-control") { response.headers_mut().insert("cache-control", cache_control.clone()); } - // Añadir ETag + // Add ETag response.headers_mut().insert( "etag", HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static("")) @@ -279,22 +279,22 @@ fn create_not_modified_response(entry: &CacheEntry) -> Response { response } -/// Configura las cabeceras de caché para una respuesta +/// Configures cache headers for a response fn set_cache_headers(response: &mut Response, etag: &str, max_age: u64) { - // Añadir ETag + // Add ETag response.headers_mut().insert( "etag", HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static("")) ); - // Configurar Cache-Control + // Configure Cache-Control let cache_control = format!("public, max-age={}", max_age); response.headers_mut().insert( "cache-control", HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static("")) ); - // Añadir cabecera Last-Modified + // Add Last-Modified header let now: DateTime = Utc::now(); let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); response.headers_mut().insert( @@ -303,7 +303,7 @@ fn set_cache_headers(response: &mut Response, etag: &str, max_age: u64) { ); } -/// Layer para aplicar middleware de caché +/// Layer for applying cache middleware #[derive(Clone)] pub struct HttpCacheLayer { cache: HttpCache, @@ -311,7 +311,7 @@ pub struct HttpCacheLayer { } impl HttpCacheLayer { - /// Crea una nueva capa de caché + /// Creates a new cache layer pub fn new(cache: HttpCache) -> Self { Self { cache, @@ -319,7 +319,7 @@ impl HttpCacheLayer { } } - /// Establece el tiempo de vida máximo + /// Sets the maximum time-to-live pub fn with_max_age(mut self, max_age: u64) -> Self { self.max_age = Some(max_age); self @@ -338,7 +338,7 @@ impl Layer for HttpCacheLayer { } } -/// Servicio que implementa la lógica de caché +/// Service that implements cache logic #[derive(Clone)] pub struct HttpCacheService { inner: S, @@ -365,10 +365,10 @@ where } fn call(&mut self, req: Request) -> Self::Future { - // Generar clave de caché + // Generate cache key let cache_key = req.uri().path().to_string(); - // Solo aplicar caché para solicitudes GET + // Only apply cache for GET requests if req.method() != Method::GET { let future = self.inner.call(req); return Box::pin(async move { @@ -377,42 +377,42 @@ where }); } - // Obtener ETag del cliente + // Get client ETag let if_none_match = req.headers() .get("if-none-match") .and_then(|v| v.to_str().ok()); - // Verificar si hay una entrada en caché + // Check if there is a cache entry let cache_clone = self.cache.clone(); let max_age = self.max_age; let entry = cache_clone.get(&cache_key); match entry { Some(cache_entry) if if_none_match == Some(&cache_entry.etag) => { - // El cliente tiene la versión correcta, enviar 304 + // The client has the correct version, send 304 debug!("Cache HIT (304): {}", cache_key); let response = create_not_modified_response(&cache_entry); return Box::pin(async move { Ok(response) }); }, Some(cache_entry) if cache_entry.data.is_some() => { - // El cliente necesita la versión actualizada + // The client needs the updated version debug!("Cache HIT (200): {}", cache_key); let mut response = Response::new(Body::from(cache_entry.data.clone().unwrap())); - // Copiar cabeceras originales + // Copy original headers for (key, value) in &cache_entry.headers { if !key.as_str().eq_ignore_ascii_case("transfer-encoding") { response.headers_mut().insert(key.clone(), value.clone()); } } - // Añadir cabeceras de caché + // Add cache headers set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age)); return Box::pin(async move { Ok(response) }); }, _ => { - // No está en caché o ha expirado + // Not cached or expired debug!("Cache MISS: {}", cache_key); let future = self.inner.call(req); let cache_clone = self.cache.clone(); @@ -423,19 +423,19 @@ where let response = future.await.map_err(|e| e.into())?; let response = response_map_body(response).await; - // No cachear errores + // Don't cache errors if !response.status().is_success() { return Ok(response); } - // Obtener el cuerpo y calcular ETag + // Get the body and calculate ETag let (parts, body) = response.into_parts(); let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?; - // Calcular ETag + // Calculate ETag let etag = cache_clone.calculate_etag_for_bytes(&bytes); - // Guardar en caché + // Save to cache cache_clone.set( &cache_key, etag.clone(), @@ -444,7 +444,7 @@ where max_age ); - // Crear la respuesta con ETag + // Create the response with ETag let mut response = Response::from_parts(parts, Body::from(bytes)); set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache_clone.default_max_age)); @@ -455,9 +455,9 @@ where } } -// Función auxiliar para convertir cualquier cuerpo en Body preservando su contenido. -// Anteriormente esta función descartaba el body con Body::empty(), causando -// pérdida de datos en respuestas no cacheadas. +// Helper function to convert any body into Body preserving its content. +// Previously this function discarded the body with Body::empty(), causing +// data loss in non-cached responses. async fn response_map_body(response: Response) -> Response where B: http_body::Body + Send + 'static, @@ -478,10 +478,10 @@ where Response::from_parts(parts, Body::from(collected)) } -/// Inicia una tarea de limpieza periódica para el caché +/// Starts a periodic cleanup task for the cache pub fn start_cache_cleanup_task(cache: HttpCache) { tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(300)); // Cada 5 minutos + let mut interval = tokio::time::interval(Duration::from_secs(300)); // Every 5 minutes loop { interval.tick().await; @@ -516,10 +516,10 @@ mod tests { let etag2 = cache.calculate_etag_for_bytes(&data2); let etag3 = cache.calculate_etag_for_bytes(&data3); - // Mismos datos deben generar mismo ETag + // Same data should generate the same ETag assert_eq!(etag1, etag2); - // Datos diferentes deben generar ETags diferentes + // Different data should generate different ETags assert_ne!(etag1, etag3); } @@ -527,19 +527,19 @@ mod tests { async fn test_cache_hit_miss() { let cache = HttpCache::new(); - // Crear datos de prueba directamente como Bytes + // Create test data directly as Bytes let bytes1 = Bytes::from(r#"{"id":1,"name":"Test"}"#); let headers1 = HeaderMap::new(); let etag1 = cache.calculate_etag_for_bytes(&bytes1); cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None); - // Verificar cache hit + // Verify cache hit let entry = cache.get("test").unwrap(); assert_eq!(entry.etag, etag1); assert_eq!(entry.data.unwrap(), bytes1); - // Verificar cache miss + // Verify cache miss assert!(cache.get("nonexistent").is_none()); } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8fb338fd..753de83a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ -// Exportar los módulos principales del proyecto +// Export the main project modules pub mod common; pub mod domain; pub mod application; pub mod infrastructure; pub mod interfaces; -// Re-exportaciones públicas comunes +// Common public re-exports pub use application::services::folder_service::FolderService; pub use application::services::i18n_application_service::I18nApplicationService; pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator}; diff --git a/static/css/favorites.css b/static/css/favorites.css index d1832ca7..c37c4c81 100644 --- a/static/css/favorites.css +++ b/static/css/favorites.css @@ -1,6 +1,6 @@ -/* Estilos para la funcionalidad de favoritos */ +/* Styles for the favorites feature */ -/* Indicador de favorito */ +/* Favorite indicator */ .favorite-indicator { position: absolute; top: 10px; @@ -31,12 +31,12 @@ text-shadow: 0 0 5px rgba(255, 193, 7, 0.5); } -/* Estilos para los elementos de la vista de favoritos */ +/* Styles for the favorites view items */ .favorite-item { position: relative; } -/* Ajustes para la vista de cuadrícula */ +/* Adjustments for grid view */ .file-card.favorite-item { border-left: 3px solid #ffc107; @@ -46,7 +46,7 @@ } } -/* Ajustes para la vista de lista */ +/* Adjustments for list view */ .file-item.favorite-item { position: relative; grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px; @@ -60,7 +60,7 @@ height: 30px; } -/* Estilos para el estado vacío específico de favoritos */ +/* Styles for the favorites-specific empty state */ .favorites-empty-state { display: flex; flex-direction: column; @@ -83,7 +83,7 @@ max-width: 400px; } -/* Animación para la estrella de favorito */ +/* Animation for the favorite star */ @keyframes favorite-pulse { 0% { transform: scale(1); } 50% { transform: scale(1.2); } diff --git a/static/css/recent.css b/static/css/recent.css index a54ec54c..7da7ef71 100644 --- a/static/css/recent.css +++ b/static/css/recent.css @@ -1,6 +1,6 @@ -/* Estilos para la funcionalidad de archivos recientes */ +/* Styles for the recent files feature */ -/* Indicador de reciente */ +/* Recent indicator */ .recent-indicator { position: absolute; top: 10px; @@ -20,12 +20,12 @@ } } -/* Estilos para los elementos de la vista de recientes */ +/* Styles for the recent view items */ .recent-item { position: relative; } -/* Ajustes para la vista de cuadrícula */ +/* Adjustments for grid view */ .file-card.recent-item { border-left: 3px solid #6c757d; @@ -35,7 +35,7 @@ } } -/* Ajustes para la vista de lista */ +/* Adjustments for list view */ .file-item.recent-item { position: relative; grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px; @@ -49,7 +49,7 @@ height: 30px; } -/* Estilos para el estado vacío específico de recientes */ +/* Styles for the recent-specific empty state */ .recents-empty-state { display: flex; flex-direction: column; @@ -72,12 +72,12 @@ max-width: 400px; } -/* Tooltip para tiempo de acceso */ +/* Tooltip for access time */ .recent-item .file-info { cursor: help; } -/* Animación para archivos recientes */ +/* Animation for recent files */ .recent-item { animation: recent-fade-in 0.3s ease; } diff --git a/static/css/style.css b/static/css/style.css index 06cd2576..bf445dd0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1271,7 +1271,7 @@ select:focus { .file-icon.doc-icon { width: 100px; height: 70px; - background-color: #e2e8f0; /* Fondo gris claro */ + background-color: #e2e8f0; /* Light gray background */ border-radius: 4px; position: relative; margin-bottom: 10px; @@ -1436,7 +1436,7 @@ select:focus { .file-icon.code-icon { width: 100px; height: 70px; - background-color: #e2e8f0; /* Fondo gris claro */ + background-color: #e2e8f0; /* Light gray background */ border-radius: 4px; position: relative; margin-bottom: 10px; @@ -1770,7 +1770,7 @@ select:focus { text-align: center; } -/* Lista de archivos - Estilo mejorado */ +/* File list - Improved style */ .files-list-view { display: flex; flex-direction: column; @@ -1802,7 +1802,7 @@ select:focus { background-color: white; } -/* Para modo papelera, ajustar columnas */ +/* For trash mode, adjust columns */ .trash-item.file-item { grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 120px 100px; } @@ -1851,7 +1851,7 @@ select:focus { display: none; } -/* Iconos específicos para archivos en vista de lista */ +/* Specific icons for files in list view */ .file-item .file-icon.pdf-icon { background-color: #fee2e2; } @@ -2129,7 +2129,7 @@ select:focus { transition: width 0.3s; } -/* Notificación */ +/* Notification */ .notification { position: absolute; top: 70px; @@ -3327,7 +3327,7 @@ header { } } -/* Estilos específicos para la página Shared */ +/* Specific styles for the Shared page */ .page-description { color: #718096; font-size: 16px; @@ -3335,7 +3335,7 @@ header { margin-bottom: 25px; } -/* Mejoras para los filtros en la página Shared */ +/* Improvements for the Shared page filters */ .shared-filters { display: flex; justify-content: space-between; @@ -3383,7 +3383,7 @@ header { width: 250px; } -/* Mejoras en la tabla de elementos compartidos */ +/* Improvements in the shared items table */ .shared-list-container { background-color: white; border-radius: 10px; @@ -3412,7 +3412,7 @@ header { vertical-align: middle; } -/* Estado vacío mejorado */ +/* Improved empty state */ .empty-state { display: flex; flex-direction: column; diff --git a/static/index.html b/static/index.html index 47965f97..3f411303 100644 --- a/static/index.html +++ b/static/index.html @@ -27,7 +27,7 @@ - + @@ -94,7 +94,7 @@
- + @@ -110,8 +110,8 @@
AD
-
Usuario
-
usuario@oxicloud.app
+
User
+
user@oxicloud.app
@@ -159,38 +159,38 @@
-

Archivos

+

Files

- -
@@ -198,7 +198,7 @@
-

Arrastra archivos aquí o haz clic para seleccionar

+

Drag files here or click to select

@@ -222,10 +222,10 @@ @@ -240,18 +240,18 @@ - +
@@ -281,7 +281,7 @@ MIT License
- + diff --git a/static/js/app.js b/static/js/app.js index 3ce7a568..467086c9 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -197,7 +197,7 @@ function setupUserMenu() { // Theme switching could be expanded here in the future window.ui.showNotification( dark ? '🌙' : '☀️', - dark ? 'Modo oscuro activado (próximamente)' : 'Modo claro activado' + dark ? 'Dark mode enabled (coming soon)' : 'Light mode enabled' ); } }); @@ -476,12 +476,12 @@ function setupEventListeners() { if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none'; // Update UI - elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Papelera'; + elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash'; elements.actionsBar.innerHTML = `
`; @@ -519,35 +519,35 @@ function setupEventListeners() { app.currentSection = 'files'; // Reset UI - elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Archivos'; + elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files'; elements.actionsBar.innerHTML = `
- -
@@ -623,14 +623,14 @@ function setupEventListeners() { */ async function loadFiles(options = {}) { try { - console.log("Iniciando loadFiles() - cargando archivos...", options); + console.log("Starting loadFiles() - loading files...", options); - // Flag para forzar el refresco completo ignorando caché + // Flag to force complete refresh ignoring cache const forceRefresh = options.forceRefresh || false; - // Prevenir múltiples solicitudes de carga simultáneas + // Prevent multiple simultaneous load requests if (window.isLoadingFiles) { - console.log("Ya hay una carga de archivos en progreso, ignorando solicitud"); + console.log("A file load is already in progress, ignoring request"); return; } @@ -640,7 +640,7 @@ async function loadFiles(options = {}) { elements.filesGrid.innerHTML = `
- ${window.i18n ? window.i18n.t('files.loading') : 'Cargando archivos…'} + ${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}
`; @@ -651,12 +651,12 @@ async function loadFiles(options = {}) { const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); if (userData.username) { // Find user's home folder - console.log("Buscando carpeta de usuario para", userData.username); + console.log("Looking for user folder for", userData.username); await findUserHomeFolder(userData.username); } } - // Agregar timestamp para evitar caché + // Add timestamp to avoid cache const timestamp = new Date().getTime(); let url; @@ -667,7 +667,7 @@ async function loadFiles(options = {}) { url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`; app.currentPath = app.userHomeFolderId; ui.updateBreadcrumb(app.userHomeFolderName || 'Home'); - console.log(`Cargando carpeta del usuario: ${app.userHomeFolderName} (${app.userHomeFolderId})`); + console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`); } else { // Emergency fallback - this should rarely happen but prevents errors url = `/api/folders?t=${timestamp}`; @@ -676,7 +676,7 @@ async function loadFiles(options = {}) { } else { // Normal case - viewing subfolder contents url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`; - console.log(`Cargando contenido de subcarpeta: ${app.currentPath}`); + console.log(`Loading subfolder content: ${app.currentPath}`); } const token = localStorage.getItem('oxicloud_token'); @@ -686,14 +686,14 @@ async function loadFiles(options = {}) { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' }, - cache: 'no-store' // Instruir al navegador a no usar caché + cache: 'no-store' // Instruct the browser not to use cache }; - // Si se especifica forceRefresh, agregar un parámetro adicional para evitar caché + // If forceRefresh is specified, add an additional parameter to avoid cache if (forceRefresh) { url += `&force_refresh=true`; requestOptions.headers['X-Force-Refresh'] = 'true'; - console.log('Forzando refresco completo ignorando caché'); + console.log('Forcing complete refresh ignoring cache'); } console.log(`Loading files from ${url}`); @@ -703,13 +703,13 @@ async function loadFiles(options = {}) { if (response.status === 401 || response.status === 403) { console.warn("Auth error when loading files, showing empty list"); // Just show empty state instead of causing redirect loops - elements.filesGrid.innerHTML = '

No se pudieron cargar los archivos

'; + elements.filesGrid.innerHTML = '

Could not load files

'; elements.filesListView.innerHTML = `
-
Nombre
-
Tipo
-
Tamaño
-
Modificado
+
Name
+
Type
+
Size
+
Modified
`; return; @@ -724,10 +724,10 @@ async function loadFiles(options = {}) { elements.filesGrid.innerHTML = ''; elements.filesListView.innerHTML = `
-
Nombre
-
Tipo
-
Tamaño
-
Modificado
+
Name
+
Type
+
Size
+
Modified
`; @@ -766,11 +766,11 @@ async function loadFiles(options = {}) { // Also load files in this folder const cacheTimestamp = new Date().getTime(); - let filesUrl = `/api/files?t=${cacheTimestamp}`; // Agregar timestamp para evitar problemas de caché + let filesUrl = `/api/files?t=${cacheTimestamp}`; // Add timestamp to avoid cache issues if (app.currentPath) { filesUrl += `&folder_id=${app.currentPath}`; } - console.log(`Cargando archivos desde: ${filesUrl}`); + console.log(`Loading files from: ${filesUrl}`); try { console.log(`Fetching files from: ${filesUrl}`); @@ -810,7 +810,7 @@ async function loadFiles(options = {}) { console.error('Error loading folders:', error); ui.showNotification('Error', 'Could not load files and folders'); } finally { - // Marcar que ya no estamos cargando archivos para permitir solicitudes futuras + // Mark that we are no longer loading files to allow future requests window.isLoadingFiles = false; } } @@ -839,11 +839,11 @@ async function loadTrashItems() { elements.filesGrid.innerHTML = ''; elements.filesListView.innerHTML = `
-
Nombre
-
Tipo
-
Ubicación original
-
Fecha eliminación
-
Acciones
+
Name
+
Type
+
Original location
+
Deletion date
+
Actions
`; @@ -864,7 +864,7 @@ async function loadTrashItems() { emptyState.className = 'empty-state'; emptyState.innerHTML = ` -

${window.i18n ? window.i18n.t('trash.empty_state') : 'La papelera está vacía'}

+

${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}

`; elements.filesGrid.appendChild(emptyState); return; @@ -877,7 +877,7 @@ async function loadTrashItems() { } catch (error) { console.error('Error loading trash items:', error); - window.ui.showNotification('Error', 'Error al cargar elementos de la papelera'); + window.ui.showNotification('Error', 'Error loading trash items'); } } @@ -896,8 +896,8 @@ function addTrashItemToView(item) { // Item type label const typeLabel = isFile ? - (window.i18n ? window.i18n.t('files.file_types.file') : 'Archivo') : - (window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'); + (window.i18n ? window.i18n.t('files.file_types.file') : 'File') : + (window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'); // Grid view element const gridElement = document.createElement('div'); @@ -912,10 +912,10 @@ function addTrashItemToView(item) {
${item.name}
${typeLabel} - ${formattedDate}
- -
@@ -956,10 +956,10 @@ function addTrashItemToView(item) {
${item.original_path || '--'}
${formattedDate}
- -
@@ -995,7 +995,7 @@ async function performSearch(query) { app.isSearchMode = true; // Set breadcrumb for search - ui.updateBreadcrumb(`Búsqueda: "${query}"`); + ui.updateBreadcrumb(`Search: "${query}"`); // Prepare search options const options = { @@ -1037,7 +1037,7 @@ async function performSearch(query) { } catch (error) { console.error('Search error:', error); - window.ui.showNotification('Error', 'Error al realizar la búsqueda'); + window.ui.showNotification('Error', 'Error performing search'); } } @@ -1076,7 +1076,7 @@ function switchToSharedView() { } // Update UI - elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Compartidos'; + elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Shared'; // Clear breadcrumb and show root ui.updateBreadcrumb(''); @@ -1105,7 +1105,7 @@ function switchToFilesView() { app.currentSection = 'files'; // Update UI - elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Archivos'; + elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files'; // Remove active class from all nav items elements.navItems.forEach(navItem => navItem.classList.remove('active')); @@ -1122,29 +1122,29 @@ function switchToFilesView() {
- -
@@ -1219,7 +1219,7 @@ function switchToFavoritesView() { } // Update UI - elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favoritos'; + elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favorites'; // Clear breadcrumb and show root ui.updateBreadcrumb(''); @@ -1235,10 +1235,10 @@ function switchToFavoritesView() {
- -
@@ -1278,7 +1278,7 @@ function switchToFavoritesView() { filesGrid.innerHTML = `
-

Error al cargar el módulo de favoritos

+

Error loading the favorites module

`; } @@ -1308,7 +1308,7 @@ function switchToRecentFilesView() { } // Update UI - elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recientes'; + elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recent'; // Clear breadcrumb and show root ui.updateBreadcrumb(''); @@ -1322,14 +1322,14 @@ function switchToRecentFilesView() { elements.actionsBar.innerHTML = `
- -
@@ -1341,7 +1341,7 @@ function switchToRecentFilesView() { if (window.recent) { window.recent.clearRecentFiles(); window.recent.displayRecentFiles(); - window.ui.showNotification('Limpieza completada', 'Se ha limpiado el historial de archivos recientes'); + window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared'); } }); @@ -1378,7 +1378,7 @@ function switchToRecentFilesView() { filesGrid.innerHTML = `
-

Error al cargar el módulo de archivos recientes

+

Error loading the recent files module

`; } @@ -1450,7 +1450,7 @@ window.refreshUserData = refreshUserData; function showUserProfileModal() { const USER_DATA_KEY = 'oxicloud_user'; const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); - const username = userData.username || 'Usuario'; + const username = userData.username || 'User'; const email = userData.email || ''; const role = userData.role || 'user'; const initials = username.substring(0, 2).toUpperCase(); @@ -1847,7 +1847,7 @@ async function findUserHomeFolder(username) { * Logout - clear all auth data and redirect to login */ function logout() { - // Nombres de variables según auth.js + // Variable names as per auth.js const TOKEN_KEY = 'oxicloud_token'; const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token'; const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry'; diff --git a/static/js/auth.js b/static/js/auth.js index 93db5b32..41a437c8 100644 --- a/static/js/auth.js +++ b/static/js/auth.js @@ -34,7 +34,7 @@ const LANGUAGE_TEXTS = { subtitle: 'Por favor, selecciona tu idioma', continue: 'Continuar', autodetected: 'Hemos detectado tu idioma', - moreLanguages: 'Más idiomas...', + moreLanguages: 'More languages...', modalTitle: 'Seleccionar idioma', searchPlaceholder: 'Buscar idioma...' }, @@ -494,8 +494,8 @@ document.addEventListener('DOMContentLoaded', () => { console.error('Error showing initial panel:', err); }); - // Siempre limpiar los contadores al cargar la página de login - // para asegurar que no quedamos atrapados en un bucle + // Always clear counters when loading the login page + // to ensure we don't get trapped in a loop console.log('Login page loaded, clearing all counters'); sessionStorage.removeItem('redirect_count'); localStorage.removeItem('refresh_attempts'); @@ -589,14 +589,14 @@ if (isLoginPage && loginForm) { localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); - // Extraer fecha de expiración desde el token JWT + // Extract expiration date from the JWT token let parsedExpiry = false; const tokenParts = token.split('.'); if (tokenParts.length === 3) { try { const payload = JSON.parse(atob(tokenParts[1])); if (payload.exp) { - // payload.exp está en segundos desde epoch + // payload.exp is in seconds since epoch const expiryDate = new Date(payload.exp * 1000); // Verify the date is valid @@ -642,7 +642,7 @@ if (isLoginPage && loginForm) { // Redirect to main app redirectToMainApp(); } catch (error) { - loginError.textContent = error.message || 'Error al iniciar sesión'; + loginError.textContent = error.message || 'Error logging in'; loginError.style.display = 'block'; } }); @@ -664,7 +664,7 @@ if (isLoginPage && registerForm) { // Validate passwords match if (password !== confirmPassword) { - const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden'; + const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match'; registerError.textContent = errorMsg; registerError.style.display = 'block'; return; @@ -674,7 +674,7 @@ if (isLoginPage && registerForm) { const data = await register(username, email, password); // Show success message - const successMsg = window.i18n ? window.i18n.t('auth.account_success') : '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.'; + const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.'; registerSuccess.textContent = successMsg; registerSuccess.style.display = 'block'; @@ -687,7 +687,7 @@ if (isLoginPage && registerForm) { registerPanel.style.display = 'none'; }, 2000); } catch (error) { - const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al registrar cuenta'; + const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error registering account'; registerError.textContent = error.message || errorMsg; registerError.style.display = 'block'; } @@ -710,7 +710,7 @@ if (isLoginPage && adminSetupForm) { // Validate passwords match if (password !== confirmPassword) { - const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden'; + const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match'; adminSetupError.textContent = errorMsg; adminSetupError.style.display = 'block'; return; @@ -721,7 +721,7 @@ if (isLoginPage && adminSetupForm) { const data = await register('admin', email, password, 'admin'); // Show success message in the GUI instead of alert - const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : '¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.'; + const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.'; if (adminSetupSuccess) { adminSetupSuccess.textContent = successMsg; @@ -736,7 +736,7 @@ if (isLoginPage && adminSetupForm) { }, 2000); } catch (error) { - const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al crear cuenta de administrador'; + const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error creating admin account'; adminSetupError.textContent = error.message || errorMsg; adminSetupError.style.display = 'block'; } @@ -792,10 +792,10 @@ async function login(username, password) { if (!response.ok) { try { const errorData = await response.json(); - throw new Error(errorData.error || 'Falló la autenticación'); + throw new Error(errorData.error || 'Authentication failed'); } catch (jsonError) { // If the error response is not valid JSON - throw new Error(`Error de autenticación (${response.status}): ${response.statusText}`); + throw new Error(`Authentication error (${response.status}): ${response.statusText}`); } } @@ -806,7 +806,7 @@ async function login(username, password) { return data; } catch (jsonError) { console.error('Error parsing login response:', jsonError); - throw new Error('Error al procesar la respuesta del servidor'); + throw new Error('Error processing server response'); } } catch (error) { console.error('Login error:', error); @@ -848,10 +848,10 @@ async function register(username, email, password, role = 'user') { if (!response.ok) { try { const errorData = await response.json(); - throw new Error(errorData.error || 'Error en el registro'); + throw new Error(errorData.error || 'Registration error'); } catch (jsonError) { // If the error response is not valid JSON - throw new Error(`Error de registro (${response.status}): ${response.statusText}`); + throw new Error(`Registration error (${response.status}): ${response.statusText}`); } } @@ -862,7 +862,7 @@ async function register(username, email, password, role = 'user') { return data; } catch (jsonError) { console.error('Error parsing registration response:', jsonError); - throw new Error('Error al procesar la respuesta del servidor'); + throw new Error('Error processing server response'); } } catch (error) { console.error('Registration error:', error); @@ -883,7 +883,7 @@ async function fetchUserData(token) { }); if (!response.ok) { - throw new Error('Error al obtener datos del usuario'); + throw new Error('Error fetching user data'); } return await response.json(); @@ -1092,7 +1092,7 @@ function redirectToMainApp() { window.location.href = '/login.html?error=redirect_fatal'; } catch (e) { // Nothing more we can do - alert('Error crítico en la redirección. Por favor, recarga la página e intenta nuevamente.'); + alert('Critical redirect error. Please reload the page and try again.'); } } diff --git a/static/js/contextMenus.js b/static/js/contextMenus.js index 3318c7a4..50f9a313 100644 --- a/static/js/contextMenus.js +++ b/static/js/contextMenus.js @@ -30,7 +30,7 @@ const contextMenus = { window.favorites.removeFromFavorites(folder.id, 'folder'); // Update menu item text document.getElementById('favorite-folder-option').querySelector('span').textContent = - window.i18n ? window.i18n.t('actions.favorite') : 'Añadir a favoritos'; + window.i18n ? window.i18n.t('actions.favorite') : 'Add to favorites'; } else { // Add to favorites window.favorites.addToFavorites( @@ -41,7 +41,7 @@ const contextMenus = { ); // Update menu item text document.getElementById('favorite-folder-option').querySelector('span').textContent = - window.i18n ? window.i18n.t('actions.unfavorite') : 'Quitar de favoritos'; + window.i18n ? window.i18n.t('actions.unfavorite') : 'Remove from favorites'; } } window.ui.closeContextMenu(); @@ -143,7 +143,7 @@ const contextMenus = { window.favorites.removeFromFavorites(file.id, 'file'); // Update menu item text document.getElementById('favorite-file-option').querySelector('span').textContent = - window.i18n ? window.i18n.t('actions.favorite') : 'Añadir a favoritos'; + window.i18n ? window.i18n.t('actions.favorite') : 'Add to favorites'; } else { // Add to favorites window.favorites.addToFavorites( @@ -154,7 +154,7 @@ const contextMenus = { ); // Update menu item text document.getElementById('favorite-file-option').querySelector('span').textContent = - window.i18n ? window.i18n.t('actions.unfavorite') : 'Quitar de favoritos'; + window.i18n ? window.i18n.t('actions.unfavorite') : 'Remove from favorites'; } } window.ui.closeFileContextMenu(); @@ -246,7 +246,7 @@ const contextMenus = { renameInput.value = folder.name; // Update header text const headerSpan = renameDialog.querySelector('.rename-dialog-header span'); - if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_folder') : 'Renombrar carpeta'; + if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_folder') : 'Rename folder'; renameDialog.style.display = 'flex'; renameInput.focus(); renameInput.select(); @@ -264,7 +264,7 @@ const contextMenus = { renameInput.value = file.name; // Update header text const headerSpan = renameDialog.querySelector('.rename-dialog-header span'); - if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_file') : 'Renombrar archivo'; + if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_file') : 'Rename file'; renameDialog.style.display = 'flex'; renameInput.focus(); renameInput.select(); @@ -293,8 +293,8 @@ const contextMenus = { // Update dialog title (preserve icon) const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header'); const titleText = mode === 'file' ? - (window.i18n ? window.i18n.t('dialogs.move_file') : 'Mover archivo') : - (window.i18n ? window.i18n.t('dialogs.move_folder') : 'Mover carpeta'); + (window.i18n ? window.i18n.t('dialogs.move_file') : 'Move file') : + (window.i18n ? window.i18n.t('dialogs.move_folder') : 'Move folder'); dialogHeader.innerHTML = ` ${titleText}`; // Load all available folders @@ -319,7 +319,7 @@ const contextMenus = { async renameItem() { const newName = document.getElementById('rename-input').value.trim(); if (!newName) { - alert(window.i18n ? window.i18n.t('errors.empty_name') : 'El nombre no puede estar vacío'); + alert(window.i18n ? window.i18n.t('errors.empty_name') : 'Name cannot be empty'); return; } @@ -361,7 +361,7 @@ const contextMenus = { // Clear container except root option folderSelectContainer.innerHTML = `
- Raíz + Root
`; @@ -438,8 +438,8 @@ const contextMenus = { // Update dialog content dialogHeader.textContent = itemType === 'file' ? - (window.i18n ? window.i18n.t('dialogs.share_file') : 'Compartir archivo') : - (window.i18n ? window.i18n.t('dialogs.share_folder') : 'Compartir carpeta'); + (window.i18n ? window.i18n.t('dialogs.share_file') : 'Share file') : + (window.i18n ? window.i18n.t('dialogs.share_folder') : 'Share folder'); itemName.textContent = item.name; @@ -470,21 +470,21 @@ const contextMenus = { shareEl.className = 'existing-share-item'; const expiresText = share.expires_at ? - `Vence: ${window.fileSharing.formatExpirationDate(share.expires_at)}` : - 'Sin vencimiento'; + `Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` : + 'No expiration'; shareEl.innerHTML = ` `; @@ -507,9 +507,9 @@ const contextMenus = { const shareId = btn.getAttribute('data-share-id'); showConfirmDialog({ - title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Eliminar enlace', - message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : '¿Estás seguro de que quieres eliminar este enlace compartido?', - confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar', + title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Delete link', + message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : 'Are you sure you want to delete this shared link?', + confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete', }).then(confirmed => { if (confirmed) { window.fileSharing.removeSharedLink(shareId); @@ -534,7 +534,7 @@ const contextMenus = { */ createSharedLink() { if (!window.app.shareDialogItem || !window.app.shareDialogItemType) { - window.ui.showNotification('Error', 'No se pudo compartir el elemento'); + window.ui.showNotification('Error', 'Could not share the item'); return; } @@ -577,14 +577,14 @@ const contextMenus = { shareUrl.select(); // Show success message - window.ui.showNotification('Enlace creado', 'Enlace compartido creado correctamente'); + window.ui.showNotification('Link created', 'Shared link created successfully'); // Reload existing shares this.showShareDialog(item, itemType); } catch (error) { console.error('Error creating shared link:', error); - window.ui.showNotification('Error', 'No se pudo crear el enlace compartido'); + window.ui.showNotification('Error', 'Could not create shared link'); } }, @@ -614,14 +614,14 @@ const contextMenus = { const shareUrl = window.app.notificationShareUrl; if (!email || !shareUrl) { - window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido'); + window.ui.showNotification('Error', 'Please enter a valid email address'); return; } // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { - window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido'); + window.ui.showNotification('Error', 'Please enter a valid email address'); return; } @@ -630,7 +630,7 @@ const contextMenus = { document.getElementById('notification-dialog').style.display = 'none'; } catch (error) { console.error('Error sending notification:', error); - window.ui.showNotification('Error', 'No se pudo enviar la notificación'); + window.ui.showNotification('Error', 'Could not send notification'); } }, diff --git a/static/js/favorites.js b/static/js/favorites.js index 28484c03..5d37ac17 100644 --- a/static/js/favorites.js +++ b/static/js/favorites.js @@ -230,8 +230,8 @@ const favorites = { // Show success notification window.ui.showNotification( - 'Añadido a favoritos', - `"${name}" añadido a favoritos` + 'Added to favorites', + `"${name}" added to favorites` ); return true; @@ -271,8 +271,8 @@ const favorites = { // Show success notification if item was found if (item) { window.ui.showNotification( - 'Eliminado de favoritos', - `"${item.name}" eliminado de favoritos` + 'Removed from favorites', + `"${item.name}" removed from favorites` ); } return true; @@ -310,10 +310,10 @@ const favorites = { filesGrid.innerHTML = ''; filesListView.innerHTML = `
-
Nombre
-
Tipo
-
Tamaño
-
Modificado
+
Name
+
Type
+
Size
+
Modified
`; @@ -326,8 +326,8 @@ const favorites = { emptyState.className = 'empty-state'; emptyState.innerHTML = ` -

${window.i18n ? window.i18n.t('favorites.empty_state') : 'No hay elementos favoritos'}

-

${window.i18n ? window.i18n.t('favorites.empty_hint') : 'Para marcar como favorito, haz clic derecho en cualquier archivo o carpeta'}

+

${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}

+

${window.i18n ? window.i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}

`; filesGrid.appendChild(emptyState); return; @@ -359,7 +359,7 @@ const favorites = { } catch (error) { console.error('Error displaying favorites:', error); - window.ui.showNotification('Error', 'Error al cargar elementos favoritos'); + window.ui.showNotification('Error', 'Error loading favorite items'); } }, @@ -468,7 +468,7 @@ const favorites = {
${folder.name}
-
Carpeta
+
Folder
`; // Click to navigate @@ -518,7 +518,7 @@ const favorites = { ${folder.name} -
${window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'}
+
${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}
--
${formattedDate}
`; @@ -559,17 +559,17 @@ const favorites = { // Determine icon and type let iconClass = 'fas fa-file'; let iconSpecialClass = ''; - let typeLabel = 'Documento'; + let typeLabel = 'Document'; if (file.mime_type) { if (file.mime_type.startsWith('image/')) { iconClass = 'fas fa-file-image'; iconSpecialClass = 'image-icon'; - typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen'; + typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image'; } else if (file.mime_type.startsWith('text/')) { iconClass = 'fas fa-file-alt'; iconSpecialClass = 'text-icon'; - typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto'; + typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text'; } else if (file.mime_type.startsWith('video/')) { iconClass = 'fas fa-file-video'; iconSpecialClass = 'video-icon'; @@ -606,7 +606,7 @@ const favorites = {
${file.name}
-
Modificado ${formattedDate.split(' ')[0]}
+
Modified ${formattedDate.split(' ')[0]}
`; // Download on click diff --git a/static/js/fileOperations.js b/static/js/fileOperations.js index d945dcc5..1234c819 100644 --- a/static/js/fileOperations.js +++ b/static/js/fileOperations.js @@ -49,8 +49,8 @@ const fileOps = { try { console.log(`Uploading file to folder: ${targetFolderId || 'root'}`); - // Usamos la URL correcta para la subida de archivos - console.log('Formulario a enviar:', { + // We use the correct URL for file upload + console.log('Form to submit:', { file: file.name, size: file.size, folder_id: targetFolderId || 'root' @@ -59,16 +59,16 @@ const fileOps = { const response = await fetch('/api/files/upload', { method: 'POST', body: formData, - // Añadir cache: 'no-store' para evitar problemas de caché durante la subida + // Add cache: 'no-store' to avoid cache issues during upload cache: 'no-store', headers: { ...getAuthHeaders(), - // Agregar este encabezado para forzar recargas frescas + // Add this header to force fresh reloads 'Cache-Control': 'no-cache, no-store, must-revalidate' } }); - console.log('Respuesta del servidor:', { + console.log('Server response:', { status: response.status, statusText: response.statusText }); @@ -83,11 +83,11 @@ const fileOps = { console.log(`Successfully uploaded ${file.name}`, responseData); // Show success notification immediately - window.ui.showNotification('Archivo subido', `${file.name} completado`); + window.ui.showNotification('File uploaded', `${file.name} completed`); if (i === totalFiles - 1) { // Last file uploaded - wait and reload once - console.log('Último archivo subido, esperando antes de recargar...'); + console.log('Last file uploaded, waiting before reloading...'); // Wait for backend to persist await new Promise(resolve => setTimeout(resolve, 800)); @@ -96,7 +96,7 @@ const fileOps = { try { await window.loadFiles({forceRefresh: true}); } catch (reloadError) { - console.error("Error recargando archivos:", reloadError); + console.error("Error reloading files:", reloadError); } // Hide upload UI @@ -109,11 +109,11 @@ const fileOps = { } else { const errorData = await response.text(); console.error('Upload error:', errorData); - window.ui.showNotification('Error', `Error al subir el archivo: ${file.name}`); + window.ui.showNotification('Error', `Error uploading file: ${file.name}`); } } catch (error) { console.error('Network error during upload:', error); - window.ui.showNotification('Error', `Error de red al subir el archivo: ${file.name}`); + window.ui.showNotification('Error', `Network error uploading file: ${file.name}`); } } }, @@ -181,7 +181,7 @@ const fileOps = { console.log(`Created folder: ${folderPath} -> ${folder.id}`); } else { console.error(`Error creating folder ${folderPath}:`, await response.text()); - window.ui.showNotification('Error', `Error creando carpeta: ${folderName}`); + window.ui.showNotification('Error', `Error creating folder: ${folderName}`); } } catch (error) { console.error(`Network error creating folder ${folderPath}:`, error); @@ -228,7 +228,7 @@ const fileOps = { } // Finish up - window.ui.showNotification('Carpeta subida', `${uploadedCount} archivos subidos correctamente`); + window.ui.showNotification('Folder uploaded', `${uploadedCount} files uploaded successfully`); await new Promise(resolve => setTimeout(resolve, 800)); @@ -253,7 +253,7 @@ const fileOps = { try { console.log('Creating folder with name:', name); - // Enviar la solicitud real al backend para crear la carpeta + // Send the actual request to the backend to create the folder const response = await fetch('/api/folders', { method: 'POST', headers: { @@ -268,28 +268,28 @@ const fileOps = { }); if (response.ok) { - // Obtener la carpeta creada del backend + // Get the created folder from the backend const folder = await response.json(); console.log('Folder created successfully:', folder); - // Añadir la carpeta a la vista de inmediato para feedback instantáneo + // Add the folder to the view immediately for instant feedback window.ui.addFolderToView(folder); - // Esperar para permitir que el backend guarde los cambios + // Wait to allow the backend to save the changes await new Promise(resolve => setTimeout(resolve, 1000)); - // Recargar los archivos para refrescar la vista + // Reload files to refresh the view await window.loadFiles({forceRefresh: true}); - window.ui.showNotification('Carpeta creada', `"${name}" creada correctamente`); + window.ui.showNotification('Folder created', `"${name}" created successfully`); } else { const errorData = await response.text(); console.error('Create folder error:', errorData); - window.ui.showNotification('Error', 'Error al crear la carpeta'); + window.ui.showNotification('Error', 'Error creating the folder'); } } catch (error) { console.error('Error creating folder:', error); - window.ui.showNotification('Error', 'Error al crear la carpeta'); + window.ui.showNotification('Error', 'Error creating the folder'); } }, @@ -315,22 +315,22 @@ const fileOps = { if (response.ok) { // Reload files after moving await window.loadFiles(); - window.ui.showNotification('Archivo movido', 'Archivo movido correctamente'); + window.ui.showNotification('File moved', 'File moved successfully'); return true; } else { - let errorMessage = 'Error desconocido'; + let errorMessage = 'Unknown error'; try { const errorData = await response.json(); - errorMessage = errorData.error || 'Error desconocido'; + errorMessage = errorData.error || 'Unknown error'; } catch (e) { - errorMessage = 'Error al procesar la respuesta del servidor'; + errorMessage = 'Error processing server response'; } - window.ui.showNotification('Error', `Error al mover el archivo: ${errorMessage}`); + window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`); return false; } } catch (error) { console.error('Error moving file:', error); - window.ui.showNotification('Error', 'Error al mover el archivo'); + window.ui.showNotification('Error', 'Error moving the file'); return false; } }, @@ -357,22 +357,22 @@ const fileOps = { if (response.ok) { // Reload files after moving await window.loadFiles(); - window.ui.showNotification('Carpeta movida', 'Carpeta movida correctamente'); + window.ui.showNotification('Folder moved', 'Folder moved successfully'); return true; } else { - let errorMessage = 'Error desconocido'; + let errorMessage = 'Unknown error'; try { const errorData = await response.json(); - errorMessage = errorData.error || 'Error desconocido'; + errorMessage = errorData.error || 'Unknown error'; } catch (e) { - errorMessage = 'Error al procesar la respuesta del servidor'; + errorMessage = 'Error processing server response'; } - window.ui.showNotification('Error', `Error al mover la carpeta: ${errorMessage}`); + window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`); return false; } } catch (error) { console.error('Error moving folder:', error); - window.ui.showNotification('Error', 'Error al mover la carpeta'); + window.ui.showNotification('Error', 'Error moving the folder'); return false; } }, @@ -400,26 +400,26 @@ const fileOps = { if (response.ok) { window.ui.showNotification( - window.i18n ? window.i18n.t('notifications.file_renamed') : 'Archivo renombrado', - window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `Archivo renombrado a "${newName}"` + window.i18n ? window.i18n.t('notifications.file_renamed') : 'File renamed', + window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"` ); return true; } else { const errorText = await response.text(); console.error('Error response:', errorText); - let errorMessage = 'Error desconocido'; + let errorMessage = 'Unknown error'; try { const errorData = JSON.parse(errorText); errorMessage = errorData.error || response.statusText; } catch (e) { errorMessage = errorText || response.statusText; } - window.ui.showNotification('Error', `Error al renombrar el archivo: ${errorMessage}`); + window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`); return false; } } catch (error) { console.error('Error renaming file:', error); - window.ui.showNotification('Error', 'Error al renombrar el archivo'); + window.ui.showNotification('Error', 'Error renaming the file'); return false; } }, @@ -446,13 +446,13 @@ const fileOps = { console.log('Response status:', response.status); if (response.ok) { - window.ui.showNotification('Carpeta renombrada', `Carpeta renombrada a "${newName}"`); + window.ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`); return true; } else { const errorText = await response.text(); console.error('Error response:', errorText); - let errorMessage = 'Error desconocido'; + let errorMessage = 'Unknown error'; try { // Try to parse as JSON const errorData = JSON.parse(errorText); @@ -462,12 +462,12 @@ const fileOps = { errorMessage = errorText || response.statusText; } - window.ui.showNotification('Error', `Error al renombrar la carpeta: ${errorMessage}`); + window.ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`); return false; } } catch (error) { console.error('Error renaming folder:', error); - window.ui.showNotification('Error', 'Error al renombrar la carpeta'); + window.ui.showNotification('Error', 'Error renaming the folder'); return false; } }, @@ -480,9 +480,9 @@ const fileOps = { */ async deleteFile(fileId, fileName) { const confirmed = await showConfirmDialog({ - title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera', - message: window.i18n ? window.i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `¿Estás seguro de que quieres mover a la papelera el archivo "${fileName}"?`, - confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar', + title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash', + message: window.i18n ? window.i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`, + confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete', }); if (!confirmed) return false; @@ -495,7 +495,7 @@ const fileOps = { if (response.ok) { window.loadFiles(); - window.ui.showNotification('Archivo movido a papelera', `"${fileName}" movido a la papelera`); + window.ui.showNotification('File moved to trash', `"${fileName}" moved to trash`); return true; } else { // Fallback to direct deletion if trash fails @@ -506,16 +506,16 @@ const fileOps = { if (fallbackResponse.ok) { window.loadFiles(); - window.ui.showNotification('Archivo eliminado', `"${fileName}" eliminado correctamente`); + window.ui.showNotification('File deleted', `"${fileName}" deleted successfully`); return true; } else { - window.ui.showNotification('Error', 'Error al eliminar el archivo'); + window.ui.showNotification('Error', 'Error deleting the file'); return false; } } } catch (error) { console.error('Error deleting file:', error); - window.ui.showNotification('Error', 'Error al eliminar el archivo'); + window.ui.showNotification('Error', 'Error deleting the file'); return false; } }, @@ -528,9 +528,9 @@ const fileOps = { */ async deleteFolder(folderId, folderName) { const confirmed = await showConfirmDialog({ - title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera', - message: window.i18n ? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName }) : `¿Estás seguro de que quieres mover a la papelera la carpeta "${folderName}" y todo su contenido?`, - confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar', + title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash', + message: window.i18n ? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName }) : `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`, + confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete', }); if (!confirmed) return false; @@ -548,7 +548,7 @@ const fileOps = { window.ui.updateBreadcrumb(''); } window.loadFiles(); - window.ui.showNotification('Carpeta movida a papelera', `"${folderName}" movida a la papelera`); + window.ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`); return true; } else { // Fallback to direct deletion if trash fails @@ -564,23 +564,23 @@ const fileOps = { window.ui.updateBreadcrumb(''); } window.loadFiles(); - window.ui.showNotification('Carpeta eliminada', `"${folderName}" eliminada correctamente`); + window.ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`); return true; } else { - window.ui.showNotification('Error', 'Error al eliminar la carpeta'); + window.ui.showNotification('Error', 'Error deleting the folder'); return false; } } } catch (error) { console.error('Error deleting folder:', error); - window.ui.showNotification('Error', 'Error al eliminar la carpeta'); + window.ui.showNotification('Error', 'Error deleting the folder'); return false; } }, /** - * Obtener elementos de la papelera - * @returns {Promise} - Lista de elementos en la papelera + * Get trash items + * @returns {Promise} - List of trash items */ async getTrashItems() { try { @@ -601,9 +601,9 @@ const fileOps = { }, /** - * Restaurar un elemento desde la papelera - * @param {string} trashId - ID del elemento en la papelera - * @returns {Promise} - Éxito de la operación + * Restore an item from trash + * @param {string} trashId - Trash item ID + * @returns {Promise} - Operation success */ async restoreFromTrash(trashId) { try { @@ -617,29 +617,29 @@ const fileOps = { }); if (response.ok) { - window.ui.showNotification('Elemento restaurado', 'Elemento restaurado correctamente'); + window.ui.showNotification('Item restored', 'Item restored successfully'); return true; } else { - window.ui.showNotification('Error', 'Error al restaurar el elemento'); + window.ui.showNotification('Error', 'Error restoring the item'); return false; } } catch (error) { console.error('Error restoring item from trash:', error); - window.ui.showNotification('Error', 'Error al restaurar el elemento'); + window.ui.showNotification('Error', 'Error restoring the item'); return false; } }, /** - * Eliminar permanentemente un elemento de la papelera - * @param {string} trashId - ID del elemento en la papelera - * @returns {Promise} - Éxito de la operación + * Permanently delete a trash item + * @param {string} trashId - Trash item ID + * @returns {Promise} - Operation success */ async deletePermanently(trashId) { const confirmed = await showConfirmDialog({ - title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Eliminar permanentemente', - message: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete_msg') : '¿Estás seguro de que quieres eliminar permanentemente este elemento? Esta acción no se puede deshacer.', - confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Eliminar permanentemente', + title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently', + message: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete_msg') : 'Are you sure you want to permanently delete this item? This action cannot be undone.', + confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently', }); if (!confirmed) return false; @@ -650,28 +650,28 @@ const fileOps = { }); if (response.ok) { - window.ui.showNotification('Elemento eliminado', 'Elemento eliminado permanentemente'); + window.ui.showNotification('Item deleted', 'Item permanently deleted'); return true; } else { - window.ui.showNotification('Error', 'Error al eliminar el elemento'); + window.ui.showNotification('Error', 'Error deleting the item'); return false; } } catch (error) { console.error('Error deleting item permanently:', error); - window.ui.showNotification('Error', 'Error al eliminar el elemento'); + window.ui.showNotification('Error', 'Error deleting the item'); return false; } }, /** - * Vaciar la papelera - * @returns {Promise} - Éxito de la operación + * Empty the trash + * @returns {Promise} - Operation success */ async emptyTrash() { const confirmed = await showConfirmDialog({ - title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Vaciar papelera', - message: window.i18n ? window.i18n.t('trash.empty_confirm') : '¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos.', - confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Vaciar papelera', + title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Empty trash', + message: window.i18n ? window.i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.', + confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash', }); if (!confirmed) return false; @@ -682,23 +682,23 @@ const fileOps = { }); if (response.ok) { - window.ui.showNotification('Papelera vaciada', 'La papelera ha sido vaciada correctamente'); + window.ui.showNotification('Trash emptied', 'The trash has been emptied successfully'); return true; } else { - window.ui.showNotification('Error', 'Error al vaciar la papelera'); + window.ui.showNotification('Error', 'Error emptying the trash'); return false; } } catch (error) { console.error('Error emptying trash:', error); - window.ui.showNotification('Error', 'Error al vaciar la papelera'); + window.ui.showNotification('Error', 'Error emptying the trash'); return false; } }, /** - * Descargar un archivo - * @param {string} fileId - ID del archivo - * @param {string} fileName - Nombre del archivo + * Download a file + * @param {string} fileId - File ID + * @param {string} fileName - File name */ async downloadFile(fileId, fileName) { try { @@ -716,23 +716,23 @@ const fileOps = { document.body.removeChild(link); URL.revokeObjectURL(url); } else { - window.ui.showNotification('Error', 'Error al descargar el archivo'); + window.ui.showNotification('Error', 'Error downloading the file'); } } catch (error) { console.error('Error downloading file:', error); - window.ui.showNotification('Error', 'Error al descargar el archivo'); + window.ui.showNotification('Error', 'Error downloading the file'); } }, /** - * Descargar una carpeta como ZIP - * @param {string} folderId - ID de la carpeta - * @param {string} folderName - Nombre de la carpeta + * Download a folder as ZIP + * @param {string} folderId - Folder ID + * @param {string} folderName - Folder name */ async downloadFolder(folderId, folderName) { try { // Show notification to user - window.ui.showNotification('Preparando descarga', 'Preparando la carpeta para descargar...'); + window.ui.showNotification('Preparing download', 'Preparing the folder for download...'); const response = await fetch(`/api/folders/${folderId}/download?format=zip`, { headers: getAuthHeaders() @@ -748,11 +748,11 @@ const fileOps = { document.body.removeChild(link); URL.revokeObjectURL(url); } else { - window.ui.showNotification('Error', 'Error al descargar la carpeta'); + window.ui.showNotification('Error', 'Error downloading the folder'); } } catch (error) { console.error('Error downloading folder:', error); - window.ui.showNotification('Error', 'Error al descargar la carpeta'); + window.ui.showNotification('Error', 'Error downloading the folder'); } } }; diff --git a/static/js/fileSharing.js b/static/js/fileSharing.js index 7de6dd5e..ae12c570 100644 --- a/static/js/fileSharing.js +++ b/static/js/fileSharing.js @@ -214,11 +214,11 @@ const fileSharing = { copyLinkToClipboard(url) { try { navigator.clipboard.writeText(url); - window.ui.showNotification('Enlace copiado', 'Enlace copiado al portapapeles'); + window.ui.showNotification('Link copied', 'Link copied to clipboard'); return true; } catch (error) { console.error('Error copying to clipboard:', error); - window.ui.showNotification('Error', 'No se pudo copiar el enlace'); + window.ui.showNotification('Error', 'Could not copy link'); return false; } }, @@ -229,7 +229,7 @@ const fileSharing = { * @returns {string} - Formatted date string */ formatExpirationDate(dateString) { - if (!dateString) return 'Sin vencimiento'; + if (!dateString) return 'No expiration'; const date = new Date(dateString); return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); @@ -252,11 +252,11 @@ const fileSharing = { // Simulate network delay //await new Promise(resolve => setTimeout(resolve, 800)); - window.ui.showNotification('Notificación enviada', `Se envió notificación a ${recipientEmail}`); + window.ui.showNotification('Notification sent', `Notification sent to ${recipientEmail}`); return true; } catch (error) { console.error('Error sending share notification:', error); - window.ui.showNotification('Error', 'No se pudo enviar la notificación'); + window.ui.showNotification('Error', 'Could not send notification'); return false; } }, diff --git a/static/js/recent.js b/static/js/recent.js index 3f89a1fb..8ecac23a 100644 --- a/static/js/recent.js +++ b/static/js/recent.js @@ -110,10 +110,10 @@ const recent = { filesGrid.innerHTML = ''; filesListView.innerHTML = `
-
Nombre
-
Tipo
-
Tamaño
-
Último acceso
+
Name
+
Type
+
Size
+
Last accessed
`; @@ -126,8 +126,8 @@ const recent = { emptyState.className = 'empty-state'; emptyState.innerHTML = ` -

${window.i18n ? window.i18n.t('recent.empty_state') : 'No hay archivos recientes'}

-

${window.i18n ? window.i18n.t('recent.empty_hint') : 'Los archivos que abras aparecerán aquí'}

+

${window.i18n ? window.i18n.t('recent.empty_state') : 'No recent files'}

+

${window.i18n ? window.i18n.t('recent.empty_hint') : 'Files you open will appear here'}

`; filesGrid.appendChild(emptyState); return; @@ -143,7 +143,7 @@ const recent = { } catch (error) { console.error('Error displaying recent files:', error); - window.ui.showNotification('Error', 'Error al cargar archivos recientes'); + window.ui.showNotification('Error', 'Error loading recent files'); } }, @@ -157,17 +157,17 @@ const recent = { // Determine icon and type let iconClass = 'fas fa-file'; let iconSpecialClass = ''; - let typeLabel = 'Documento'; + let typeLabel = 'Document'; if (file.mime_type) { if (file.mime_type.startsWith('image/')) { iconClass = 'fas fa-file-image'; iconSpecialClass = 'image-icon'; - typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen'; + typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image'; } else if (file.mime_type.startsWith('text/')) { iconClass = 'fas fa-file-alt'; iconSpecialClass = 'text-icon'; - typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto'; + typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text'; } else if (file.mime_type.startsWith('video/')) { iconClass = 'fas fa-file-video'; iconSpecialClass = 'video-icon'; @@ -204,7 +204,7 @@ const recent = {
${file.name}
-
Accedido ${formattedDate.split(' ')[0]}
+
Accessed ${formattedDate.split(' ')[0]}
`; // Download on click diff --git a/static/js/search.js b/static/js/search.js index 1f9d3a63..61eb57f2 100644 --- a/static/js/search.js +++ b/static/js/search.js @@ -52,7 +52,7 @@ const search = { } } catch (error) { console.error('Error performing search:', error); - window.ui.showNotification('Error', 'Error al realizar la búsqueda'); + window.ui.showNotification('Error', 'Error performing search'); return { files: [], folders: [], total_count: 0 }; } }, @@ -91,7 +91,7 @@ const search = { } } catch (error) { console.error('Error performing advanced search:', error); - window.ui.showNotification('Error', 'Error al realizar la búsqueda avanzada'); + window.ui.showNotification('Error', 'Error performing advanced search'); return { files: [], folders: [], total_count: 0 }; } }, @@ -109,10 +109,10 @@ const search = { filesGrid.innerHTML = ''; filesListView.innerHTML = `
-
Nombre
-
Tipo
-
Tamaño
-
Modificado
+
Name
+
Type
+
Size
+
Modified
`; @@ -120,9 +120,9 @@ const search = { const searchHeader = document.createElement('div'); searchHeader.className = 'search-results-header'; searchHeader.innerHTML = ` -

Resultados de búsqueda (${results.total_count || (results.files.length + results.folders.length)})

+

Search results (${results.total_count || (results.files.length + results.folders.length)})

`; filesGrid.appendChild(searchHeader); @@ -147,7 +147,7 @@ const search = { emptyState.className = 'empty-state'; emptyState.innerHTML = ` -

No se encontraron resultados para esta búsqueda

+

No results found for this search

`; filesGrid.appendChild(emptyState); return; @@ -178,15 +178,15 @@ const search = { }); if (response.ok) { - window.ui.showNotification('Caché limpiada', 'Caché de búsqueda limpiada correctamente'); + window.ui.showNotification('Cache cleared', 'Search cache cleared successfully'); return true; } else { - window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda'); + window.ui.showNotification('Error', 'Error clearing search cache'); return false; } } catch (error) { console.error('Error clearing search cache:', error); - window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda'); + window.ui.showNotification('Error', 'Error clearing search cache'); return false; } } diff --git a/static/js/ui.js b/static/js/ui.js index 91f44e18..daefedf0 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -16,24 +16,24 @@ const ui = { folderMenu.id = 'folder-context-menu'; folderMenu.innerHTML = `
- Descargar + Download
- Añadir a favoritos + Add to favorites
- Compartir + Share
- Renombrar + Rename
- Mover a... + Move to...
- Eliminar + Delete
`; document.body.appendChild(folderMenu); @@ -46,28 +46,28 @@ const ui = { fileMenu.id = 'file-context-menu'; fileMenu.innerHTML = `
- Ver + View
- Descargar + Download
- Añadir a favoritos + Add to favorites
- Compartir + Share
- Renombrar + Rename
- Mover a... + Move to...
- Eliminar + Delete
`; document.body.appendChild(fileMenu); @@ -82,14 +82,14 @@ const ui = {
- Renombrar + Rename
- +
- - + +
`; @@ -105,19 +105,19 @@ const ui = {
- Mover + Move
-

Selecciona la carpeta destino:

+

Select destination folder:

- Raíz + Root
- - + +
`; @@ -133,67 +133,67 @@ const ui = { `; @@ -228,24 +228,24 @@ const ui = { `; @@ -402,7 +402,7 @@ const ui = { homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home'); // For searching, we might have a custom breadcrumb text - if (folderName && folderName.startsWith('Búsqueda:')) { + if (folderName && folderName.startsWith('Search:')) { // We're in search mode - don't add click handler breadcrumb.appendChild(homeItem); return; @@ -421,7 +421,7 @@ const ui = { breadcrumb.appendChild(homeItem); // If we have a subfolder, add it to the breadcrumb - if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Búsqueda:')) { + if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Search:')) { const separator = document.createElement('span'); separator.className = 'breadcrumb-separator'; separator.textContent = '>'; @@ -633,14 +633,14 @@ const ui = { * @param {Object} folder - Folder object */ addFolderToView(folder) { - // Verificar si la carpeta ya existe en la vista para evitar duplicados + // Check if the folder already exists in the view to avoid duplicates if (document.querySelector(`.file-card[data-folder-id="${folder.id}"]`) || document.querySelector(`.file-item[data-folder-id="${folder.id}"]`)) { - console.log(`Carpeta ${folder.name} (${folder.id}) ya existe en la vista, no duplicando`); + console.log(`Folder ${folder.name} (${folder.id}) already exists in the view, not duplicating`); return; } - console.log(`Añadiendo carpeta a la vista: ${folder.name} (${folder.id})`); + console.log(`Adding folder to the view: ${folder.name} (${folder.id})`); // Grid view element const folderGridElement = document.createElement('div'); @@ -655,7 +655,7 @@ const ui = {
${folder.name}
-
Carpeta
+
Folder
`; // Drag and drop setup for folders @@ -746,7 +746,7 @@ const ui = { if (id) { if (isFolder) { if (id === folder.id) { - alert("No puedes mover una carpeta a sí misma"); + alert("You cannot move a folder to itself"); return; } await fileOps.moveFolder(id, folder.id); @@ -758,7 +758,7 @@ const ui = { document.getElementById('files-grid').appendChild(folderGridElement); - // List view element - Mejorado + // List view element - Improved const folderListElement = document.createElement('div'); folderListElement.className = 'file-item'; folderListElement.dataset.folderId = folder.id; @@ -788,7 +788,7 @@ const ui = { }); } - // Mejorado: Estructura y clases para la vista de lista + // Improved: Structure and classes for list view folderListElement.innerHTML = `
@@ -796,7 +796,7 @@ const ui = {
${folder.name}
-
${window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'}
+
${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}
--
${formattedDate}
`; @@ -844,7 +844,7 @@ const ui = { if (id) { if (isFolder) { if (id === folder.id) { - alert("No puedes mover una carpeta a sí misma"); + alert("You cannot move a folder to itself"); return; } await fileOps.moveFolder(id, folder.id); @@ -862,29 +862,29 @@ const ui = { * @param {Object} file - File object */ addFileToView(file) { - // Verificar si el archivo ya existe en la vista para evitar duplicados + // Check if the file already exists in the view to avoid duplicates if (document.querySelector(`.file-card[data-file-id="${file.id}"]`) || document.querySelector(`.file-item[data-file-id="${file.id}"]`)) { - console.log(`Archivo ${file.name} (${file.id}) ya existe en la vista, no duplicando`); + console.log(`File ${file.name} (${file.id}) already exists in the view, not duplicating`); return; } - console.log(`Añadiendo archivo a la vista: ${file.name} (${file.id})`); + console.log(`Adding file to the view: ${file.name} (${file.id})`); // Determine icon and type let iconClass = 'fas fa-file'; let iconSpecialClass = ''; - let typeLabel = 'Documento'; + let typeLabel = 'Document'; if (file.mime_type) { if (file.mime_type.startsWith('image/')) { iconClass = 'fas fa-file-image'; iconSpecialClass = 'image-icon'; - typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen'; + typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image'; } else if (file.mime_type.startsWith('text/')) { iconClass = 'fas fa-file-alt'; iconSpecialClass = 'text-icon'; - typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto'; + typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text'; } else if (file.mime_type.startsWith('video/')) { iconClass = 'fas fa-file-video'; iconSpecialClass = 'video-icon'; @@ -916,7 +916,7 @@ const ui = {
${file.name}
-
Modificado ${formattedDate.split(' ')[0]}
+
Modified ${formattedDate.split(' ')[0]}
`; fileGridElement.dataset.fileId = file.id; @@ -1007,7 +1007,7 @@ const ui = { document.getElementById('files-grid').appendChild(fileGridElement); - // List view element - Mejorado con clases específicas y diseño mejorado + // List view element - Improved with specific classes and enhanced layout const fileListElement = document.createElement('div'); fileListElement.className = 'file-item'; fileListElement.dataset.fileId = file.id; @@ -1238,9 +1238,9 @@ window.initRubberBandSelection = initRubberBandSelection; * @returns {Promise} true if confirmed, false if cancelled */ function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) { - const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Eliminar'); - const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancelar'); - const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirmar acción'); + const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Delete'); + const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancel'); + const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirm action'); return new Promise((resolve) => { // Remove any previous confirm dialog diff --git a/static/login.html b/static/login.html index cdfa322c..ecfd0435 100644 --- a/static/login.html +++ b/static/login.html @@ -62,46 +62,46 @@
OxiCloud
-

Iniciar sesión

+

Log in

- +
- +
- +
- ¿No tienes cuenta? - Regístrate + Don't have an account? + Sign up
- ¿Primera vez? - Configurar administrador + First time? + Set up administrator
@@ -115,20 +115,20 @@
OxiCloud
-

Crear cuenta

+

Create account

- +
- +
- +
- +
- ¿Ya tienes cuenta? - Iniciar sesión + Already have an account? + Log in
@@ -191,7 +191,7 @@
OxiCloud
-

Configuración inicial

+

Initial setup

@@ -200,11 +200,11 @@
2
-
Sistema
+
System
3
-
Completado
+
Completed
@@ -213,7 +213,7 @@
- +
- +
- +
- +
- +
- ¿Ya está configurado? - Iniciar sesión + Already configured? + Log in
diff --git a/static/test.html b/static/test.html index c66d594a..a68b8d38 100644 --- a/static/test.html +++ b/static/test.html @@ -8,17 +8,17 @@

OxiCloud Test Page

- - + + diff --git a/storage/folder_ids.json b/storage/folder_ids.json index babba8c8..4157e238 100644 --- a/storage/folder_ids.json +++ b/storage/folder_ids.json @@ -1,11 +1,19 @@ { "path_to_id": { + "/Mi Carpeta - testuser999": "7c1afffd-867f-4649-a0f6-4cc8a9979af4", "/Mi Carpeta - test": "edfc23e0-d9f9-4cc7-b1dc-6369568b19e6", - "/Mi Carpeta - testuser": "304384d6-9a7e-4b22-a659-287957e8b667" + "/Mi Carpeta - user1": "b775c641-a698-458b-a561-42ae93073c74", + "/Mi Carpeta - testadmin": "02aed53b-f194-4e28-8680-2cc81a3584b3", + "/Mi Carpeta - testuser": "304384d6-9a7e-4b22-a659-287957e8b667", + "/Mi Carpeta - admin": "562e90aa-a3fc-4672-a0fd-9187fef5d4f1" }, "id_to_path": { + "562e90aa-a3fc-4672-a0fd-9187fef5d4f1": "/Mi Carpeta - admin", + "7c1afffd-867f-4649-a0f6-4cc8a9979af4": "/Mi Carpeta - testuser999", + "02aed53b-f194-4e28-8680-2cc81a3584b3": "/Mi Carpeta - testadmin", "304384d6-9a7e-4b22-a659-287957e8b667": "/Mi Carpeta - testuser", + "b775c641-a698-458b-a561-42ae93073c74": "/Mi Carpeta - user1", "edfc23e0-d9f9-4cc7-b1dc-6369568b19e6": "/Mi Carpeta - test" }, - "version": 2 + "version": 6 } \ No newline at end of file