chore: translate all Spanish comments and log messages to English

This commit is contained in:
Dionisio
2026-02-12 09:41:25 +01:00
parent bc169de8f6
commit d31a413e57
89 changed files with 2480 additions and 2482 deletions
+4 -4
View File
@@ -44,12 +44,12 @@ impl From<File> for FileDto {
} }
} }
// Para convertir de FileDto a File para los batch handlers // To convert from FileDto to File for batch handlers
impl From<FileDto> for File { impl From<FileDto> for File {
fn from(dto: FileDto) -> Self { fn from(dto: FileDto) -> Self {
// Usar constructor para crear una entidad desde DTO // Use constructor to create an entity from DTO
// Nota: esto debe simplificarse si File tiene un constructor adecuado // Note: this should be simplified if File has a proper constructor
// Si no, deberías hacer la conversión de la mejor manera posible // If not, make the conversion as best as possible
File::from_dto( File::from_dto(
dto.id, dto.id,
dto.name, dto.name,
+3 -3
View File
@@ -66,11 +66,11 @@ impl From<Folder> for FolderDto {
} }
} }
// Para convertir de FolderDto a Folder para los batch handlers // To convert from FolderDto to Folder for batch handlers
impl From<FolderDto> for Folder { impl From<FolderDto> for Folder {
fn from(dto: FolderDto) -> Self { fn from(dto: FolderDto) -> Self {
// Usar constructor para crear una entidad desde DTO // Use constructor to create an entity from DTO
// Nota: esto debe simplificarse si Folder tiene un constructor adecuado // Note: this should be simplified if Folder has a proper constructor
Folder::from_dto( Folder::from_dto(
dto.id, dto.id,
dto.name, dto.name,
+21 -21
View File
@@ -1,39 +1,39 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
/// Un DTO para representar información de paginación /// A DTO to represent pagination information
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationDto { pub struct PaginationDto {
/// Página actual (comienza en 0) /// Current page (starts at 0)
pub page: usize, pub page: usize,
/// Tamaño de página /// Page size
pub page_size: usize, pub page_size: usize,
/// Número total de elementos /// Total number of items
pub total_items: usize, pub total_items: usize,
/// Número total de páginas /// Total number of pages
pub total_pages: usize, pub total_pages: usize,
/// Indica si hay una página siguiente /// Indicates if there is a next page
pub has_next: bool, pub has_next: bool,
/// Indica si hay una página anterior /// Indicates if there is a previous page
pub has_prev: bool, 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationRequestDto { pub struct PaginationRequestDto {
/// Página solicitada (comienza en 0) /// Requested page (starts at 0)
#[serde(default)] #[serde(default)]
pub page: usize, pub page: usize,
/// Tamaño de página solicitado /// Requested page size
#[serde(default = "default_page_size")] #[serde(default = "default_page_size")]
pub page_size: usize, pub page_size: usize,
} }
/// Un DTO para representar una respuesta paginada /// A DTO to represent a paginated response
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResponseDto<T> { pub struct PaginatedResponseDto<T> {
/// Datos en la página actual /// Data on the current page
pub items: Vec<T>, pub items: Vec<T>,
/// Información de paginación /// Pagination information
pub pagination: PaginationDto, 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 { fn default_page_size() -> usize {
100 // Por defecto, 100 items por página 100 // By default, 100 items per page
} }
impl PaginationRequestDto { impl PaginationRequestDto {
/// Calcula el offset para consultas paginadas /// Calculates the offset for paginated queries
pub fn offset(&self) -> usize { pub fn offset(&self) -> usize {
self.page * self.page_size self.page * self.page_size
} }
/// Calcula el límite para consultas paginadas /// Calculates the limit for paginated queries
pub fn limit(&self) -> usize { pub fn limit(&self) -> usize {
self.page_size 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 { pub fn validate_and_adjust(&self) -> Self {
let mut page = self.page; let mut page = self.page;
let mut page_size = self.page_size; 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 { if page < 1 {
page = 0; 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 { if page_size < 10 {
page_size = 10; page_size = 10;
} else if page_size > 500 { } else if page_size > 500 {
@@ -87,7 +87,7 @@ impl PaginationRequestDto {
} }
impl<T> PaginatedResponseDto<T> { impl<T> PaginatedResponseDto<T> {
/// 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( pub fn new(
items: Vec<T>, items: Vec<T>,
page: usize, page: usize,
+6 -6
View File
@@ -1,21 +1,21 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
/// DTO para elementos recientes /// DTO for recent items
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentItemDto { pub struct RecentItemDto {
/// Identificador único para el elemento reciente /// Unique identifier for the recent item
pub id: String, pub id: String,
/// ID del usuario propietario /// Owner user ID
pub user_id: String, pub user_id: String,
/// ID del elemento (archivo o carpeta) /// Item ID (file or folder)
pub item_id: String, pub item_id: String,
/// Tipo del elemento ('file' o 'folder') /// Item type ('file' or 'folder')
pub item_type: String, pub item_type: String,
/// Cuándo se accedió al elemento /// When the item was accessed
pub accessed_at: DateTime<Utc>, pub accessed_at: DateTime<Utc>,
} }
+1 -1
View File
@@ -67,7 +67,7 @@ pub struct RefreshTokenDto {
pub refresh_token: String, 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentUser { pub struct CurrentUser {
pub id: String, pub id: String,
+18 -18
View File
@@ -65,49 +65,49 @@ pub trait TokenServicePort: Send + Sync + 'static {
#[async_trait] #[async_trait]
pub trait UserStoragePort: Send + Sync + 'static { pub trait UserStoragePort: Send + Sync + 'static {
/// Crea un nuevo usuario /// Creates a new user
async fn create_user(&self, user: User) -> Result<User, DomainError>; async fn create_user(&self, user: User) -> Result<User, DomainError>;
/// Obtiene un usuario por ID /// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>; async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>;
/// Obtiene un usuario por nombre de usuario /// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>; async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
/// Obtiene un usuario por correo electrónico /// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError>; async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError>;
/// Actualiza un usuario existente /// Updates an existing user
async fn update_user(&self, user: User) -> Result<User, DomainError>; async fn update_user(&self, user: User) -> Result<User, DomainError>;
/// 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>; 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<Vec<User>, DomainError>; async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, 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<Vec<User>, DomainError>; async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
/// Elimina un usuario por su ID /// Deletes a user by their ID
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>; 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>; async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
/// Finds a user by OIDC provider + subject pair /// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError>; async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError>;
/// Activa o desactiva un usuario /// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>; 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>; 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>; 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<i64, DomainError>; async fn count_users(&self) -> Result<i64, DomainError>;
} }
@@ -156,15 +156,15 @@ pub trait OidcServicePort: Send + Sync + 'static {
#[async_trait] #[async_trait]
pub trait SessionStoragePort: Send + Sync + 'static { pub trait SessionStoragePort: Send + Sync + 'static {
/// Crea una nueva sesión /// Creates a new session
async fn create_session(&self, session: Session) -> Result<Session, DomainError>; async fn create_session(&self, session: Session) -> Result<Session, DomainError>;
/// 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<Session, DomainError>; async fn get_session_by_refresh_token(&self, refresh_token: &str) -> Result<Session, DomainError>;
/// Revoca una sesión específica /// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>; 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<u64, DomainError>; async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
} }
+8 -8
View File
@@ -22,22 +22,22 @@ pub trait FavoritesUseCase: Send + Sync {
// Outbound port — persistence abstraction // 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 /// Application services depend on this trait instead of
/// acceder directamente a `PgPool`. La implementación concreta /// accessing `PgPool` directly. The concrete implementation
/// vive en `infrastructure::repositories::pg`. /// lives in `infrastructure::repositories::pg`.
#[async_trait] #[async_trait]
pub trait FavoritesRepositoryPort: Send + Sync + 'static { 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<Vec<FavoriteItemDto>>; async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
/// 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<()>; 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<bool>; async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
/// 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<bool>; async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
} }
+15 -15
View File
@@ -22,10 +22,10 @@ pub enum UploadStrategy {
Streaming, Streaming,
} }
/// Puerto primario para operaciones de subida de archivos /// Primary port for file upload operations
#[async_trait] #[async_trait]
pub trait FileUploadUseCase: Send + Sync + 'static { pub trait FileUploadUseCase: Send + Sync + 'static {
/// Sube un nuevo archivo desde bytes /// Uploads a new file from bytes
async fn upload_file( async fn upload_file(
&self, &self,
name: String, name: String,
@@ -47,10 +47,10 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
total_size: usize, total_size: usize,
) -> Result<(FileDto, UploadStrategy), DomainError>; ) -> 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<FileDto, DomainError>; async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<FileDto, DomainError>;
/// 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>; async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>;
} }
@@ -76,22 +76,22 @@ pub enum OptimizedFileContent {
Stream(Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>), Stream(Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>),
} }
/// Puerto primario para operaciones de recuperación de archivos /// Primary port for file retrieval operations
#[async_trait] #[async_trait]
pub trait FileRetrievalUseCase: Send + Sync + 'static { 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<FileDto, DomainError>; async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
/// 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<FileDto, DomainError>; async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
/// Lista archivos en una carpeta /// Lists files in a folder
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>; async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, 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<Vec<u8>, DomainError>; async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, 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<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>; async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
/// Optimized multi-tier download. /// Optimized multi-tier download.
@@ -119,16 +119,16 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
// Management port (delete, move) // Management port (delete, move)
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
/// Puerto primario para operaciones de gestión de archivos /// Primary port for file management operations
#[async_trait] #[async_trait]
pub trait FileManagementUseCase: Send + Sync + 'static { 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<String>) -> Result<FileDto, DomainError>; async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
/// Renombra un archivo /// Renames a file
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>; async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
/// Elimina un archivo /// Deletes a file
async fn delete_file(&self, id: &str) -> Result<(), DomainError>; async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Smart delete: trash-first with dedup reference cleanup. /// Smart delete: trash-first with dedup reference cleanup.
@@ -145,7 +145,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
) -> Result<bool, DomainError>; ) -> Result<bool, DomainError>;
} }
/// Factory para crear implementaciones de casos de uso de archivos /// Factory for creating file use case implementations
pub trait FileUseCaseFactory: Send + Sync + 'static { pub trait FileUseCaseFactory: Send + Sync + 'static {
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>; fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>;
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>; fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>;
+17 -17
View File
@@ -4,58 +4,58 @@ use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolde
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto}; use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
/// Puerto primario para operaciones de carpetas /// Primary port for folder operations
#[async_trait] #[async_trait]
pub trait FolderUseCase: Send + Sync + 'static { pub trait FolderUseCase: Send + Sync + 'static {
/// Crea una nueva carpeta /// Creates a new folder
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>; async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>;
/// Obtiene una carpeta por su ID /// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>; async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>;
/// Obtiene una carpeta por su ruta /// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>; async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
/// Lista carpetas dentro de una carpeta padre /// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError>; async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError>;
/// Lista carpetas con paginación /// Lists folders with pagination
async fn list_folders_paginated( async fn list_folders_paginated(
&self, &self,
parent_id: Option<&str>, parent_id: Option<&str>,
pagination: &crate::application::dtos::pagination::PaginationRequestDto pagination: &crate::application::dtos::pagination::PaginationRequestDto
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>; ) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
/// Renombra una carpeta /// Renames a folder
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError>; async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError>;
/// Mueve una carpeta a otro padre /// Moves a folder to another parent
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>; async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>;
/// Elimina una carpeta /// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>; 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 * Defines the operations related to advanced search of
* archivos y carpetas basándose en diversos criterios. * files and folders based on various criteria.
*/ */
#[async_trait] #[async_trait]
pub trait SearchUseCase: Send + Sync + 'static { 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. * @param criteria Search criteria including text, dates, sizes, etc.
* @return Resultados de la búsqueda que contienen archivos y carpetas coincidentes * @return Search results containing matching files and folders
*/ */
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>; async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>;
/** /**
* 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>; async fn clear_search_cache(&self) -> Result<(), DomainError>;
} }
+27 -27
View File
@@ -9,71 +9,71 @@ pub use crate::domain::repositories::folder_repository::FolderRepository;
use super::storage_ports::{FileReadPort, FileWritePort}; use super::storage_ports::{FileReadPort, FileWritePort};
/// Puerto secundario para operaciones de almacenamiento /// Secondary port for storage operations
#[async_trait] #[async_trait]
pub trait StoragePort: Send + Sync + 'static { 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; 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>; 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<bool, DomainError>; async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
/// 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<bool, DomainError>; async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
} }
/// Puerto unificado para persistencia de archivos (backward-compatible). /// Unified port for file persistence (backward-compatible).
/// ///
/// Ahora es un **supertrait** de `FileReadPort + FileWritePort`. /// Now it is a **supertrait** of `FileReadPort + FileWritePort`.
/// Cualquier tipo que implemente ambos ports obtiene `FileStoragePort` /// Any type that implements both ports gets `FileStoragePort`
/// automáticamente via blanket impl. Esto permite migrar consumidores /// automatically via blanket impl. This allows consumers to be migrated
/// gradualmente a los ports granulares mientras los existentes siguen /// gradually to granular ports while existing ones continue
/// funcionando sin cambios. /// working without changes.
pub trait FileStoragePort: FileReadPort + FileWritePort {} pub trait FileStoragePort: FileReadPort + FileWritePort {}
/// Blanket implementation: cualquier tipo que implemente ambos ports /// Blanket implementation: any type that implements both ports
/// es automáticamente un FileStoragePort. /// is automatically a FileStoragePort.
impl<T: FileReadPort + FileWritePort> FileStoragePort for T {} impl<T: FileReadPort + FileWritePort> 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. /// Has the same signature as the domain's `FolderRepository`.
/// Las implementaciones concretas deben implementar `FolderRepository`, /// Concrete implementations must implement `FolderRepository`,
/// obteniendo `FolderStoragePort` automáticamente vía blanket impl. /// getting `FolderStoragePort` automatically via blanket impl.
pub trait FolderStoragePort: FolderRepository {} pub trait FolderStoragePort: FolderRepository {}
/// Blanket implementation: cualquier tipo que implemente FolderRepository /// Blanket implementation: any type that implements FolderRepository
/// es automáticamente un FolderStoragePort. /// is automatically a FolderStoragePort.
impl<T: FolderRepository> FolderStoragePort for T {} impl<T: FolderRepository> FolderStoragePort for T {}
/// Puerto secundario para mapeo de IDs /// Secondary port for ID mapping
#[async_trait] #[async_trait]
pub trait IdMappingPort: Send + Sync + 'static { 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<String, DomainError>; async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError>;
/// Obtiene una ruta por su ID /// Gets a path by its ID
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError>; async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError>;
/// 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>; 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>; async fn remove_id(&self, id: &str) -> Result<(), DomainError>;
/// Guarda cambios pendientes /// Saves pending changes
async fn save_changes(&self) -> Result<(), DomainError>; 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<PathBuf, DomainError> { async fn get_file_path(&self, file_id: &str) -> Result<PathBuf, DomainError> {
let storage_path = self.get_path_by_id(file_id).await?; let storage_path = self.get_path_by_id(file_id).await?;
Ok(PathBuf::from(storage_path.to_string())) 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> { 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()); let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string());
self.update_path(file_id, &storage_path).await self.update_path(file_id, &storage_path).await
+13 -13
View File
@@ -2,19 +2,19 @@ use async_trait::async_trait;
use crate::common::errors::Result; use crate::common::errors::Result;
use crate::application::dtos::recent_dto::RecentItemDto; use crate::application::dtos::recent_dto::RecentItemDto;
/// Define operaciones para gestionar elementos recientes del usuario /// Defines operations for managing user recent items
#[async_trait] #[async_trait]
pub trait RecentItemsUseCase: Send + Sync { 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<i32>) -> Result<Vec<RecentItemDto>>; async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>>;
/// 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<()>; 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<bool>; async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
/// Limpiar toda la lista de elementos recientes /// Clear the entire recent items list
async fn clear_recent_items(&self, user_id: &str) -> Result<()>; async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
} }
@@ -22,24 +22,24 @@ pub trait RecentItemsUseCase: Send + Sync {
// Outbound port — persistence abstraction // 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 /// Abstracts access to the `auth.user_recent_files` table so that
/// `RecentService` no dependa directamente de `PgPool`. /// `RecentService` does not depend directly on `PgPool`.
#[async_trait] #[async_trait]
pub trait RecentItemsRepositoryPort: Send + Sync + 'static { 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<Vec<RecentItemDto>>; async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>>;
/// 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<()>; 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<bool>; async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
/// Elimina todos los elementos recientes de un usuario. /// Removes all recent items for a user.
async fn clear_all(&self, user_id: &str) -> Result<()>; 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<()>; async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
} }
+37 -37
View File
@@ -17,28 +17,28 @@ pub use crate::domain::repositories::folder_repository::FolderRepository;
// FileReadPort — application-layer alias for FileReadRepository // 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: /// Encapsulates every operation that queries state without modifying it:
/// get, list, content, stream, mmap, range, resolución de rutas. /// get, list, content, stream, mmap, range, path resolution.
#[async_trait] #[async_trait]
pub trait FileReadPort: Send + Sync + 'static { 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<File, DomainError>; async fn get_file(&self, id: &str) -> Result<File, DomainError>;
/// Lista archivos en una carpeta. /// Lists files in a folder.
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>; async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, 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<Vec<u8>, DomainError>; async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
/// Obtiene contenido como stream (ideal para archivos grandes). /// Gets content as a stream (ideal for large files).
async fn get_file_stream( async fn get_file_stream(
&self, &self,
id: &str, id: &str,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>; ) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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( async fn get_file_range_stream(
&self, &self,
id: &str, id: &str,
@@ -46,13 +46,13 @@ pub trait FileReadPort: Send + Sync + 'static {
end: Option<u64>, end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>; ) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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<Bytes, DomainError>; async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
/// 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<StoragePath, DomainError>; async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
/// 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<String, DomainError>; async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
} }
@@ -60,13 +60,13 @@ pub trait FileReadPort: Send + Sync + 'static {
// FileWritePort — all write / mutate operations // FileWritePort — all write / mutate operations
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
/// Puerto secundario para **escritura** de archivos. /// Secondary port for file **writing**.
/// ///
/// Cubre: upload (buffered + streaming), move, delete, update, /// Covers: upload (buffered + streaming), move, delete, update,
/// y el registro diferido para write-behind cache. /// and deferred registration for the write-behind cache.
#[async_trait] #[async_trait]
pub trait FileWritePort: Send + Sync + 'static { pub trait FileWritePort: Send + Sync + 'static {
/// Guarda un nuevo archivo desde bytes. /// Saves a new file from bytes.
async fn save_file( async fn save_file(
&self, &self,
name: String, name: String,
@@ -75,7 +75,7 @@ pub trait FileWritePort: Send + Sync + 'static {
content: Vec<u8>, content: Vec<u8>,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// 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( async fn save_file_from_stream(
&self, &self,
name: String, name: String,
@@ -84,30 +84,30 @@ pub trait FileWritePort: Send + Sync + 'static {
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>, stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Mueve un archivo a otra carpeta. /// Moves a file to another folder.
async fn move_file( async fn move_file(
&self, &self,
file_id: &str, file_id: &str,
target_folder_id: Option<String>, target_folder_id: Option<String>,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Renombra un archivo (same folder, different name). /// Renames a file (same folder, different name).
async fn rename_file( async fn rename_file(
&self, &self,
file_id: &str, file_id: &str,
new_name: &str, new_name: &str,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Elimina un archivo. /// Deletes a file.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>; 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<u8>) -> Result<(), DomainError>; async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> 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 /// Returns `(File, PathBuf)` where `PathBuf` is the destination path for the
/// escritura diferida que realizará el `WriteBehindCache`. /// deferred write that the `WriteBehindCache` will perform.
async fn register_file_deferred( async fn register_file_deferred(
&self, &self,
name: String, name: String,
@@ -118,13 +118,13 @@ pub trait FileWritePort: Send + Sync + 'static {
// ── Trash operations ── // ── 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>; 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>; 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>; async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
} }
@@ -132,40 +132,40 @@ pub trait FileWritePort: Send + Sync + 'static {
// Auxiliary ports (unchanged) // Auxiliary ports (unchanged)
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
/// Puerto secundario para resolución de rutas de archivos /// Secondary port for file path resolution
#[async_trait] #[async_trait]
pub trait FilePathResolutionPort: Send + Sync + 'static { 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<StoragePath, DomainError>; async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
/// 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; 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] #[async_trait]
pub trait StorageVerificationPort: Send + Sync + 'static { 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<bool, DomainError>; async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
/// 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<bool, DomainError>; async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
} }
/// Puerto secundario para gestión de directorios /// Secondary port for directory management
#[async_trait] #[async_trait]
pub trait DirectoryManagementPort: Send + Sync + 'static { 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>; 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] #[async_trait]
pub trait StorageUsagePort: Send + Sync + 'static { 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<i64, DomainError>; async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
/// 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>; async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
} }
@@ -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<dyn FolderUseCase>) -> Self { pub fn with_folder_service(mut self, folder_service: Arc<dyn FolderUseCase>) -> Self {
self.folder_service = Some(folder_service); self.folder_service = Some(folder_service);
self self
} }
/// Configura el servicio OIDC /// Configures the OIDC service
pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self { pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self {
{ {
let mut state = self.oidc.write().unwrap(); let mut state = self.oidc.write().unwrap();
@@ -123,12 +123,12 @@ impl AuthApplicationService {
} }
pub async fn register(&self, dto: RegisterDto) -> Result<UserDto, DomainError> { pub async fn register(&self, dto: RegisterDto) -> Result<UserDto, DomainError> {
// Verificar usuario duplicado // Check for duplicate user
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() { if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AlreadyExists, ErrorKind::AlreadyExists,
"User", "User",
format!("El usuario '{}' ya existe", dto.username) format!("User '{}' already exists", dto.username)
)); ));
} }
@@ -136,62 +136,62 @@ impl AuthApplicationService {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AlreadyExists, ErrorKind::AlreadyExists,
"User", "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" || let is_admin_request = dto.username.to_lowercase() == "admin" ||
(dto.role.is_some() && dto.role.as_ref().unwrap().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 { if is_admin_request {
match self.count_admin_users().await { match self.count_admin_users().await {
Ok(admin_count) => { Ok(admin_count) => {
// Si ya hay admins en el sistema y no estamos en instalación limpia, // If there are already admins in the system and this is not a clean install,
// no permitimos crear otro admin desde el registro // we do not allow creating another admin from registration
if admin_count > 0 { 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 { match self.count_all_users().await {
Ok(user_count) => { 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 { 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( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"User", "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 // Otherwise, it is a clean install and the first admin is allowed
tracing::info!("Permitiendo creación de admin en instalación limpia"); tracing::info!("Allowing admin creation on clean install");
}, },
Err(e) => { Err(e) => {
tracing::error!("Error al contar usuarios: {}", e); tracing::error!("Error counting users: {}", e);
// Por seguridad, si no podemos verificar, rechazamos la creación de admin // For security, if we cannot verify, we reject admin creation
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"User", "User",
"No se permite crear usuarios admin adicionales" "Creating additional admin users is not allowed"
)); ));
} }
} }
} }
}, },
Err(e) => { Err(e) => {
tracing::error!("Error al contar usuarios admin: {}", e); tracing::error!("Error counting admin users: {}", e);
// Por seguridad, si no podemos verificar, rechazamos la creación de admin // For security, if we cannot verify, we reject admin creation
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"User", "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 // Determine role and quota based on user type
// Si se proporciona un rol explícito de "admin", usar rol de administrador // If an explicit "admin" role is provided, use the administrator role
let role = if let Some(role_str) = &dto.role { let role = if let Some(role_str) = &dto.role {
if role_str.to_lowercase() == "admin" { if role_str.to_lowercase() == "admin" {
UserRole::Admin UserRole::Admin
@@ -199,7 +199,7 @@ impl AuthApplicationService {
UserRole::User UserRole::User
} }
} else { } 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" { if dto.username.to_lowercase() == "admin" {
UserRole::Admin UserRole::Admin
} else { } 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 { let quota = if role == UserRole::Admin {
107374182400 // 100GB para admin 107374182400 // 100GB for admin
} else { } 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 { if dto.password.len() < 8 {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"User", "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)?; 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( let user = User::new(
dto.username.clone(), dto.username.clone(),
dto.email, dto.email,
@@ -236,13 +236,13 @@ impl AuthApplicationService {
).map_err(|e| DomainError::new( ).map_err(|e| DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"User", "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?; 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 { if let Some(folder_service) = &self.folder_service {
let folder_name = format!("Mi Carpeta - {}", dto.username); let folder_name = format!("Mi Carpeta - {}", dto.username);
@@ -252,20 +252,20 @@ impl AuthApplicationService {
}).await { }).await {
Ok(folder) => { Ok(folder) => {
tracing::info!( tracing::info!(
"Carpeta personal creada para el usuario {}: {} (ID: {})", "Personal folder created for user {}: {} (ID: {})",
created_user.id(), created_user.id(),
folder.name, folder.name,
folder.id folder.id
); );
// Aquí se podría guardar la asociación de la carpeta al usuario // Here we could save the folder-to-user association,
// por ejemplo, en una tabla de relación carpeta-usuario // for example, in a folder-user relationship table
}, },
Err(e) => { Err(e) => {
// No fallamos el registro por un error en la creación de la carpeta // We don't fail registration due to a folder creation error,
// pero lo registramos para investigación // but we log it for investigation
tracing::error!( tracing::error!(
"No se pudo crear la carpeta personal para el usuario {}: {}", "Could not create personal folder for user {}: {}",
created_user.id(), created_user.id(),
e e
); );
@@ -273,67 +273,67 @@ impl AuthApplicationService {
} }
} else { } else {
tracing::warn!( 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() created_user.id()
); );
} }
tracing::info!("Usuario registrado: {}", created_user.id()); tracing::info!("User registered: {}", created_user.id());
Ok(UserDto::from(created_user)) Ok(UserDto::from(created_user))
} }
pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> { pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> {
// Buscar usuario // Find user
let mut user = self.user_storage let mut user = self.user_storage
.get_user_by_username(&dto.username) .get_user_by_username(&dto.username)
.await .await
.map_err(|_| DomainError::new( .map_err(|_| DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Auth", "Auth",
"Credenciales inválidas" "Invalid credentials"
))?; ))?;
// Verificar si usuario está activo // Check if user is active
if !user.is_active() { if !user.is_active() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Auth", "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())?; let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
if !is_valid { if !is_valid {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Auth", "Auth",
"Credenciales inválidas" "Invalid credentials"
)); ));
} }
// Actualizar último login // Update last login
user.register_login(); user.register_login();
self.user_storage.update_user(user.clone()).await?; 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 access_token = self.token_service.generate_access_token(&user)?;
let refresh_token = self.token_service.generate_refresh_token(); let refresh_token = self.token_service.generate_refresh_token();
// Guardar sesión // Save session
let session = Session::new( let session = Session::new(
user.id().to_string(), user.id().to_string(),
refresh_token.clone(), refresh_token.clone(),
None, // IP (se puede añadir desde la capa HTTP) None, // IP (can be added from the HTTP layer)
None, // User-Agent (se puede añadir desde la capa HTTP) None, // User-Agent (can be added from the HTTP layer)
self.token_service.refresh_token_expiry_days(), self.token_service.refresh_token_expiry_days(),
); );
self.session_storage.create_session(session).await?; self.session_storage.create_session(session).await?;
// Respuesta de autenticación // Authentication response
Ok(AuthResponseDto { Ok(AuthResponseDto {
user: UserDto::from(user), user: UserDto::from(user),
access_token, access_token,
@@ -344,43 +344,43 @@ impl AuthApplicationService {
} }
pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result<AuthResponseDto, DomainError> { pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result<AuthResponseDto, DomainError> {
// Obtener sesión válida // Get valid session
let session = self.session_storage let session = self.session_storage
.get_session_by_refresh_token(&dto.refresh_token) .get_session_by_refresh_token(&dto.refresh_token)
.await?; .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() { if session.is_expired() || session.is_revoked() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Auth", "Auth",
"Sesión expirada o inválida" "Session expired or invalid"
)); ));
} }
// Obtener usuario // Get user
let user = self.user_storage let user = self.user_storage
.get_user_by_id(session.user_id()) .get_user_by_id(session.user_id())
.await?; .await?;
// Verificar si usuario está activo // Check if user is active
if !user.is_active() { if !user.is_active() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Auth", "Auth",
"Cuenta desactivada" "Account deactivated"
)); ));
} }
// Revocar sesión actual // Revoke current session
self.session_storage.revoke_session(session.id()).await?; 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 access_token = self.token_service.generate_access_token(&user)?;
let new_refresh_token = self.token_service.generate_refresh_token(); let new_refresh_token = self.token_service.generate_refresh_token();
// Crear nueva sesión // Create new session
let new_session = Session::new( let new_session = Session::new(
user.id().to_string(), user.id().to_string(),
new_refresh_token.clone(), new_refresh_token.clone(),
@@ -401,67 +401,67 @@ impl AuthApplicationService {
} }
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> { 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 { let session = match self.session_storage.get_session_by_refresh_token(refresh_token).await {
Ok(s) => s, 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(()), 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 { if session.user_id() != user_id {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Auth", "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?; self.session_storage.revoke_session(session.id()).await?;
Ok(()) Ok(())
} }
pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> { pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
// Revocar todas las sesiones del usuario // Revoke all user sessions
let revoked_count = self.session_storage.revoke_all_user_sessions(user_id).await?; let revoked_count = self.session_storage.revoke_all_user_sessions(user_id).await?;
Ok(revoked_count) Ok(revoked_count)
} }
pub async fn change_password(&self, user_id: &str, dto: ChangePasswordDto) -> Result<(), DomainError> { 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?; 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())?; let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
if !is_valid { if !is_valid {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Auth", "Auth",
"Contraseña actual incorrecta" "Current password is incorrect"
)); ));
} }
// Validar nueva contraseña // Validate new password
if dto.new_password.len() < 8 { if dto.new_password.len() < 8 {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"User", "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)?; let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
user.update_password_hash(new_hash); user.update_password_hash(new_hash);
// Guardar usuario actualizado // Save updated user
self.user_storage.update_user(user).await?; 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?; self.session_storage.revoke_all_user_sessions(user_id).await?;
Ok(()) Ok(())
@@ -492,7 +492,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new( .map_err(|e| DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"User", "User",
format!("Error al contar usuarios administradores: {}", e) format!("Error counting admin users: {}", e)
))?; ))?;
Ok(admin_users.len() as i64) Ok(admin_users.len() as i64)
@@ -506,7 +506,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new( .map_err(|e| DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"User", "User",
format!("Error al contar usuarios: {}", e) format!("Error counting users: {}", e)
))?; ))?;
Ok(all_users.len() as i64) Ok(all_users.len() as i64)
@@ -523,7 +523,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new( .map_err(|e| DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"User", "User",
format!("Error al eliminar usuario admin predeterminado: {}", e) format!("Error deleting default admin user: {}", e)
)) ))
}, },
Err(_) => { Err(_) => {
@@ -545,7 +545,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new( .map_err(|e| DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"User", "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 // 3. Create new admin user with the provided credentials but admin role
@@ -564,7 +564,7 @@ impl AuthApplicationService {
).map_err(|e| DomainError::new( ).map_err(|e| DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"User", "User",
format!("Error al crear usuario admin: {}", e) format!("Error creating admin user: {}", e)
))?; ))?;
// 4. Save the new admin user // 4. Save the new admin user
@@ -580,7 +580,7 @@ impl AuthApplicationService {
}).await { }).await {
Ok(folder) => { Ok(folder) => {
tracing::info!( tracing::info!(
"Carpeta personal creada para el admin {}: {} (ID: {})", "Personal folder created for admin {}: {} (ID: {})",
created_user.id(), created_user.id(),
folder.name, folder.name,
folder.id folder.id
@@ -588,7 +588,7 @@ impl AuthApplicationService {
}, },
Err(e) => { Err(e) => {
tracing::error!( tracing::error!(
"No se pudo crear la carpeta personal para el admin {}: {}", "Could not create personal folder for admin {}: {}",
created_user.id(), created_user.id(),
e 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)) Ok(UserDto::from(created_user))
} }
+118 -118
View File
@@ -12,52 +12,52 @@ use crate::application::ports::inbound::FolderUseCase;
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto; use crate::application::dtos::folder_dto::FolderDto;
/// Errores específicos para operaciones por lotes /// Specific errors for batch operations
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum BatchOperationError { pub enum BatchOperationError {
#[error("Error de dominio: {0}")] #[error("Domain error: {0}")]
Domain(#[from] DomainError), Domain(#[from] DomainError),
#[error("Operación cancelada: {0}")] #[error("Operation cancelled: {0}")]
Cancelled(String), Cancelled(String),
#[error("Límite de concurrencia excedido: {0}")] #[error("Concurrency limit exceeded: {0}")]
ConcurrencyLimit(String), 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), PartialFailure(String, usize, usize),
#[error("Error interno: {0}")] #[error("Internal error: {0}")]
Internal(String), Internal(String),
} }
/// Resultado de una operación por lotes con estadísticas /// Result of a batch operation with statistics
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BatchResult<T> { pub struct BatchResult<T> {
/// Resultados exitosos /// Successful results
pub successful: Vec<T>, pub successful: Vec<T>,
/// Operaciones fallidas con sus errores /// Failed operations with their errors
pub failed: Vec<(String, String)>, pub failed: Vec<(String, String)>,
/// Estadísticas de la operación /// Operation statistics
pub stats: BatchStats, pub stats: BatchStats,
} }
/// Estadísticas de una operación por lotes /// Statistics of a batch operation
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct BatchStats { pub struct BatchStats {
/// Número total de operaciones /// Total number of operations
pub total: usize, pub total: usize,
/// Número de operaciones exitosas /// Number of successful operations
pub successful: usize, pub successful: usize,
/// Número de operaciones fallidas /// Number of failed operations
pub failed: usize, pub failed: usize,
/// Tiempo total de ejecución en milisegundos /// Total execution time in milliseconds
pub execution_time_ms: u128, pub execution_time_ms: u128,
/// Concurrencia máxima alcanzada /// Maximum concurrency reached
pub max_concurrency: usize, pub max_concurrency: usize,
} }
/// Servicio de operaciones por lotes /// Batch operations service
pub struct BatchOperationService { pub struct BatchOperationService {
file_retrieval: Arc<dyn FileRetrievalUseCase>, file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>, file_management: Arc<dyn FileManagementUseCase>,
@@ -67,14 +67,14 @@ pub struct BatchOperationService {
} }
impl 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( pub fn new(
file_retrieval: Arc<dyn FileRetrievalUseCase>, file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>, file_management: Arc<dyn FileManagementUseCase>,
folder_service: Arc<FolderService>, folder_service: Arc<FolderService>,
config: AppConfig config: AppConfig
) -> Self { ) -> Self {
// Limitar la concurrencia basada en la configuración // Limit concurrency based on configuration
let max_concurrency = config.concurrency.max_concurrent_files; let max_concurrency = config.concurrency.max_concurrent_files;
Self { 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( pub fn default(
file_retrieval: Arc<dyn FileRetrievalUseCase>, file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>, file_management: Arc<dyn FileManagementUseCase>,
@@ -95,16 +95,16 @@ impl BatchOperationService {
Self::new(file_retrieval, file_management, folder_service, AppConfig::default()) 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( pub async fn copy_files(
&self, &self,
file_ids: Vec<String>, file_ids: Vec<String>,
target_folder_id: Option<String>, target_folder_id: Option<String>,
) -> Result<BatchResult<FileDto>, BatchOperationError> { ) -> Result<BatchResult<FileDto>, 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone(); let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone(); let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Adquirir permiso del semáforo // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await; 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); 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) (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; let operation_results = join_all(operations).await;
// Procesar los resultados // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(file) => { 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.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Copia en lote completada: {}/{} exitosas en {}ms", "Batch copy completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -166,16 +166,16 @@ impl BatchOperationService {
Ok(result) Ok(result)
} }
/// Mueve múltiples archivos en paralelo /// Moves multiple files in parallel
pub async fn move_files( pub async fn move_files(
&self, &self,
file_ids: Vec<String>, file_ids: Vec<String>,
target_folder_id: Option<String>, target_folder_id: Option<String>,
) -> Result<BatchResult<FileDto>, BatchOperationError> { ) -> Result<BatchResult<FileDto>, 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone(); let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone(); let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Adquirir permiso del semáforo // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let move_result = mgmt.move_file(&file_id, target_folder.clone()).await; let move_result = mgmt.move_file(&file_id, target_folder.clone()).await;
// Liberar el permiso explícitamente // Release the permit explicitly
drop(permit); 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) (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; let operation_results = join_all(operations).await;
// Procesar los resultados // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(file) => { 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.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Movimiento en lote completado: {}/{} exitosas en {}ms", "Batch move completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -237,15 +237,15 @@ impl BatchOperationService {
Ok(result) Ok(result)
} }
/// Elimina múltiples archivos en paralelo /// Deletes multiple files in parallel
pub async fn delete_files( pub async fn delete_files(
&self, &self,
file_ids: Vec<String>, file_ids: Vec<String>,
) -> Result<BatchResult<String>, BatchOperationError> { ) -> Result<BatchResult<String>, 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone(); let mgmt = self.file_management.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
let id_clone = file_id.clone(); let id_clone = file_id.clone();
async move { async move {
// Adquirir permiso del semáforo // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let delete_result = mgmt.delete_file(&file_id).await; let delete_result = mgmt.delete_file(&file_id).await;
// Liberar el permiso explícitamente // Release the permit explicitly
drop(permit); drop(permit);
// Devolver el resultado junto con el ID // Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone)) (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; let operation_results = join_all(operations).await;
// Procesar los resultados // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(id) => { 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.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Eliminación en lote completada: {}/{} exitosas en {}ms", "Batch deletion completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -307,15 +307,15 @@ impl BatchOperationService {
Ok(result) 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( pub async fn get_multiple_files(
&self, &self,
file_ids: Vec<String>, file_ids: Vec<String>,
) -> Result<BatchResult<FileDto>, BatchOperationError> { ) -> Result<BatchResult<FileDto>, 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 operations = file_ids.into_iter().map(|file_id| {
let retrieval = self.file_retrieval.clone(); let retrieval = self.file_retrieval.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Adquirir permiso del semáforo // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let get_result = retrieval.get_file(&file_id).await; let get_result = retrieval.get_file(&file_id).await;
// Liberar el permiso explícitamente // Release the permit explicitly
drop(permit); drop(permit);
// Devolver el resultado junto con el ID // Return the result along with the ID
(file_id, get_result) (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; let operation_results = join_all(operations).await;
// Procesar los resultados // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(file) => { 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.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Carga en lote completada: {}/{} exitosas en {}ms", "Batch load completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -376,16 +376,16 @@ impl BatchOperationService {
Ok(result) Ok(result)
} }
/// Elimina múltiples carpetas en paralelo /// Deletes multiple folders in parallel
pub async fn delete_folders( pub async fn delete_folders(
&self, &self,
folder_ids: Vec<String>, folder_ids: Vec<String>,
_recursive: bool, _recursive: bool,
) -> Result<BatchResult<String>, BatchOperationError> { ) -> Result<BatchResult<String>, 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone(); let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
let id_clone = folder_id.clone(); let id_clone = folder_id.clone();
async move { async move {
// Adquirir permiso del semáforo // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
// For both recursive and non-recursive, use the standard delete_folder method // For both recursive and non-recursive, use the standard delete_folder method
// since FolderUseCase only has a single delete_folder method // since FolderUseCase only has a single delete_folder method
let delete_result = folder_service.delete_folder(&folder_id).await; let delete_result = folder_service.delete_folder(&folder_id).await;
// Liberar el permiso explícitamente // Release the permit explicitly
drop(permit); drop(permit);
// Devolver el resultado junto con el ID // Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone)) (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; let operation_results = join_all(operations).await;
// Procesar los resultados // Process the results
for (folder_id, operation_result) in operation_results { for (folder_id, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(id) => { 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.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Eliminación en lote de carpetas completada: {}/{} exitosas en {}ms", "Batch folder deletion completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -449,7 +449,7 @@ impl BatchOperationService {
Ok(result) 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<T, F, Fut>( pub async fn generic_batch_operation<T, F, Fut>(
&self, &self,
items: Vec<T>, items: Vec<T>,
@@ -460,10 +460,10 @@ impl BatchOperationService {
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static, F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<T, DomainError>> + Send + 'static, Fut: Future<Output = Result<T, DomainError>> + 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 tasks = items.iter().map(|item| {
let item_clone = item.clone(); let item_clone = item.clone();
let op = operation.clone(); let op = operation.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { 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; 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) (item_clone, op_result)
} }
}); });
// Ejecutar todas las tareas en paralelo // Execute all tasks in parallel
let operation_results = join_all(tasks).await; let operation_results = join_all(tasks).await;
// Procesar resultados // Process results
for (item, operation_result) in operation_results { for (item, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(result_item) => { Ok(result_item) => {
@@ -499,20 +499,20 @@ impl BatchOperationService {
result.stats.successful += 1; result.stats.successful += 1;
} }
Err(e) => { 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.failed.push((format!("{:?}", item), e.to_string()));
result.stats.failed += 1; result.stats.failed += 1;
} }
} }
} }
// Completar estadísticas // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Operación genérica en lote completada: {}/{} exitosas en {}ms", "Generic batch operation completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -521,15 +521,15 @@ impl BatchOperationService {
Ok(result) Ok(result)
} }
/// Crear múltiples carpetas en paralelo /// Create multiple folders in parallel
pub async fn create_folders( pub async fn create_folders(
&self, &self,
folders: Vec<(String, Option<String>)>, // (nombre, padre_id) folders: Vec<(String, Option<String>)>, // (name, parent_id)
) -> Result<BatchResult<FolderDto>, BatchOperationError> { ) -> Result<BatchResult<FolderDto>, 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 operations = folders.into_iter().map(|(name, parent_id)| {
let folder_service = self.folder_service.clone(); let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Adquirir permiso del semáforo // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let dto = crate::application::dtos::folder_dto::CreateFolderDto { let dto = crate::application::dtos::folder_dto::CreateFolderDto {
@@ -554,19 +554,19 @@ impl BatchOperationService {
}; };
let create_result = folder_service.create_folder(dto).await; let create_result = folder_service.create_folder(dto).await;
// Liberar el permiso explícitamente // Release the permit explicitly
drop(permit); 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()); let id = format!("{}:{}", name, parent_id.unwrap_or_default());
(id, create_result) (id, create_result)
} }
}); });
// Ejecutar todas las operaciones en paralelo // Execute all operations in parallel
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Procesar los resultados // Process the results
for (id, operation_result) in operation_results { for (id, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(folder) => { 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.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Creación en lote de carpetas completada: {}/{} exitosas en {}ms", "Batch folder creation completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -595,15 +595,15 @@ impl BatchOperationService {
Ok(result) Ok(result)
} }
/// Obtener metadatos de múltiples carpetas en paralelo /// Get metadata of multiple folders in parallel
pub async fn get_multiple_folders( pub async fn get_multiple_folders(
&self, &self,
folder_ids: Vec<String>, folder_ids: Vec<String>,
) -> Result<BatchResult<FolderDto>, BatchOperationError> { ) -> Result<BatchResult<FolderDto>, 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(); let start_time = std::time::Instant::now();
// Crear estructura para el resultado // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
failed: 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 operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone(); let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Adquirir permiso del semáforo // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let get_result = folder_service.get_folder(&folder_id).await; let get_result = folder_service.get_folder(&folder_id).await;
// Liberar el permiso explícitamente // Release the permit explicitly
drop(permit); drop(permit);
// Devolver el resultado con su ID // Return the result with its ID
(folder_id, get_result) (folder_id, get_result)
} }
}); });
// Ejecutar todas las operaciones en paralelo // Execute all operations in parallel
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Procesar los resultados // Process the results
for (folder_id, operation_result) in operation_results { for (folder_id, operation_result) in operation_results {
match operation_result { match operation_result {
Ok(folder) => { 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.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Carga en lote de carpetas completada: {}/{} exitosas en {}ms", "Batch folder load completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful,
result.stats.total, result.stats.total,
result.stats.execution_time_ms result.stats.execution_time_ms
@@ -673,7 +673,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_generic_batch_operation() { async fn test_generic_batch_operation() {
// Crear el servicio de batch with stubs // Create the batch service with stubs
let batch_service = BatchOperationService::new( let batch_service = BatchOperationService::new(
Arc::new(StubFileRetrievalUseCase), Arc::new(StubFileRetrievalUseCase),
Arc::new(StubFileManagementUseCase), Arc::new(StubFileManagementUseCase),
@@ -683,35 +683,35 @@ mod tests {
AppConfig::default() AppConfig::default()
); );
// Definir una operación genérica de prueba // Define a generic test operation
let operation = |item: i32, semaphore: Arc<Semaphore>| async move { let operation = |item: i32, semaphore: Arc<Semaphore>| async move {
// Adquirir y liberar el semáforo // Acquire and release the semaphore
let _permit = semaphore.acquire().await.unwrap(); let _permit = semaphore.acquire().await.unwrap();
if item % 2 == 0 { if item % 2 == 0 {
// Simular éxito para números pares // Simulate success for even numbers
Ok(item * 2) Ok(item * 2)
} else { } else {
// Simular error para números impares // Simulate error for odd numbers
Err(DomainError::validation_error("Odd number not allowed")) 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 items = vec![1, 2, 3, 4, 5];
let result = batch_service.generic_batch_operation(items, operation).await.unwrap(); 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.total, 5);
assert_eq!(result.stats.successful, 2); assert_eq!(result.stats.successful, 2);
assert_eq!(result.stats.failed, 3); 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(&4)); // 2*2
assert!(result.successful.contains(&8)); // 4*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); assert_eq!(result.failed.len(), 3);
} }
} }
@@ -16,7 +16,7 @@ const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
/// Threshold above which mmap is used instead of streaming (100 MB). /// Threshold above which mmap is used instead of streaming (100 MB).
const MMAP_THRESHOLD: u64 = 100 * 1024 * 1024; 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: /// Implements a multi-tier download strategy:
/// - Tier 0: Write-behind cache (just-uploaded files still in RAM) /// - Tier 0: Write-behind cache (just-uploaded files still in RAM)
@@ -29,7 +29,7 @@ fn extract_username_from_path(path: &str) -> Option<String> {
Some(parts[1].trim().to_string()) Some(parts[1].trim().to_string())
} }
/// Servicio para operaciones de subida de archivos /// Service for file upload operations
/// ///
/// Encapsulates the three-tier upload strategy: /// Encapsulates the three-tier upload strategy:
/// 1. **Write-Behind** (<256 KB): store in RAM, respond instantly, flush async. /// 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( pub fn with_storage_usage_service(
mut self, mut self,
storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>, storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>,
@@ -6,14 +6,14 @@ use crate::application::services::file_retrieval_service::FileRetrievalService;
use crate::application::services::file_management_service::FileManagementService; use crate::application::services::file_management_service::FileManagementService;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; 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 { pub struct AppFileUseCaseFactory {
file_read_repository: Arc<dyn FileReadPort>, file_read_repository: Arc<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort>, file_write_repository: Arc<dyn FileWritePort>,
} }
impl AppFileUseCaseFactory { impl AppFileUseCaseFactory {
/// Crea una nueva factory para casos de uso de archivos /// Creates a new factory for file use cases
pub fn new( pub fn new(
file_read_repository: Arc<dyn FileReadPort>, file_read_repository: Arc<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort> file_write_repository: Arc<dyn FileWritePort>
+43 -43
View File
@@ -7,13 +7,13 @@ use crate::application::ports::outbound::FolderStoragePort;
use crate::application::transactions::storage_transaction::StorageTransaction; use crate::application::transactions::storage_transaction::StorageTransaction;
use crate::common::errors::{DomainError, ErrorKind}; 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 { pub struct FolderService {
folder_storage: Arc<dyn FolderStoragePort>, folder_storage: Arc<dyn FolderStoragePort>,
} }
impl FolderService { impl FolderService {
/// Crea un nuevo servicio de carpetas /// Creates a new folder service
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self { pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
Self { folder_storage } Self { folder_storage }
} }
@@ -72,9 +72,9 @@ impl FolderService {
#[async_trait] #[async_trait]
impl FolderUseCase for FolderService { impl FolderUseCase for FolderService {
/// Crea una nueva carpeta /// Creates a new folder
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> { async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
// Validación de entrada // Input validation
if dto.name.is_empty() { if dto.name.is_empty() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, 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 { if let Some(parent_id) = &dto.parent_id {
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok(); let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists { 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) let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?; .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?;
// Convertir a DTO // Convert to DTO
Ok(FolderDto::from(folder)) Ok(FolderDto::from(folder))
} }
/// Obtiene una carpeta por su ID /// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> { async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
let folder = self.folder_storage.get_folder(id) let folder = self.folder_storage.get_folder(id)
.await .await
@@ -109,9 +109,9 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder)) 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<FolderDto, DomainError> { async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
// Convertir la ruta de string a StoragePath // Convert the string path to StoragePath
let storage_path = StoragePath::from_string(path); let storage_path = StoragePath::from_string(path);
let folder = self.folder_storage.get_folder_by_path(&storage_path) let folder = self.folder_storage.get_folder_by_path(&storage_path)
@@ -121,39 +121,39 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder)) 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<Vec<FolderDto>, DomainError> { async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
let folders = self.folder_storage.list_folders(parent_id) let folders = self.folder_storage.list_folders(parent_id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?; .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()) Ok(folders.into_iter().map(FolderDto::from).collect())
} }
/// Lista carpetas con paginación /// Lists folders with pagination
async fn list_folders_paginated( async fn list_folders_paginated(
&self, &self,
parent_id: Option<&str>, parent_id: Option<&str>,
pagination: &crate::application::dtos::pagination::PaginationRequestDto pagination: &crate::application::dtos::pagination::PaginationRequestDto
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> { ) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
// Validar y ajustar la paginación // Validate and adjust pagination
let pagination = pagination.validate_and_adjust(); 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( let (folders, total_items) = self.folder_storage.list_folders_paginated(
parent_id, parent_id,
pagination.offset(), pagination.offset(),
pagination.limit(), pagination.limit(),
true // Siempre incluir total para mejor UX true // Always include total for better UX
) )
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?; .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()); let total = total_items.unwrap_or(folders.len());
// Convertir a PaginatedResponseDto // Convert to PaginatedResponseDto
let response = crate::application::dtos::pagination::PaginatedResponseDto::new( let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
folders.into_iter().map(FolderDto::from).collect(), folders.into_iter().map(FolderDto::from).collect(),
pagination.page, pagination.page,
@@ -164,9 +164,9 @@ impl FolderUseCase for FolderService {
Ok(response) Ok(response)
} }
/// Renombra una carpeta /// Renames a folder
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> { async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> {
// Validación de entrada // Input validation
if dto.name.is_empty() { if dto.name.is_empty() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, 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) let existing_folder = self.folder_storage.get_folder(id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?; .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"); let mut transaction = StorageTransaction::new("rename_folder");
// Operación principal: renombrar carpeta // Main operation: rename folder
// Clone all values to avoid lifetime issues // Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone(); let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string(); let id_owned = id.to_string();
@@ -200,7 +200,7 @@ impl FolderUseCase for FolderService {
let id_clone = id.to_string(); let id_clone = id.to_string();
async move { 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 storage.rename_folder(&id_clone, original_name).await
.map(|_| ()) .map(|_| ())
.map_err(|e| DomainError::new( .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); transaction.add_operation(rename_op, rollback_op);
// Ejecutar transacción // Execute transaction
transaction.commit().await?; transaction.commit().await?;
// Obtener la carpeta renombrada // Get the renamed folder
let folder = self.folder_storage.get_folder(id) let folder = self.folder_storage.get_folder(id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?; .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)) 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<FolderDto, DomainError> { async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
// Verificar que la carpeta origen existe // Verify the source folder exists
let source_folder = self.folder_storage.get_folder(id) let source_folder = self.folder_storage.get_folder(id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?; .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 { 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 { if parent_id == id {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, 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(); let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists { if !parent_exists {
return Err(DomainError::not_found("Folder", parent_id)); 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"); let mut transaction = StorageTransaction::new("move_folder");
// Operación principal: mover carpeta // Main operation: move folder
// Clone all values to avoid lifetime issues // Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone(); let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string(); let id_owned = id.to_string();
@@ -275,7 +275,7 @@ impl FolderUseCase for FolderService {
let id_clone = id.to_string(); let id_clone = id.to_string();
async move { 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 storage.move_folder(&id_clone, original_parent_id.as_deref()).await
.map(|_| ()) .map(|_| ())
.map_err(|e| DomainError::new( .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); transaction.add_operation(move_op, rollback_op);
// Ejecutar transacción // Execute transaction
transaction.commit().await?; transaction.commit().await?;
// Obtener la carpeta movida // Get the moved folder
let folder = self.folder_storage.get_folder(id) let folder = self.folder_storage.get_folder(id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?; .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)) Ok(FolderDto::from(folder))
} }
/// Elimina una carpeta /// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> { 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) let _folder = self.folder_storage.get_folder(id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?; .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) self.folder_storage.delete_folder(id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e))) .map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
+17 -17
View File
@@ -5,17 +5,17 @@ use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::application::ports::recent_ports::{RecentItemsUseCase, RecentItemsRepositoryPort}; use crate::application::ports::recent_ports::{RecentItemsUseCase, RecentItemsRepositoryPort};
use crate::application::dtos::recent_dto::RecentItemDto; 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 /// Depends on `RecentItemsRepositoryPort` (outbound port) instead
/// de acceder directamente a `PgPool`, siguiendo la arquitectura hexagonal. /// of accessing `PgPool` directly, following the hexagonal architecture.
pub struct RecentService { pub struct RecentService {
repo: Arc<dyn RecentItemsRepositoryPort>, repo: Arc<dyn RecentItemsRepositoryPort>,
max_recent_items: i32, max_recent_items: i32,
} }
impl RecentService { impl RecentService {
/// Crear un nuevo servicio de elementos recientes /// Create a new recent items service
pub fn new(repo: Arc<dyn RecentItemsRepositoryPort>, max_recent_items: i32) -> Self { pub fn new(repo: Arc<dyn RecentItemsRepositoryPort>, max_recent_items: i32) -> Self {
Self { Self {
repo, repo,
@@ -26,51 +26,51 @@ impl RecentService {
#[async_trait] #[async_trait]
impl RecentItemsUseCase for RecentService { 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<i32>) -> Result<Vec<RecentItemDto>> { async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>> {
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 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?; 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) 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<()> { 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" { if item_type != "file" && item_type != "folder" {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"RecentItems", "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.upsert_access(user_id, item_id, item_type).await?;
self.repo.prune(user_id, self.max_recent_items).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(()) 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<bool> { async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
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?; let removed = self.repo.remove_item(user_id, item_id, item_type).await?;
info!( info!(
"{} {} '{}' de recientes para usuario {}", "{} {} '{}' 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 item_type, item_id, user_id
); );
Ok(removed) Ok(removed)
} }
/// Limpiar todos los elementos recientes /// Clear all recent items
async fn clear_recent_items(&self, user_id: &str) -> Result<()> { 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?; 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(()) Ok(())
} }
} }
+93 -93
View File
@@ -14,57 +14,57 @@ use crate::application::ports::outbound::FolderStoragePort;
use crate::application::ports::storage_ports::FileReadPort; 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 * This service implements the advanced search functionality that allows
* a los usuarios encontrar archivos y carpetas basados en diversos criterios * users to find files and folders based on various criteria
* como nombre, tipo, fecha y tamaño. También incluye una caché para mejorar * such as name, type, date and size. It also includes a cache to improve
* el rendimiento de búsquedas repetidas. * the performance of repeated searches.
*/ */
pub struct SearchService { pub struct SearchService {
/// Repositorio para operaciones con archivos /// Repository for file operations
file_repository: Arc<dyn FileReadPort>, file_repository: Arc<dyn FileReadPort>,
/// Repositorio para operaciones con carpetas /// Repository for folder operations
folder_repository: Arc<dyn FolderStoragePort>, folder_repository: Arc<dyn FolderStoragePort>,
/// Caché de resultados de búsqueda con tiempo de expiración /// Search results cache with expiration time
search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>, search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
/// Duración de validez de la caché en segundos /// Cache validity duration in seconds
cache_ttl: u64, 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, max_cache_size: usize,
} }
/// Clave para la caché de búsqueda /// Key for the search cache
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct SearchCacheKey { struct SearchCacheKey {
/// Representación serializada de los criterios de búsqueda /// Serialized representation of the search criteria
criteria_hash: String, criteria_hash: String,
/// ID del usuario (para aislar búsquedas entre usuarios) /// User ID (to isolate searches between users)
user_id: String, user_id: String,
} }
/// Resultado de búsqueda en caché con tiempo de expiración /// Cached search result with expiration time
struct CachedSearchResult { struct CachedSearchResult {
/// Resultados de la búsqueda /// Search results
results: SearchResultsDto, results: SearchResultsDto,
/// Momento en que se creó la entrada de caché /// Time when the cache entry was created
timestamp: Instant, timestamp: Instant,
} }
impl SearchService { 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 file_repository Repository for file operations
* @param folder_repository Repositorio para operaciones con carpetas * @param folder_repository Repository for folder operations
* @param cache_ttl Tiempo de vida de la caché en segundos (0 para desactivar) * @param cache_ttl Cache time-to-live in seconds (0 to disable)
* @param max_cache_size Tamaño máximo de la caché * @param max_cache_size Maximum cache size
*/ */
pub fn new( pub fn new(
file_repository: Arc<dyn FileReadPort>, file_repository: Arc<dyn FileReadPort>,
@@ -80,7 +80,7 @@ impl SearchService {
max_cache_size, max_cache_size,
}; };
// Iniciar tarea de limpieza de caché si TTL > 0 // Start cache cleanup task if TTL > 0
if cache_ttl > 0 { if cache_ttl > 0 {
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl); 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 cache_ref Reference to the shared cache
* @param ttl_seconds TTL en segundos * @param ttl_seconds TTL in seconds
*/ */
fn start_cache_cleanup_task( fn start_cache_cleanup_task(
cache_ref: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>, cache_ref: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
@@ -105,18 +105,18 @@ impl SearchService {
loop { loop {
time::sleep(cleanup_interval).await; 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() { if let Ok(mut cache) = cache_ref.lock() {
let now = Instant::now(); let now = Instant::now();
// Identificar entradas expiradas // Identify expired entries
let expired_keys: Vec<SearchCacheKey> = cache let expired_keys: Vec<SearchCacheKey> = cache
.iter() .iter()
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl) .filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
.map(|(key, _)| key.clone()) .map(|(key, _)| key.clone())
.collect(); .collect();
// Eliminar entradas expiradas // Remove expired entries
for key in expired_keys { for key in expired_keys {
cache.remove(&key); 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 criteria Search criteria
* @param user_id ID del usuario (para aislar caché entre usuarios) * @param user_id User ID (to isolate cache between users)
* @return Clave para la caché * @return Cache key
*/ */
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey { 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(); let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
SearchCacheKey { 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é * @param key Cache key
* @return Opcionalmente, los resultados si existen y no han expirado * @return Optionally, the results if they exist and have not expired
*/ */
fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> { fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> {
// Si TTL es 0, la caché está desactivada // If TTL is 0, the cache is disabled
if self.cache_ttl == 0 { if self.cache_ttl == 0 {
return None; return None;
} }
@@ -159,7 +159,7 @@ impl SearchService {
let now = Instant::now(); let now = Instant::now();
let ttl = Duration::from_secs(self.cache_ttl); 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 { if now.duration_since(cached_result.timestamp) < ttl {
return Some(cached_result.results.clone()); 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 key Cache key
* @param results Resultados a almacenar * @param results Results to store
*/ */
fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) { 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 { if self.cache_ttl == 0 {
return; return;
} }
if let Ok(mut cache) = self.search_cache.lock() { 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 cache.len() >= self.max_cache_size {
if let Some((oldest_key, _)) = cache if let Some((oldest_key, _)) = cache
.iter() .iter()
@@ -192,7 +192,7 @@ impl SearchService {
} }
} }
// Almacenar el nuevo resultado // Store the new result
cache.insert(key, CachedSearchResult { cache.insert(key, CachedSearchResult {
results, results,
timestamp: Instant::now(), 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 files List of files to filter
* @param criteria Criterios de búsqueda * @param criteria Search criteria
* @return Archivos que cumplen con los criterios * @return Files that match the criteria
*/ */
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> { fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
files.into_iter() files.into_iter()
.filter(|file| { .filter(|file| {
// Filtrar por nombre // Filter by name
if let Some(name_query) = &criteria.name_contains { if let Some(name_query) = &criteria.name_contains {
if !file.name.to_lowercase().contains(&name_query.to_lowercase()) { if !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false; 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(file_types) = &criteria.file_types {
if let Some(extension) = file.name.split('.').last() { if let Some(extension) = file.name.split('.').last() {
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) { if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) {
return false; return false;
} }
} else { } else {
// No tiene extensión // Has no extension
return false; return false;
} }
} }
// Filtrar por fecha de creación // Filter by creation date
if let Some(created_after) = criteria.created_after { if let Some(created_after) = criteria.created_after {
if file.created_at < created_after { if file.created_at < created_after {
return false; 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 let Some(modified_after) = criteria.modified_after {
if file.modified_at < modified_after { if file.modified_at < modified_after {
return false; return false;
@@ -255,7 +255,7 @@ impl SearchService {
} }
} }
// Filtrar por tamaño // Filter by size
if let Some(min_size) = criteria.min_size { if let Some(min_size) = criteria.min_size {
if file.size < min_size { if file.size < min_size {
return false; 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 folders List of folders to filter
* @param criteria Criterios de búsqueda * @param criteria Search criteria
* @return Carpetas que cumplen con los criterios * @return Folders that match the criteria
*/ */
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> { fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
folders.into_iter() folders.into_iter()
.filter(|folder| { .filter(|folder| {
// Filtrar por nombre // Filter by name
if let Some(name_query) = &criteria.name_contains { if let Some(name_query) = &criteria.name_contains {
if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) { if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false; return false;
} }
} }
// Filtrar por fecha de creación // Filter by creation date
if let Some(created_after) = criteria.created_after { if let Some(created_after) = criteria.created_after {
if folder.created_at < created_after { if folder.created_at < created_after {
return false; 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 let Some(modified_after) = criteria.modified_after {
if folder.modified_at < modified_after { if folder.modified_at < modified_after {
return false; 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 current_folder_id ID of the current folder
* @param criteria Criterios de búsqueda * @param criteria Search criteria
* @param found_files Archivos encontrados hasta ahora * @param found_files Files found so far
* @param found_folders Carpetas encontradas hasta ahora * @param found_folders Folders found so far
*/ */
async fn search_recursive( async fn search_recursive(
&self, &self,
@@ -337,31 +337,31 @@ impl SearchService {
found_folders: &mut Vec<FolderDto>, found_folders: &mut Vec<FolderDto>,
) -> Result<()> { ) -> Result<()> {
Box::pin(async move { 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?; 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( let filtered_files = self.filter_files(
files.into_iter().map(FileDto::from).collect(), files.into_iter().map(FileDto::from).collect(),
criteria criteria
); );
found_files.extend(filtered_files); found_files.extend(filtered_files);
// Si la búsqueda es recursiva, procesar subcarpetas // If the search is recursive, process subfolders
if criteria.recursive { if criteria.recursive {
// Listar subcarpetas // List subfolders
let folders = self.folder_repository.list_folders(current_folder_id).await?; 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<FolderDto> = self.filter_folders( let filtered_folders: Vec<FolderDto> = self.filter_folders(
folders.into_iter().map(FolderDto::from).collect(), folders.into_iter().map(FolderDto::from).collect(),
criteria criteria
); );
// Añadir las carpetas filtradas a los resultados // Add filtered folders to the results
found_folders.extend(filtered_folders.iter().cloned()); found_folders.extend(filtered_folders.iter().cloned());
// Buscar recursivamente en cada subcarpeta // Search recursively in each subfolder
for folder in filtered_folders { for folder in filtered_folders {
self.search_recursive( self.search_recursive(
Some(&folder.id), Some(&folder.id),
@@ -380,26 +380,26 @@ impl SearchService {
#[async_trait] #[async_trait]
impl SearchUseCase for SearchService { 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 * @param criteria Search criteria
* @return Resultados de la búsqueda * @return Search results
*/ */
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> { async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
// TODO: Obtener ID de usuario del contexto de autenticación // TODO: Get user ID from the authentication context
let user_id = "default-user"; let user_id = "default-user";
let cache_key = self.create_cache_key(&criteria, user_id); 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) { if let Some(cached_results) = self.get_from_cache(&cache_key) {
return Ok(cached_results); return Ok(cached_results);
} }
// Inicializar colecciones para resultados // Initialize collections for results
let mut found_files: Vec<FileDto> = Vec::new(); let mut found_files: Vec<FileDto> = Vec::new();
let mut found_folders: Vec<FolderDto> = Vec::new(); let mut found_folders: Vec<FolderDto> = 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( self.search_recursive(
criteria.folder_id.as_deref(), criteria.folder_id.as_deref(),
&criteria, &criteria,
@@ -407,29 +407,29 @@ impl SearchUseCase for SearchService {
&mut found_folders, &mut found_folders,
).await?; ).await?;
// Aplicar paginación // Apply pagination
let total_count = found_files.len() + found_folders.len(); let total_count = found_files.len() + found_folders.len();
// Ordenar por relevancia o fecha según criterios // Sort by relevance or date according to criteria
// Por defecto, ordenamos por fecha de modificación (más reciente primero) // By default, sort by modification date (most recent first)
found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); 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)); 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 start_idx = criteria.offset.min(total_count);
let end_idx = (criteria.offset + criteria.limit).min(total_count); let end_idx = (criteria.offset + criteria.limit).min(total_count);
let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx) let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx)
.map(|i| { .map(|i| {
if i < found_folders.len() { if i < found_folders.len() {
(true, i) // Es una carpeta (true, i) // It's a folder
} else { } else {
(false, i - found_folders.len()) // Es un archivo (false, i - found_folders.len()) // It's a file
} }
}) })
.collect(); .collect();
// Extraer elementos paginados // Extract paginated items
let mut paginated_folders = Vec::new(); let mut paginated_folders = Vec::new();
let mut paginated_files = 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( let search_results = SearchResultsDto::new(
paginated_files, paginated_files,
paginated_folders, paginated_folders,
@@ -454,16 +454,16 @@ impl SearchUseCase for SearchService {
Some(total_count), Some(total_count),
); );
// Almacenar en caché // Store in cache
self.store_in_cache(cache_key, search_results.clone()); self.store_in_cache(cache_key, search_results.clone());
Ok(search_results) 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<()> { async fn clear_search_cache(&self) -> Result<()> {
if let Ok(mut cache) = self.search_cache.lock() { 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 { 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 { pub fn new_stub() -> impl SearchUseCase {
struct SearchServiceStub; struct SearchServiceStub;
+38 -38
View File
@@ -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( async fn verify_item_exists(
&self, &self,
item_id: &str, item_id: &str,
@@ -89,13 +89,13 @@ impl ShareService {
match item_type { match item_type {
ShareItemType::File => { ShareItemType::File => {
self.file_repository 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 .await
.map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?; .map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?;
} }
ShareItemType::Folder => { ShareItemType::Folder => {
self.folder_repository 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 .await
.map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?; .map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?;
} }
@@ -103,7 +103,7 @@ impl ShareService {
Ok(()) 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 { fn hash_password(&self, password: &str) -> String {
use argon2::{Argon2, PasswordHasher}; use argon2::{Argon2, PasswordHasher};
use argon2::password_hash::SaltString; use argon2::password_hash::SaltString;
@@ -126,20 +126,20 @@ impl ShareUseCase for ShareService {
user_id: &str, user_id: &str,
dto: CreateShareDto, dto: CreateShareDto,
) -> Result<ShareDto, DomainError> { ) -> Result<ShareDto, DomainError> {
// Convertir el tipo de elemento // Convert the item type
let item_type = ShareItemType::try_from(dto.item_type.as_str()) let item_type = ShareItemType::try_from(dto.item_type.as_str())
.map_err(|e| ShareServiceError::InvalidItemType(e.to_string()))?; .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?; 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()); 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)); let password_hash = dto.password.map(|p| self.hash_password(&p));
// Crear la entidad Share // Create the Share entity
let share = Share::new( let share = Share::new(
dto.item_id.clone(), dto.item_id.clone(),
item_type, item_type,
@@ -150,48 +150,48 @@ impl ShareUseCase for ShareService {
) )
.map_err(|e| ShareServiceError::Validation(e.to_string()))?; .map_err(|e| ShareServiceError::Validation(e.to_string()))?;
// Guardar en el repositorio // Save to the repository
let saved_share = self let saved_share = self
.share_repository .share_repository
.save_share(&share) .save_share(&share)
.await .await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?; .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))) 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<ShareDto, DomainError> { async fn get_shared_link(&self, id: &str) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido por su ID // Find the shared link by its ID
let share = self let share = self
.share_repository .share_repository
.find_share_by_id(id) .find_share_by_id(id)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?; .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() { if share.is_expired() {
return Err(ShareServiceError::Expired.into()); 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))) 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<ShareDto, DomainError> { async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido por su token // Find the shared link by its token
let share = self let share = self
.share_repository .share_repository
.find_share_by_token(token) .find_share_by_token(token)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; .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() { if share.is_expired() {
return Err(ShareServiceError::Expired.into()); 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))) 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_id: &str,
item_type: &ShareItemType, item_type: &ShareItemType,
) -> Result<Vec<ShareDto>, DomainError> { ) -> Result<Vec<ShareDto>, DomainError> {
// Buscar todos los enlaces compartidos para el elemento // Find all shared links for the item
let shares = self let shares = self
.share_repository .share_repository
.find_shares_by_item(item_id, item_type) .find_shares_by_item(item_id, item_type)
.await .await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?; .map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Filtrar los enlaces expirados // Filter out expired links
let active_shares: Vec<Share> = shares.into_iter().filter(|s| !s.is_expired()).collect(); let active_shares: Vec<Share> = 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 let share_dtos = active_shares
.iter() .iter()
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) .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, id: &str,
dto: UpdateShareDto, dto: UpdateShareDto,
) -> Result<ShareDto, DomainError> { ) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido existente // Find the existing shared link
let mut share = self let mut share = self
.share_repository .share_repository
.find_share_by_id(id) .find_share_by_id(id)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?; .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 { if let Some(permissions_dto) = dto.permissions {
let permissions = SharePermissions::new( let permissions = SharePermissions::new(
permissions_dto.read, permissions_dto.read,
@@ -241,7 +241,7 @@ impl ShareUseCase for ShareService {
share = share.with_permissions(permissions); share = share.with_permissions(permissions);
} }
// Actualizar contraseña si se proporciona // Update password if provided
if let Some(password) = dto.password { if let Some(password) = dto.password {
let password_hash = if password.is_empty() { let password_hash = if password.is_empty() {
None None
@@ -251,24 +251,24 @@ impl ShareUseCase for ShareService {
share = share.with_password(password_hash); 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() { if dto.expires_at.is_some() {
share = share.with_expiration(dto.expires_at); share = share.with_expiration(dto.expires_at);
} }
// Guardar los cambios // Save the changes
let updated_share = self let updated_share = self
.share_repository .share_repository
.update_share(&share) .update_share(&share)
.await .await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?; .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))) 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> { async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> {
// Eliminar el enlace compartido // Delete the shared link
self.share_repository self.share_repository
.delete_share(id) .delete_share(id)
.await .await
@@ -283,23 +283,23 @@ impl ShareUseCase for ShareService {
page: usize, page: usize,
per_page: usize, per_page: usize,
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> { ) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
// Calcular offset para paginación // Calculate offset for pagination
let offset = (page - 1) * per_page; let offset = (page - 1) * per_page;
// Buscar los enlaces compartidos del usuario // Find the user's shared links
let (shares, total) = self let (shares, total) = self
.share_repository .share_repository
.find_shares_by_user(user_id, offset, per_page) .find_shares_by_user(user_id, offset, per_page)
.await .await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?; .map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convertir las entidades a DTOs // Convert the entities to DTOs
let share_dtos: Vec<ShareDto> = shares let share_dtos: Vec<ShareDto> = shares
.iter() .iter()
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port))) .map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
.collect(); .collect();
// Crear el resultado paginado // Create the paginated result
let paginated = PaginatedResponseDto::new( let paginated = PaginatedResponseDto::new(
share_dtos, share_dtos,
page, page,
@@ -315,19 +315,19 @@ impl ShareUseCase for ShareService {
token: &str, token: &str,
password: &str, password: &str,
) -> Result<bool, DomainError> { ) -> Result<bool, DomainError> {
// Buscar el enlace compartido por su token // Find the shared link by its token
let share = self let share = self
.share_repository .share_repository
.find_share_by_token(token) .find_share_by_token(token)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; .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() { if share.is_expired() {
return Err(ShareServiceError::Expired.into()); 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() { match share.password_hash() {
Some(hash) => { Some(hash) => {
self.password_hasher.verify_password(password, 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> { 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 let share = self
.share_repository .share_repository
.find_share_by_token(token) .find_share_by_token(token)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; .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() { if share.is_expired() {
return Err(ShareServiceError::Expired.into()); return Err(ShareServiceError::Expired.into());
} }
// Incrementar el contador de accesos // Increment the access counter
let updated_share = share.increment_access_count(); let updated_share = share.increment_access_count();
// Guardar los cambios // Save the changes
self.share_repository self.share_repository
.update_share(&updated_share) .update_share(&updated_share)
.await .await
+31 -31
View File
@@ -7,69 +7,69 @@ use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort}; use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort};
/// Errores específicos del mediador de almacenamiento /// Storage mediator specific errors
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum StorageMediatorError { pub enum StorageMediatorError {
#[error("Entidad no encontrada: {0}")] #[error("Entity not found: {0}")]
NotFound(String), NotFound(String),
#[error("Entidad ya existe: {0}")] #[error("Entity already exists: {0}")]
AlreadyExists(String), AlreadyExists(String),
#[error("Ruta inválida: {0}")] #[error("Invalid path: {0}")]
InvalidPath(String), InvalidPath(String),
#[error("Error de acceso: {0}")] #[error("Access error: {0}")]
AccessError(String), AccessError(String),
#[error("Error interno: {0}")] #[error("Internal error: {0}")]
InternalError(String), InternalError(String),
#[error("Error de dominio: {0}")] #[error("Domain error: {0}")]
DomainError(#[from] crate::common::errors::DomainError), DomainError(#[from] crate::common::errors::DomainError),
} }
/// Tipo de resultado para las operaciones del mediador /// Result type for mediator operations
pub type StorageMediatorResult<T> = Result<T, StorageMediatorError>; pub type StorageMediatorResult<T> = Result<T, StorageMediatorError>;
/// Interfaz del servicio mediador entre repositorios de archivos y carpetas /// Interface for the mediator service between file and folder repositories
#[async_trait] #[async_trait]
pub trait StorageMediator: Send + Sync + 'static { 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<PathBuf>; async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf>;
/// 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<StoragePath>; async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>;
/// 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<Folder>; async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>;
/// 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<bool>; async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
/// 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<bool>; async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
/// 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<bool>; async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
/// 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<bool>; async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
/// Resuelve una ruta relativa a absoluta (legacy) /// Resolves a relative path to absolute (legacy)
fn resolve_path(&self, relative_path: &Path) -> PathBuf; 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; 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<()>; 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<()>; 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 struct FileSystemStorageMediator {
pub folder_storage_port: Arc<dyn FolderStoragePort>, pub folder_storage_port: Arc<dyn FolderStoragePort>,
pub path_service: Arc<dyn StoragePort>, pub path_service: Arc<dyn StoragePort>,
@@ -189,7 +189,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> { async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_path(path); 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(); let exists = abs_path.exists() && abs_path.is_file();
Ok(exists) Ok(exists)
@@ -198,7 +198,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> { async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path); 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(); let exists = abs_path.exists() && abs_path.is_file();
Ok(exists) Ok(exists)
@@ -207,7 +207,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> { async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_path(path); 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(); let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists) Ok(exists)
@@ -216,7 +216,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> { async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path); 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(); let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists) Ok(exists)
@@ -236,13 +236,13 @@ impl StorageMediator for FileSystemStorageMediator {
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> { async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
let abs_path = self.resolve_path(path); let abs_path = self.resolve_path(path);
// Crear directorios si no existen // Create directories if they don't exist
if !abs_path.exists() { if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await 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() { } else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath( 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<()> { async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
let abs_path = self.resolve_storage_path(storage_path); 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() { if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await 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() { } else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath( 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())
)); ));
} }
+4 -14
View File
@@ -303,17 +303,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;
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() {
TrashedItemType::File => { TrashedItemType::File => {
// Restore the file to its original location // Restore the file to its original location
let file_id = item.original_id().to_string(); 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); info!("Retrieving trash item from repository: ID={}", trash_id);
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await; 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 // Permanently delete based on type
match item.item_type() { match item.item_type() {
TrashedItemType::File => { TrashedItemType::File => {
// Eliminar el archivo permanentemente // Permanently delete the file
let file_id = item.original_id().to_string(); let file_id = item.original_id().to_string();
info!("Permanently deleting file: {}", file_id); info!("Permanently deleting file: {}", file_id);
@@ -466,7 +456,7 @@ impl TrashUseCase for TrashService {
} }
}, },
TrashedItemType::Folder => { TrashedItemType::Folder => {
// Eliminar la carpeta permanentemente // Permanently delete the folder
let folder_id = item.original_id().to_string(); let folder_id = item.original_id().to_string();
info!("Permanently deleting folder: {}", folder_id); info!("Permanently deleting folder: {}", folder_id);
@@ -2,22 +2,22 @@ use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use crate::common::errors::{DomainError, ErrorKind}; use crate::common::errors::{DomainError, ErrorKind};
/// Tipo para operaciones y rollbacks asíncronos /// Type for async operations and rollbacks
type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>; type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>;
/// Transacción para operaciones de almacenamiento /// Transaction for storage operations
/// Permite definir un conjunto de operaciones y sus rollbacks correspondientes /// Allows defining a set of operations and their corresponding rollbacks
pub struct StorageTransaction { pub struct StorageTransaction {
/// Operaciones a ejecutar /// Operations to execute
operations: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>, operations: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
/// Operaciones de rollback para revertir cambios en caso de error /// Rollback operations to revert changes in case of error
rollbacks: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>, rollbacks: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
/// Nombre de la transacción para logging /// Transaction name for logging
name: String, name: String,
} }
impl StorageTransaction { impl StorageTransaction {
/// Crea una nueva transacción /// Creates a new transaction
pub fn new(name: &str) -> Self { pub fn new(name: &str) -> Self {
Self { Self {
operations: Vec::new(), 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<F, R>(&mut self, operation: F, rollback: R) pub fn add_operation<F, R>(&mut self, operation: F, rollback: R)
where where
F: Future<Output = Result<(), DomainError>> + Send + 'static, F: Future<Output = Result<(), DomainError>> + Send + 'static,
@@ -36,74 +36,74 @@ impl StorageTransaction {
self.rollbacks.push(Box::new(move || Box::pin(rollback))); 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<F>(&mut self, finalizer: F) pub fn add_finalizer<F>(&mut self, finalizer: F)
where where
F: Future<Output = Result<(), DomainError>> + Send + 'static, F: Future<Output = Result<(), DomainError>> + Send + 'static,
{ {
// El rollback es una operación nula // The rollback is a no-op
let noop = async { Ok(()) }; let noop = async { Ok(()) };
self.operations.push(Box::new(move || Box::pin(finalizer))); self.operations.push(Box::new(move || Box::pin(finalizer)));
self.rollbacks.push(Box::new(move || Box::pin(noop))); self.rollbacks.push(Box::new(move || Box::pin(noop)));
} }
/// Ejecuta la transacción aplicando todas las operaciones en orden /// Executes the transaction by applying all operations in order
/// Si alguna falla, ejecuta los rollbacks en orden inverso /// If any fails, executes rollbacks in reverse order
pub async fn commit(mut self) -> Result<(), DomainError> { 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(); 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 operations = std::mem::take(&mut self.operations);
let transaction_name = self.name.clone(); let transaction_name = self.name.clone();
// Ejecutar operaciones // Execute operations
for (i, op) in operations.into_iter().enumerate() { for (i, op) in operations.into_iter().enumerate() {
match op().await { match op().await {
Ok(()) => { Ok(()) => {
completed_ops.push(i); 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) => { 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?; self.rollback(completed_ops).await?;
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Transaction", "Transaction",
format!("Falló la transacción '{}': {}", transaction_name, e) format!("Transaction '{}' failed: {}", transaction_name, e)
).with_source(e)); ).with_source(e));
} }
} }
} }
tracing::debug!("Transacción completada exitosamente: {}", transaction_name); tracing::debug!("Transaction completed successfully: {}", transaction_name);
Ok(()) Ok(())
} }
/// Ejecuta rollbacks para las operaciones completadas /// Executes rollbacks for completed operations
async fn rollback(mut self, completed_ops: Vec<usize>) -> Result<(), DomainError> { async fn rollback(mut self, completed_ops: Vec<usize>) -> 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(); let mut rollback_errors = Vec::new();
// Extraer rollbacks para evitar problemas de propiedad // Extract rollbacks to avoid ownership issues
let mut rollbacks = Vec::new(); let mut rollbacks = Vec::new();
std::mem::swap(&mut rollbacks, &mut self.rollbacks); 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() { for i in completed_ops.into_iter().rev() {
if i < rollbacks.len() { 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) { 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(()) }))); let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) })));
if let Err(e) = rollback().await { 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); i, self.name, e);
rollback_errors.push(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() { 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()); self.name, rollback_errors.len());
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Transaction", "Transaction",
format!("Errores durante rollback de transacción '{}': {} errores", format!("Errors during transaction '{}' rollback: {} errors",
self.name, rollback_errors.len()) self.name, rollback_errors.len())
)); ));
} }
tracing::info!("Rollback de transacción completado: {}", self.name); tracing::info!("Transaction rollback completed: {}", self.name);
Ok(()) Ok(())
} }
} }
+14 -14
View File
@@ -5,10 +5,10 @@ use std::time::Duration;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configurar logging // Configure logging
tracing_subscriber::fmt::init(); 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") { if let Ok(path) = env::var("DOTENV_PATH") {
dotenv::from_path(Path::new(&path)).ok(); dotenv::from_path(Path::new(&path)).ok();
} else { } else {
@@ -16,34 +16,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
} }
// Obtener DATABASE_URL desde variables de entorno // Get DATABASE_URL from environment variables
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada"); 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() let pool = PgPoolOptions::new()
.max_connections(5) .max_connections(5)
.acquire_timeout(Duration::from_secs(10)) .acquire_timeout(Duration::from_secs(10))
.connect(&database_url) .connect(&database_url)
.await?; .await?;
// Ejecutar migraciones // Run migrations
println!("Ejecutando migraciones..."); 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()); 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)) let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir))
.await .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?; migrator.run(&pool).await?;
println!("Migraciones aplicadas correctamente"); println!("Migrations applied successfully");
Ok(()) Ok(())
} }
+77 -77
View File
@@ -2,98 +2,98 @@ use std::time::Duration;
use std::path::PathBuf; use std::path::PathBuf;
use std::env; use std::env;
/// Configuración de caché /// Cache configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CacheConfig { pub struct CacheConfig {
/// TTL para entradas de archivos en caché (ms) /// TTL for file cache entries (ms)
pub file_ttl_ms: u64, pub file_ttl_ms: u64,
/// TTL para entradas de directorios en caché (ms) /// TTL for directory cache entries (ms)
pub directory_ttl_ms: u64, pub directory_ttl_ms: u64,
/// Máximo número de entradas en caché /// Maximum number of cache entries
pub max_entries: usize, pub max_entries: usize,
} }
impl Default for CacheConfig { impl Default for CacheConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
file_ttl_ms: 60_000, // 1 minuto file_ttl_ms: 60_000, // 1 minute
directory_ttl_ms: 120_000, // 2 minutos directory_ttl_ms: 120_000, // 2 minutes
max_entries: 10_000, // 10,000 entradas max_entries: 10_000, // 10,000 entries
} }
} }
} }
/// Configuración de timeouts para diferentes operaciones /// Timeout configuration for different operations
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TimeoutConfig { pub struct TimeoutConfig {
/// Timeout para operaciones de archivo (ms) /// Timeout for file operations (ms)
pub file_operation_ms: u64, pub file_operation_ms: u64,
/// Timeout para operaciones de directorio (ms) /// Timeout for directory operations (ms)
pub dir_operation_ms: u64, pub dir_operation_ms: u64,
/// Timeout para adquisición de locks (ms) /// Timeout for lock acquisition (ms)
pub lock_acquisition_ms: u64, pub lock_acquisition_ms: u64,
/// Timeout para operaciones de red (ms) /// Timeout for network operations (ms)
pub network_operation_ms: u64, pub network_operation_ms: u64,
} }
impl Default for TimeoutConfig { impl Default for TimeoutConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
file_operation_ms: 10000, // 10 segundos file_operation_ms: 10000, // 10 seconds
dir_operation_ms: 30000, // 30 segundos dir_operation_ms: 30000, // 30 seconds
lock_acquisition_ms: 5000, // 5 segundos lock_acquisition_ms: 5000, // 5 seconds
network_operation_ms: 15000, // 15 segundos network_operation_ms: 15000, // 15 seconds
} }
} }
} }
impl TimeoutConfig { impl TimeoutConfig {
/// Obtiene un Duration para operaciones de archivo /// Gets a Duration for file operations
pub fn file_timeout(&self) -> Duration { pub fn file_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms) 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 { pub fn file_write_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms) 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 { pub fn file_read_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms) 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 { pub fn file_delete_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms) 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 { pub fn dir_timeout(&self) -> Duration {
Duration::from_millis(self.dir_operation_ms) 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 { pub fn lock_timeout(&self) -> Duration {
Duration::from_millis(self.lock_acquisition_ms) 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 { pub fn network_timeout(&self) -> Duration {
Duration::from_millis(self.network_operation_ms) Duration::from_millis(self.network_operation_ms)
} }
} }
/// Configuración para manejo de recursos grandes /// Configuration for large resource handling
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ResourceConfig { 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, 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, 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, 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, pub max_in_memory_file_size_mb: u64,
} }
@@ -101,7 +101,7 @@ impl Default for ResourceConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
large_file_threshold_mb: 100, // 100 MB 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 chunk_size_bytes: 1024 * 1024, // 1 MB
max_in_memory_file_size_mb: 50, // 50 MB max_in_memory_file_size_mb: 50, // 50 MB
} }
@@ -109,71 +109,71 @@ impl Default for ResourceConfig {
} }
impl 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 { pub fn bytes_to_mb(&self, bytes: u64) -> u64 {
bytes / (1024 * 1024) 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 { pub fn is_large_file(&self, size_bytes: u64) -> bool {
self.bytes_to_mb(size_bytes) >= self.large_file_threshold_mb 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 { 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 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 { pub fn can_load_in_memory(&self, size_bytes: u64) -> bool {
self.bytes_to_mb(size_bytes) <= self.max_in_memory_file_size_mb 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 { pub fn is_large_directory(&self, entry_count: usize) -> bool {
entry_count >= self.large_dir_threshold_entries 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 { 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) { if !self.needs_parallel_processing(size_bytes, config) {
return 1; 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) let chunk_count = (size_bytes as usize + config.parallel_chunk_size_bytes - 1)
/ config.parallel_chunk_size_bytes; / 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) 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 { pub fn calculate_chunk_size(&self, file_size: u64, chunk_count: usize) -> usize {
if chunk_count <= 1 { if chunk_count <= 1 {
return file_size as usize; 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 ((file_size as usize) + chunk_count - 1) / chunk_count
} }
} }
/// Configuración para operaciones concurrentes /// Configuration for concurrent operations
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ConcurrencyConfig { pub struct ConcurrencyConfig {
/// Máximo de tareas de archivo concurrentes /// Maximum concurrent file tasks
pub max_concurrent_files: usize, pub max_concurrent_files: usize,
/// Máximo de tareas de directorio concurrentes /// Maximum concurrent directory tasks
pub max_concurrent_dirs: usize, pub max_concurrent_dirs: usize,
/// Máximo de operaciones de IO concurrentes /// Maximum concurrent IO operations
pub max_concurrent_io: usize, 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, 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, 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, pub parallel_chunk_size_bytes: usize,
} }
@@ -190,16 +190,16 @@ impl Default for ConcurrencyConfig {
} }
} }
/// Configuración de almacenamiento /// Storage configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StorageConfig { pub struct StorageConfig {
/// Directorio raíz para el almacenamiento /// Root directory for storage
pub root_dir: String, pub root_dir: String,
/// Tamaño de chunk para procesamiento de archivos /// Chunk size for file processing
pub chunk_size: usize, pub chunk_size: usize,
/// Umbral para procesamiento paralelo /// Threshold for parallel processing
pub parallel_threshold: usize, 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, pub trash_retention_days: u32,
} }
@@ -209,12 +209,12 @@ impl Default for StorageConfig {
root_dir: "storage".to_string(), root_dir: "storage".to_string(),
chunk_size: 1024 * 1024, // 1 MB chunk_size: 1024 * 1024, // 1 MB
parallel_threshold: 100 * 1024 * 1024, // 100 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)] #[derive(Debug, Clone)]
pub struct DatabaseConfig { pub struct DatabaseConfig {
pub connection_string: String, pub connection_string: String,
@@ -239,7 +239,7 @@ impl Default for DatabaseConfig {
} }
} }
/// Configuración de autenticación /// Authentication configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AuthConfig { pub struct AuthConfig {
pub jwt_secret: String, pub jwt_secret: String,
@@ -256,15 +256,15 @@ impl Default for AuthConfig {
// to set OXICLOUD_JWT_SECRET in production. The from_env() method // to set OXICLOUD_JWT_SECRET in production. The from_env() method
// will validate this and warn/panic if not configured. // will validate this and warn/panic if not configured.
jwt_secret: String::new(), jwt_secret: String::new(),
access_token_expiry_secs: 3600, // 1 hora access_token_expiry_secs: 3600, // 1 hour
refresh_token_expiry_secs: 2592000, // 30 días refresh_token_expiry_secs: 2592000, // 30 days
hash_memory_cost: 65536, // 64MB hash_memory_cost: 65536, // 64MB
hash_time_cost: 3, hash_time_cost: 3,
} }
} }
} }
/// Configuración de OpenID Connect (OIDC) /// OpenID Connect (OIDC) configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OidcConfig { pub struct OidcConfig {
/// Whether OIDC authentication is enabled /// Whether OIDC authentication is enabled
@@ -335,7 +335,7 @@ impl OidcConfig {
} }
} }
/// Configuración de funcionalidades (feature flags) /// Feature configuration (feature flags)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FeaturesConfig { pub struct FeaturesConfig {
pub enable_auth: bool, 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)] #[derive(Debug, Clone)]
pub struct AppConfig { pub struct AppConfig {
/// Ruta del directorio de almacenamiento /// Storage directory path
pub storage_path: PathBuf, pub storage_path: PathBuf,
/// Ruta del directorio de archivos estáticos /// Static files directory path
pub static_path: PathBuf, pub static_path: PathBuf,
/// Puerto del servidor /// Server port
pub server_port: u16, pub server_port: u16,
/// Host del servidor /// Server host
pub server_host: String, pub server_host: String,
/// Configuración de caché /// Cache configuration
pub cache: CacheConfig, pub cache: CacheConfig,
/// Configuración de timeouts /// Timeout configuration
pub timeouts: TimeoutConfig, pub timeouts: TimeoutConfig,
/// Configuración de recursos /// Resource configuration
pub resources: ResourceConfig, pub resources: ResourceConfig,
/// Configuración de concurrencia /// Concurrency configuration
pub concurrency: ConcurrencyConfig, pub concurrency: ConcurrencyConfig,
/// Configuración de almacenamiento /// Storage configuration
pub storage: StorageConfig, pub storage: StorageConfig,
/// Configuración de base de datos /// Database configuration
pub database: DatabaseConfig, pub database: DatabaseConfig,
/// Configuración de autenticación /// Authentication configuration
pub auth: AuthConfig, pub auth: AuthConfig,
/// Configuración de funcionalidades /// Feature configuration
pub features: FeaturesConfig, pub features: FeaturesConfig,
/// Configuración OIDC /// OIDC configuration
pub oidc: OidcConfig, pub oidc: OidcConfig,
} }
@@ -412,7 +412,7 @@ impl AppConfig {
pub fn from_env() -> Self { pub fn from_env() -> Self {
let mut config = Self::default(); 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") { if let Ok(storage_path) = env::var("OXICLOUD_STORAGE_PATH") {
config.storage_path = PathBuf::from(storage_path); config.storage_path = PathBuf::from(storage_path);
} }
@@ -431,7 +431,7 @@ impl AppConfig {
config.server_host = server_host; config.server_host = server_host;
} }
// Configuración de Database // Database configuration
if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") { if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") {
config.database.connection_string = 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") { if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
config.auth.jwt_secret = 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 { pub fn default_config() -> AppConfig {
AppConfig::default() AppConfig::default()
} }
+53 -53
View File
@@ -53,10 +53,10 @@ use crate::common::stubs::{
StubSearchUseCase, 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, /// This factory centralizes the creation of all application services,
/// garantizando el orden correcto de inicialización y resolviendo dependencias circulares. /// ensuring the correct initialization order and resolving circular dependencies.
pub struct AppServiceFactory { pub struct AppServiceFactory {
storage_path: PathBuf, storage_path: PathBuf,
locales_path: PathBuf, locales_path: PathBuf,
@@ -64,7 +64,7 @@ pub struct AppServiceFactory {
} }
impl 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 { pub fn new(storage_path: PathBuf, locales_path: PathBuf) -> Self {
Self { Self {
storage_path, 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 { pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self {
Self { Self {
storage_path, storage_path,
@@ -82,17 +82,17 @@ impl AppServiceFactory {
} }
} }
/// Obtiene la configuración /// Gets the configuration
pub fn config(&self) -> &AppConfig { pub fn config(&self) -> &AppConfig {
&self.config &self.config
} }
/// Obtiene la ruta de almacenamiento /// Gets the storage path
pub fn storage_path(&self) -> &PathBuf { pub fn storage_path(&self) -> &PathBuf {
&self.storage_path &self.storage_path
} }
/// Inicializa los servicios base del sistema /// Initializes the core system services
pub async fn create_core_services(&self) -> Result<CoreServices, DomainError> { pub async fn create_core_services(&self) -> Result<CoreServices, DomainError> {
// Path service // Path service
let path_service = Arc::new(PathService::new(self.storage_path.clone())); 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"); 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_path = self.storage_path.join("folder_ids.json");
let folder_id_mapping_service = Arc::new( let folder_id_mapping_service = Arc::new(
IdMappingService::new(folder_id_mapping_path).await? 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_path = self.storage_path.join("file_ids.json");
let file_id_mapping_service = Arc::new( let file_id_mapping_service = Arc::new(
IdMappingService::new(file_id_mapping_path).await? 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( let id_mapping_optimizer = Arc::new(
IdMappingOptimizer::new(folder_id_mapping_service.clone()) 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()); 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( let thumbnail_service = Arc::new(
crate::infrastructure::services::thumbnail_service::ThumbnailService::new( crate::infrastructure::services::thumbnail_service::ThumbnailService::new(
&self.storage_path, &self.storage_path,
5000, // max 5000 thumbnails en cache 5000, // max 5000 thumbnails in cache
100 * 1024 * 1024, // max 100MB de cache 100 * 1024 * 1024, // max 100MB cache
) )
); );
// Inicializar directorios de thumbnails // Initialize thumbnail directories
thumbnail_service.initialize().await?; 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(); 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_temp_dir = std::path::PathBuf::from(&self.storage_path).join(".uploads");
let chunked_upload_service = Arc::new( let chunked_upload_service = Arc::new(
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(chunked_temp_dir) 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( let image_transcode_service = Arc::new(
crate::infrastructure::services::image_transcode_service::ImageTranscodeService::new( crate::infrastructure::services::image_transcode_service::ImageTranscodeService::new(
&self.storage_path, &self.storage_path,
2000, // max 2000 imágenes transcodificadas en cache 2000, // max 2000 transcoded images in cache
50 * 1024 * 1024, // max 50MB de cache en memoria 50 * 1024 * 1024, // max 50MB in-memory cache
) )
); );
image_transcode_service.initialize().await?; image_transcode_service.initialize().await?;
// Deduplication service para eliminar archivos duplicados // Deduplication service for removing duplicate files
let dedup_service = Arc::new( let dedup_service = Arc::new(
crate::infrastructure::services::dedup_service::DedupService::new(&self.storage_path) 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 { pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices {
// Storage mediator - uses stub initially, will be replaced after folder repo is ready // Storage mediator - uses stub initially, will be replaced after folder repo is ready
let storage_mediator_stub: Arc<dyn StorageMediator> = Arc::new( let storage_mediator_stub: Arc<dyn StorageMediator> = Arc::new(
@@ -216,13 +216,13 @@ impl AppServiceFactory {
FileMetadataCache::default_with_config(core.config.clone()) 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(); let cache_clone = metadata_cache.clone();
tokio::spawn(async move { tokio::spawn(async move {
FileMetadataCache::start_cleanup_task(cache_clone).await; 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 let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL
BufferPool::start_cleaner(buffer_pool.clone()); BufferPool::start_cleaner(buffer_pool.clone());
@@ -232,7 +232,7 @@ impl AppServiceFactory {
buffer_pool.clone() 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( let file_read_repository = Arc::new(FileFsReadRepository::new(
self.storage_path.clone(), self.storage_path.clone(),
storage_mediator.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( pub fn create_application_services(
&self, &self,
core: &CoreServices, core: &CoreServices,
repos: &RepositoryServices, repos: &RepositoryServices,
trash_service: Option<Arc<dyn TrashUseCase>>, trash_service: Option<Arc<dyn TrashUseCase>>,
) -> ApplicationServices { ) -> ApplicationServices {
// Servicios principales // Main services
let folder_service = Arc::new(FolderService::new( let folder_service = Arc::new(FolderService::new(
repos.folder_repository.clone() 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( let file_upload_service = Arc::new(FileUploadService::new_full(
repos.file_write_repository.clone(), repos.file_write_repository.clone(),
repos.file_read_repository.clone(), repos.file_read_repository.clone(),
@@ -308,7 +308,7 @@ impl AppServiceFactory {
core.image_transcode_service.clone(), core.image_transcode_service.clone(),
)); ));
// FileManagementService con dedup y trash // FileManagementService with dedup and trash
let file_management_service = Arc::new(FileManagementService::new_full( let file_management_service = Arc::new(FileManagementService::new_full(
repos.file_write_repository.clone(), repos.file_write_repository.clone(),
repos.file_read_repository.clone(), repos.file_read_repository.clone(),
@@ -325,7 +325,7 @@ impl AppServiceFactory {
repos.i18n_repository.clone() repos.i18n_repository.clone()
)); ));
// Search service con caché // Search service with cache
let search_service: Option<Arc<dyn SearchUseCase>> = Some(Arc::new(SearchService::new( let search_service: Option<Arc<dyn SearchUseCase>> = Some(Arc::new(SearchService::new(
repos.file_read_repository.clone(), repos.file_read_repository.clone(),
repos.folder_repository.clone(), repos.folder_repository.clone(),
@@ -336,9 +336,9 @@ impl AppServiceFactory {
tracing::info!("Application services initialized"); tracing::info!("Application services initialized");
ApplicationServices { ApplicationServices {
// Tipos concretos para handlers que los necesitan // Concrete types for handlers that need them
folder_service_concrete: folder_service.clone(), folder_service_concrete: folder_service.clone(),
// Traits para abstracción // Traits for abstraction
folder_service, folder_service,
file_upload_service, file_upload_service,
file_retrieval_service, file_retrieval_service,
@@ -347,13 +347,13 @@ impl AppServiceFactory {
i18n_service, i18n_service,
trash_service, // Already set via parameter trash_service, // Already set via parameter
search_service, search_service,
share_service: None, // Se configura después con create_share_service share_service: None, // Configured later with create_share_service
favorites_service: None, // Se configura después con create_favorites_service favorites_service: None, // Configured later with create_favorites_service
recent_service: None, // Se configura después con create_recent_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( pub async fn create_trash_service(
&self, &self,
repos: &RepositoryServices, repos: &RepositoryServices,
@@ -374,7 +374,7 @@ impl AppServiceFactory {
self.config.storage.trash_retention_days, self.config.storage.trash_retention_days,
)); ));
// Inicializar servicio de limpieza // Initialize cleanup service
let cleanup_service = TrashCleanupService::new( let cleanup_service = TrashCleanupService::new(
service.clone(), service.clone(),
trash_repo.clone(), trash_repo.clone(),
@@ -387,7 +387,7 @@ impl AppServiceFactory {
Some(service as Arc<dyn TrashUseCase>) Some(service as Arc<dyn TrashUseCase>)
} }
/// Crea el servicio de compartición /// Creates the sharing service
pub fn create_share_service( pub fn create_share_service(
&self, &self,
repos: &RepositoryServices, repos: &RepositoryServices,
@@ -417,7 +417,7 @@ impl AppServiceFactory {
Some(service) Some(service)
} }
/// Crea el servicio de favoritos (requiere base de datos) /// Creates the favorites service (requires database)
pub fn create_favorites_service( pub fn create_favorites_service(
&self, &self,
db_pool: &Arc<PgPool>, db_pool: &Arc<PgPool>,
@@ -430,7 +430,7 @@ impl AppServiceFactory {
service service
} }
/// Crea el servicio de elementos recientes (requiere base de datos) /// Creates the recent items service (requires database)
pub fn create_recent_service( pub fn create_recent_service(
&self, &self,
db_pool: &Arc<PgPool>, db_pool: &Arc<PgPool>,
@@ -446,7 +446,7 @@ impl AppServiceFactory {
service service
} }
/// Precarga traducciones /// Preloads translations
pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) { pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) {
use crate::domain::services::i18n_service::Locale; use crate::domain::services::i18n_service::Locale;
@@ -468,7 +468,7 @@ impl AppServiceFactory {
tracing::info!("Translations preloaded"); tracing::info!("Translations preloaded");
} }
/// Precarga directorios en caché /// Preloads directories into cache
pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) { pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) {
tracing::info!("Preloading common directories to warm up cache..."); tracing::info!("Preloading common directories to warm up cache...");
if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await { 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( pub fn create_storage_usage_service(
&self, &self,
repos: &RepositoryServices, repos: &RepositoryServices,
@@ -495,9 +495,9 @@ impl AppServiceFactory {
service 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( pub async fn build_app_state(
&self, &self,
db_pool: Option<Arc<PgPool>>, db_pool: Option<Arc<PgPool>>,
@@ -677,7 +677,7 @@ impl AppServiceFactory {
} }
} }
/// Contenedor para servicios base /// Container for core services
#[derive(Clone)] #[derive(Clone)]
pub struct CoreServices { pub struct CoreServices {
pub path_service: Arc<PathService>, pub path_service: Arc<PathService>,
@@ -695,7 +695,7 @@ pub struct CoreServices {
pub config: AppConfig, pub config: AppConfig,
} }
/// Contenedor para servicios de repositorio /// Container for repository services
#[derive(Clone)] #[derive(Clone)]
pub struct RepositoryServices { pub struct RepositoryServices {
pub folder_repository: Arc<dyn FolderStoragePort>, pub folder_repository: Arc<dyn FolderStoragePort>,
@@ -707,12 +707,12 @@ pub struct RepositoryServices {
pub trash_repository: Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>, pub trash_repository: Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>,
} }
/// Contenedor para servicios de aplicación /// Container for application services
#[derive(Clone)] #[derive(Clone)]
pub struct ApplicationServices { pub struct ApplicationServices {
// Tipos concretos para compatibilidad con handlers existentes // Concrete types for compatibility with existing handlers
pub folder_service_concrete: Arc<FolderService>, pub folder_service_concrete: Arc<FolderService>,
// Traits para abstracción // Traits for abstraction
pub folder_service: Arc<dyn FolderUseCase>, pub folder_service: Arc<dyn FolderUseCase>,
pub file_upload_service: Arc<dyn FileUploadUseCase>, pub file_upload_service: Arc<dyn FileUploadUseCase>,
pub file_retrieval_service: Arc<dyn FileRetrievalUseCase>, pub file_retrieval_service: Arc<dyn FileRetrievalUseCase>,
@@ -726,14 +726,14 @@ pub struct ApplicationServices {
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>, pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
} }
/// Contenedor para servicios de autenticación /// Container for authentication services
#[derive(Clone)] #[derive(Clone)]
pub struct AuthServices { pub struct AuthServices {
pub token_service: Arc<dyn crate::application::ports::auth_ports::TokenServicePort>, pub token_service: Arc<dyn crate::application::ports::auth_ports::TokenServicePort>,
pub auth_application_service: Arc<AuthApplicationService>, pub auth_application_service: Arc<AuthApplicationService>,
} }
/// Estado global de la aplicación para dependency injection /// Global application state for dependency injection
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub core: CoreServices, pub core: CoreServices,
+14 -14
View File
@@ -1,21 +1,21 @@
//! Errores de la aplicación //! Application errors
//! //!
//! Este módulo re-exporta los errores del dominio para compatibilidad. //! This module re-exports domain errors for compatibility.
//! Las conversiones de errores de infraestructura (sqlx, serde_json, etc.) //! Infrastructure error conversions (sqlx, serde_json, etc.)
//! se encuentran en infrastructure/adapters/error_adapters.rs, siguiendo //! are located in infrastructure/adapters/error_adapters.rs, following
//! los principios de Clean Architecture donde el dominio no debe conocer //! Clean Architecture principles where the domain should not know about
//! detalles de infraestructura. //! infrastructure details.
// Re-exportar errores del dominio para compatibilidad // Re-export domain errors for compatibility
pub use crate::domain::errors::{DomainError, ErrorKind, Result}; pub use crate::domain::errors::{DomainError, ErrorKind, Result};
// Re-exportar AppError desde interfaces para compatibilidad hacia atrás // Re-export AppError from interfaces for backward compatibility
// NOTA: El lugar canónico de AppError es ahora crate::interfaces::errors // 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 // crate::infrastructure::adapters::error_adapters
// //
// Para convertir errores de infraestructura a DomainError, use: // To convert infrastructure errors to DomainError, use:
// - El trait IntoDomainError para conversiones explícitas con contexto // - The IntoDomainError trait for explicit conversions with context
// - O maneje los errores en los repositorios/servicios de infraestructura // - Or handle errors in infrastructure repositories/services
// usando map_err() con DomainError::internal_error() o métodos similares // using map_err() with DomainError::internal_error() or similar methods
+1 -1
View File
@@ -14,7 +14,7 @@ use chrono::{DateTime, Utc};
use crate::common::errors::{Result, DomainError, ErrorKind}; 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; pub use super::entity_errors::CalendarError;
/** /**
+1 -1
View File
@@ -14,7 +14,7 @@ use chrono::{DateTime, Utc, Duration, TimeZone};
use crate::common::errors::{Result, DomainError, ErrorKind}; 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; pub use super::entity_errors::CalendarEventError;
/** /**
+42 -42
View File
@@ -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 //! This module defines domain entity-specific errors
//! sin dependencias de frameworks externos, siguiendo los principios de //! without external framework dependencies, following
//! Clean Architecture. //! Clean Architecture principles.
//! //!
//! Los errores implementan manualmente `std::error::Error` y `std::fmt::Display` //! Errors manually implement `std::error::Error` and `std::fmt::Display`
//! para mantener el dominio libre de dependencias externas. //! to keep the domain free of external dependencies.
use std::error::Error; use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fmt::{Display, Formatter, Result as FmtResult};
@@ -14,12 +14,12 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
// FILE ERRORS // FILE ERRORS
// ============================================================================ // ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades File /// Errors that can occur during File entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileError { 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), InvalidFileName(String),
/// Ocurre cuando falla la validación de cualquier atributo de la entidad /// Occurs when validation of any entity attribute fails
ValidationError(String), ValidationError(String),
} }
@@ -34,19 +34,19 @@ impl Display for FileError {
impl Error 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<T> = Result<T, FileError>; pub type FileResult<T> = Result<T, FileError>;
// ============================================================================ // ============================================================================
// FOLDER ERRORS // FOLDER ERRORS
// ============================================================================ // ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades Folder /// Errors that can occur during Folder entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum FolderError { 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), InvalidFolderName(String),
/// Ocurre cuando falla la validación de cualquier atributo de la entidad /// Occurs when validation of any entity attribute fails
ValidationError(String), ValidationError(String),
} }
@@ -61,54 +61,54 @@ impl Display for FolderError {
impl Error 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<T> = Result<T, FolderError>; pub type FolderResult<T> = Result<T, FolderError>;
// ============================================================================ // ============================================================================
// USER ERRORS // USER ERRORS
// ============================================================================ // ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades User /// Errors that can occur during User entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserError { pub enum UserError {
/// Nombre de usuario inválido /// Invalid username
InvalidUsername(String), InvalidUsername(String),
/// Contraseña inválida /// Invalid password
InvalidPassword(String), InvalidPassword(String),
/// Error de validación general /// General validation error
ValidationError(String), ValidationError(String),
/// Error de autenticación /// Authentication error
AuthenticationError(String), AuthenticationError(String),
} }
impl Display for UserError { impl Display for UserError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
UserError::InvalidUsername(msg) => write!(f, "Username inválido: {}", msg), UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg),
UserError::InvalidPassword(msg) => write!(f, "Password inválido: {}", msg), UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg),
UserError::ValidationError(msg) => write!(f, "Error en la validación: {}", msg), UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
UserError::AuthenticationError(msg) => write!(f, "Error en la autenticación: {}", msg), UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
} }
} }
} }
impl Error for UserError {} impl Error for UserError {}
/// Alias de tipo para resultados de operaciones con entidades User /// Type alias for User entity operation results
pub type UserResult<T> = Result<T, UserError>; pub type UserResult<T> = Result<T, UserError>;
// ============================================================================ // ============================================================================
// SHARE ERRORS // SHARE ERRORS
// ============================================================================ // ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades Share /// Errors that can occur during Share entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShareError { pub enum ShareError {
/// Token de compartición inválido /// Invalid share token
InvalidToken(String), InvalidToken(String),
/// Fecha de expiración inválida /// Invalid expiration date
InvalidExpiration(String), InvalidExpiration(String),
/// Error de validación general /// General validation error
ValidationError(String), ValidationError(String),
} }
@@ -124,21 +124,21 @@ impl Display for ShareError {
impl Error 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<T> = Result<T, ShareError>; pub type ShareResult<T> = Result<T, ShareError>;
// ============================================================================ // ============================================================================
// CALENDAR ERRORS // CALENDAR ERRORS
// ============================================================================ // ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades Calendar /// Errors that can occur during Calendar entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarError { pub enum CalendarError {
/// Nombre de calendario inválido /// Invalid calendar name
InvalidName(String), InvalidName(String),
/// Código de color inválido /// Invalid color code
InvalidColor(String), InvalidColor(String),
/// ID de propietario inválido /// Invalid owner ID
InvalidOwnerId(String), InvalidOwnerId(String),
} }
@@ -154,23 +154,23 @@ impl Display for CalendarError {
impl Error 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<T> = Result<T, CalendarError>; pub type CalendarResult<T> = Result<T, CalendarError>;
// ============================================================================ // ============================================================================
// CALENDAR EVENT ERRORS // 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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarEventError { pub enum CalendarEventError {
/// Resumen/título de evento inválido /// Invalid event summary/title
InvalidSummary(String), InvalidSummary(String),
/// Fechas de evento inválidas /// Invalid event dates
InvalidDates(String), InvalidDates(String),
/// Regla de recurrencia inválida /// Invalid recurrence rule
InvalidRecurrence(String), InvalidRecurrence(String),
/// Datos iCalendar inválidos /// Invalid iCalendar data
InvalidICalData(String), InvalidICalData(String),
} }
@@ -187,7 +187,7 @@ impl Display for CalendarEventError {
impl Error 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<T> = Result<T, CalendarEventError>; pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
// ============================================================================ // ============================================================================
@@ -216,10 +216,10 @@ mod tests {
#[test] #[test]
fn test_user_error_display() { fn test_user_error_display() {
let err = UserError::InvalidUsername("".to_string()); 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()); 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] #[test]
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::domain::services::path_service::StoragePath; 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}; pub use super::entity_errors::{FileError, FileResult};
/** /**
@@ -366,6 +366,6 @@ mod tests {
assert!(renamed.is_ok()); assert!(renamed.is_ok());
let renamed = renamed.unwrap(); let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "newname.txt"); 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
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::domain::services::path_service::StoragePath; 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}; pub use super::entity_errors::{FolderError, FolderResult};
/// Represents a folder entity in the domain /// Represents a folder entity in the domain
+1 -1
View File
@@ -1,7 +1,7 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid; 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; pub use super::entity_errors::ShareError;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
+12 -12
View File
@@ -1,7 +1,7 @@
use uuid::Uuid; use uuid::Uuid;
use chrono::{DateTime, Utc}; 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}; pub use super::entity_errors::{UserError, UserResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -57,22 +57,22 @@ impl User {
role: UserRole, role: UserRole,
storage_quota_bytes: i64, storage_quota_bytes: i64,
) -> UserResult<Self> { ) -> UserResult<Self> {
// Validaciones // Validations
if username.is_empty() || username.len() < 3 || username.len() > 32 { if username.is_empty() || username.len() < 3 || username.len() > 32 {
return Err(UserError::InvalidUsername(format!( 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 { if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError(format!( return Err(UserError::ValidationError(format!(
"Email inválido" "Invalid email"
))); )));
} }
if password_hash.is_empty() { if password_hash.is_empty() {
return Err(UserError::InvalidPassword(format!( return Err(UserError::InvalidPassword(format!(
"Password hash no puede estar vacío" "Password hash cannot be empty"
))); )));
} }
@@ -106,12 +106,12 @@ impl User {
) -> UserResult<Self> { ) -> UserResult<Self> {
if username.is_empty() || username.len() < 3 || username.len() > 32 { if username.is_empty() || username.len() < 3 || username.len() > 32 {
return Err(UserError::InvalidUsername( 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 { if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError( return Err(UserError::ValidationError(
"Email inválido".to_string(), "Invalid email".to_string(),
)); ));
} }
let now = Utc::now(); 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( pub fn from_data(
id: String, id: String,
username: String, username: String,
@@ -263,26 +263,26 @@ impl User {
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
// Actualizar uso de almacenamiento // Update storage usage
pub fn update_storage_used(&mut self, storage_used_bytes: i64) { pub fn update_storage_used(&mut self, storage_used_bytes: i64) {
self.storage_used_bytes = storage_used_bytes; self.storage_used_bytes = storage_used_bytes;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
// Registrar login // Register login
pub fn register_login(&mut self) { pub fn register_login(&mut self) {
let now = Utc::now(); let now = Utc::now();
self.last_login_at = Some(now); self.last_login_at = Some(now);
self.updated_at = now; self.updated_at = now;
} }
// Desactivar usuario // Deactivate user
pub fn deactivate(&mut self) { pub fn deactivate(&mut self) {
self.active = false; self.active = false;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
// Activar usuario // Activate user
pub fn activate(&mut self) { pub fn activate(&mut self) {
self.active = true; self.active = true;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
+34 -34
View File
@@ -1,35 +1,35 @@
//! Errores del dominio //! Domain errors
//! //!
//! Este módulo contiene los tipos de error propios del dominio. //! This module contains domain-specific error types.
//! DomainError es el error base que se usa en toda la capa de dominio. //! DomainError is the base error used throughout the domain layer.
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fmt::{Display, Formatter, Result as FmtResult};
use std::error::Error as StdError; use std::error::Error as StdError;
use thiserror::Error; 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<T> = std::result::Result<T, DomainError>; pub type Result<T> = std::result::Result<T, DomainError>;
/// Tipos de errores del dominio /// Domain error types
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind { pub enum ErrorKind {
/// Entidad no encontrada /// Entity not found
NotFound, NotFound,
/// Entidad ya existe /// Entity already exists
AlreadyExists, AlreadyExists,
/// Entrada inválida o validación fallida /// Invalid input or failed validation
InvalidInput, InvalidInput,
/// Error de acceso o permisos /// Access or permissions error
AccessDenied, AccessDenied,
/// Tiempo de espera agotado /// Timeout expired
Timeout, Timeout,
/// Error interno del sistema /// Internal system error
InternalError, InternalError,
/// Funcionalidad no implementada /// Functionality not implemented
NotImplemented, NotImplemented,
/// Operación no soportada /// Unsupported operation
UnsupportedOperation, UnsupportedOperation,
/// Error de base de datos /// Database error
DatabaseError, 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)] #[derive(Error, Debug)]
#[error("{kind}: {message}")] #[error("{kind}: {message}")]
pub struct DomainError { pub struct DomainError {
/// Tipo de error /// Error type
pub kind: ErrorKind, pub kind: ErrorKind,
/// Tipo de entidad afectada (ej: "File", "Folder") /// Affected entity type (e.g.: "File", "Folder")
pub entity_type: &'static str, pub entity_type: &'static str,
/// Identificador de la entidad si está disponible /// Entity identifier if available
pub entity_id: Option<String>, pub entity_id: Option<String>,
/// Mensaje descriptivo del error /// Descriptive error message
pub message: String, pub message: String,
/// Error fuente (opcional) /// Source error (optional)
#[source] #[source]
pub source: Option<Box<dyn StdError + Send + Sync>>, pub source: Option<Box<dyn StdError + Send + Sync>>,
} }
impl DomainError { impl DomainError {
/// Crea un nuevo error de dominio /// Creates a new domain error
pub fn new<S: Into<String>>( pub fn new<S: Into<String>>(
kind: ErrorKind, kind: ErrorKind,
entity_type: &'static str, 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<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self { pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into(); let id = entity_id.into();
Self { Self {
@@ -94,7 +94,7 @@ impl DomainError {
} }
} }
/// Crea un error de entidad ya existente /// Creates an entity already exists error
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self { pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into(); let id = entity_id.into();
Self { 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<S: Into<String>>(entity_type: &'static str, message: S) -> Self { pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self::new( Self::new(
ErrorKind::UnsupportedOperation, ErrorKind::UnsupportedOperation,
@@ -115,7 +115,7 @@ impl DomainError {
) )
} }
/// Crea un error de tiempo agotado /// Creates a timeout error
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self { pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self { Self {
kind: ErrorKind::Timeout, kind: ErrorKind::Timeout,
@@ -126,7 +126,7 @@ impl DomainError {
} }
} }
/// Crea un error interno /// Creates an internal error
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self { pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self { Self {
kind: ErrorKind::InternalError, kind: ErrorKind::InternalError,
@@ -137,7 +137,7 @@ impl DomainError {
} }
} }
/// Crea un error de acceso denegado /// Creates an access denied error
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self { pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self { Self {
kind: ErrorKind::AccessDenied, kind: ErrorKind::AccessDenied,
@@ -159,7 +159,7 @@ impl DomainError {
} }
} }
/// Crea un error de base de datos /// Creates a database error
pub fn database_error<S: Into<String>>(message: S) -> Self { pub fn database_error<S: Into<String>>(message: S) -> Self {
Self { Self {
kind: ErrorKind::DatabaseError, kind: ErrorKind::DatabaseError,
@@ -170,7 +170,7 @@ impl DomainError {
} }
} }
/// Crea un error de validación /// Creates a validation error
pub fn validation_error<S: Into<String>>(message: S) -> Self { pub fn validation_error<S: Into<String>>(message: S) -> Self {
Self { Self {
kind: ErrorKind::InvalidInput, 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<S: Into<String>>(entity_type: &'static str, message: S) -> Self { pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self { Self {
kind: ErrorKind::NotImplemented, kind: ErrorKind::NotImplemented,
@@ -192,20 +192,20 @@ impl DomainError {
} }
} }
/// Establece el ID de la entidad /// Sets the entity ID
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self { pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
self.entity_id = Some(entity_id.into()); self.entity_id = Some(entity_id.into());
self self
} }
/// Establece el error fuente /// Sets the source error
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self { pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
self.source = Some(Box::new(source)); self.source = Some(Box::new(source));
self self
} }
} }
/// Trait para añadir contexto a los errores /// Trait for adding context to errors
pub trait ErrorContext<T, E> { pub trait ErrorContext<T, E> {
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError> fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
where where
@@ -245,7 +245,7 @@ impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> 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<std::io::Error> for DomainError { impl From<std::io::Error> for DomainError {
fn from(err: std::io::Error) -> Self { fn from(err: std::io::Error) -> Self {
DomainError { DomainError {
+42 -42
View File
@@ -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 //! Defines the contract that any file storage implementation must fulfill.
//! debe cumplir. Este trait vive en el dominio porque File es una entidad core //! This trait lives in the domain because File is a core entity of the system
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio, //! and its persistence contracts belong to the domain layer, following
//! siguiendo los principios de Clean/Hexagonal Architecture. //! Clean/Hexagonal Architecture principles.
//! //!
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en //! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in
//! la capa de infraestructura. //! the infrastructure layer.
use std::path::PathBuf; use std::path::PathBuf;
use std::pin::Pin; use std::pin::Pin;
@@ -20,31 +20,31 @@ use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError; 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: /// Encapsulates every operation that queries state without modifying it:
/// obtener, listar, contenido, stream, mmap, rango, resolución de rutas. /// get, list, content, stream, mmap, range, path resolution.
#[async_trait] #[async_trait]
pub trait FileReadRepository: Send + Sync + 'static { 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<File, DomainError>; async fn get_file(&self, id: &str) -> Result<File, DomainError>;
/// Lista archivos en una carpeta. /// Lists files in a folder.
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>; async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, 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<Vec<u8>, DomainError>; async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
/// Obtiene contenido como stream (ideal para archivos grandes). /// Gets content as a stream (ideal for large files).
async fn get_file_stream( async fn get_file_stream(
&self, &self,
id: &str, id: &str,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>; ) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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( async fn get_file_range_stream(
&self, &self,
id: &str, id: &str,
@@ -52,27 +52,27 @@ pub trait FileReadRepository: Send + Sync + 'static {
end: Option<u64>, end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>; ) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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<Bytes, DomainError>; async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
/// 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<StoragePath, DomainError>; async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
/// 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<String, DomainError>; async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
} }
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
// 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, /// Covers: upload (buffered + streaming), move, delete, update,
/// y el registro diferido para write-behind cache. /// and deferred registration for write-behind cache.
#[async_trait] #[async_trait]
pub trait FileWriteRepository: Send + Sync + 'static { pub trait FileWriteRepository: Send + Sync + 'static {
/// Guarda un nuevo archivo desde bytes. /// Saves a new file from bytes.
async fn save_file( async fn save_file(
&self, &self,
name: String, name: String,
@@ -81,7 +81,7 @@ pub trait FileWriteRepository: Send + Sync + 'static {
content: Vec<u8>, content: Vec<u8>,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// 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( async fn save_file_from_stream(
&self, &self,
name: String, name: String,
@@ -90,30 +90,30 @@ pub trait FileWriteRepository: Send + Sync + 'static {
stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>, stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Mueve un archivo a otra carpeta. /// Moves a file to another folder.
async fn move_file( async fn move_file(
&self, &self,
file_id: &str, file_id: &str,
target_folder_id: Option<String>, target_folder_id: Option<String>,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Renombra un archivo (same folder, different name). /// Renames a file (same folder, different name).
async fn rename_file( async fn rename_file(
&self, &self,
file_id: &str, file_id: &str,
new_name: &str, new_name: &str,
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Elimina un archivo. /// Deletes a file.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>; 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<u8>) -> Result<(), DomainError>; async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> 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 /// Returns `(File, PathBuf)` where `PathBuf` is the destination path for
/// escritura diferida que realizará el `WriteBehindCache`. /// the deferred write that the `WriteBehindCache` will perform.
async fn register_file_deferred( async fn register_file_deferred(
&self, &self,
name: String, name: String,
@@ -124,27 +124,27 @@ pub trait FileWriteRepository: Send + Sync + 'static {
// ── Trash operations ── // ── 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>; 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>; 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>; 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`. /// It is a supertrait of `FileReadRepository + FileWriteRepository`.
/// Cualquier tipo que implemente ambos ports obtiene `FileRepository` /// Any type that implements both ports gets `FileRepository`
/// automáticamente vía blanket impl. /// automatically via blanket impl.
pub trait FileRepository: FileReadRepository + FileWriteRepository {} pub trait FileRepository: FileReadRepository + FileWriteRepository {}
/// Blanket implementation: cualquier tipo que implemente ambos ports /// Blanket implementation: any type that implements both ports
/// es automáticamente un FileRepository. /// is automatically a FileRepository.
impl<T: FileReadRepository + FileWriteRepository> FileRepository for T {} impl<T: FileReadRepository + FileWriteRepository> FileRepository for T {}
+23 -23
View File
@@ -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 //! Defines the contract that any folder storage implementation
//! debe cumplir. Este trait vive en el dominio porque Folder es una entidad core //! must fulfill. This trait lives in the domain because Folder is a core entity
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio, //! of the system and its persistence contracts belong to the domain layer,
//! siguiendo los principios de Clean/Hexagonal Architecture. //! following the principles of Clean/Hexagonal Architecture.
//! //!
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en //! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in
//! la capa de infraestructura. //! the infrastructure layer.
use async_trait::async_trait; use async_trait::async_trait;
@@ -14,25 +14,25 @@ use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError; 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 /// Defines the CRUD and management operations required for
/// la entidad Folder en el sistema de almacenamiento. /// the Folder entity in the storage system.
#[async_trait] #[async_trait]
pub trait FolderRepository: Send + Sync + 'static { pub trait FolderRepository: Send + Sync + 'static {
/// Crea una nueva carpeta /// Creates a new folder
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>; async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
/// Obtiene una carpeta por su ID /// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>; async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
/// 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<Folder, DomainError>; async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
/// Lista carpetas dentro de una carpeta padre /// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>; async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
/// Lista carpetas con paginación /// Lists folders with pagination
async fn list_folders_paginated( async fn list_folders_paginated(
&self, &self,
parent_id: Option<&str>, parent_id: Option<&str>,
@@ -41,29 +41,29 @@ pub trait FolderRepository: Send + Sync + 'static {
include_total: bool include_total: bool
) -> Result<(Vec<Folder>, Option<usize>), DomainError>; ) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
/// Renombra una carpeta /// Renames a folder
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>; async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
/// 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<Folder, DomainError>; async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>;
/// Elimina una carpeta /// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>; 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<bool, DomainError>; async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
/// Obtiene la ruta de una carpeta /// Gets the path of a folder
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>; async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
// ── Trash operations ── // ── 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>; 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>; 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>; async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
} }
+11 -11
View File
@@ -4,19 +4,19 @@ use crate::common::errors::DomainError;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum SessionRepositoryError { pub enum SessionRepositoryError {
#[error("Sesión no encontrada: {0}")] #[error("Session not found: {0}")]
NotFound(String), NotFound(String),
#[error("Error de base de datos: {0}")] #[error("Database error: {0}")]
DatabaseError(String), DatabaseError(String),
#[error("Error de tiempo de espera: {0}")] #[error("Timeout error: {0}")]
Timeout(String), Timeout(String),
} }
pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>; pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
// Conversión de SessionRepositoryError a DomainError // Conversion from SessionRepositoryError to DomainError
impl From<SessionRepositoryError> for DomainError { impl From<SessionRepositoryError> for DomainError {
fn from(err: SessionRepositoryError) -> Self { fn from(err: SessionRepositoryError) -> Self {
match err { match err {
@@ -35,24 +35,24 @@ impl From<SessionRepositoryError> for DomainError {
#[async_trait] #[async_trait]
pub trait SessionRepository: Send + Sync + 'static { pub trait SessionRepository: Send + Sync + 'static {
/// Crea una nueva sesión /// Creates a new session
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>; async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
/// Obtiene una sesión por ID /// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>; async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
/// 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<Session>; async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session>;
/// 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<Vec<Session>>; async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>>;
/// Revoca una sesión específica /// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>; 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<u64>; async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>;
/// Elimina sesiones expiradas /// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>; async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
} }
+24 -24
View File
@@ -4,28 +4,28 @@ use crate::common::errors::DomainError;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum UserRepositoryError { pub enum UserRepositoryError {
#[error("Usuario no encontrado: {0}")] #[error("User not found: {0}")]
NotFound(String), NotFound(String),
#[error("Usuario ya existe: {0}")] #[error("User already exists: {0}")]
AlreadyExists(String), AlreadyExists(String),
#[error("Error de base de datos: {0}")] #[error("Database error: {0}")]
DatabaseError(String), DatabaseError(String),
#[error("Error de validación: {0}")] #[error("Validation error: {0}")]
ValidationError(String), ValidationError(String),
#[error("Error de tiempo de espera: {0}")] #[error("Timeout error: {0}")]
Timeout(String), Timeout(String),
#[error("Operación no permitida: {0}")] #[error("Operation not allowed: {0}")]
OperationNotAllowed(String), OperationNotAllowed(String),
} }
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>; pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
// Conversión de UserRepositoryError a DomainError // Conversion from UserRepositoryError to DomainError
impl From<UserRepositoryError> for DomainError { impl From<UserRepositoryError> for DomainError {
fn from(err: UserRepositoryError) -> Self { fn from(err: UserRepositoryError) -> Self {
match err { match err {
@@ -53,59 +53,59 @@ impl From<UserRepositoryError> for DomainError {
#[async_trait] #[async_trait]
pub trait UserRepository: Send + Sync + 'static { pub trait UserRepository: Send + Sync + 'static {
/// Crea un nuevo usuario /// Creates a new user
async fn create_user(&self, user: User) -> UserRepositoryResult<User>; async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
/// Obtiene un usuario por ID /// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>; async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>;
/// Obtiene un usuario por nombre de usuario /// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>; async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
/// Obtiene un usuario por correo electrónico /// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>; async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
/// Actualiza un usuario existente /// Updates an existing user
async fn update_user(&self, user: User) -> UserRepositoryResult<User>; async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
/// 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<()>; 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<()>; 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<Vec<User>>; async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>;
/// Activa o desactiva un usuario /// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()>; 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<()>; 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<()>; 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<Vec<User>>; async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
/// Elimina un usuario /// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>; async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
/// Finds a user by OIDC provider + subject pair /// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User>; async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User>;
/// 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<()>; 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<i64>; async fn count_users(&self) -> UserRepositoryResult<i64>;
/// Obtiene estadísticas de almacenamiento agregadas /// Gets aggregated storage statistics
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>; async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>;
} }
/// Estadísticas de almacenamiento agregadas /// Aggregated storage statistics
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StorageStats { pub struct StorageStats {
pub total_users: i64, pub total_users: i64,
+19 -19
View File
@@ -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. //! This module contains only the StoragePath Value Object which is part of the pure domain.
//! PathService (que implementa StoragePort y StorageMediator) fue movido a //! PathService (which implements StoragePort and StorageMediator) was moved to
//! infrastructure/services/path_service.rs porque tiene dependencias de sistema de archivos. //! infrastructure/services/path_service.rs because it has file system dependencies.
use std::path::PathBuf; 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)] #[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StoragePath { pub struct StoragePath {
segments: Vec<String>, segments: Vec<String>,
} }
impl StoragePath { impl StoragePath {
/// Crea una nueva ruta de almacenamiento /// Creates a new storage path
pub fn new(segments: Vec<String>) -> Self { pub fn new(segments: Vec<String>) -> Self {
Self { segments } Self { segments }
} }
/// Crea una ruta vacía (raíz) /// Creates an empty path (root)
pub fn root() -> Self { pub fn root() -> Self {
Self { segments: Vec::new() } 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 { pub fn from_string(path: &str) -> Self {
let segments = path let segments = path
.split('/') .split('/')
@@ -33,7 +33,7 @@ impl StoragePath {
Self { segments } Self { segments }
} }
/// Crea una ruta a partir de un PathBuf /// Creates a path from a PathBuf
pub fn from(path_buf: PathBuf) -> Self { pub fn from(path_buf: PathBuf) -> Self {
let segments = path_buf let segments = path_buf
.components() .components()
@@ -45,19 +45,19 @@ impl StoragePath {
Self { segments } Self { segments }
} }
/// Añade un segmento a la ruta /// Appends a segment to the path
pub fn join(&self, segment: &str) -> Self { pub fn join(&self, segment: &str) -> Self {
let mut new_segments = self.segments.clone(); let mut new_segments = self.segments.clone();
new_segments.push(segment.to_string()); new_segments.push(segment.to_string());
Self { segments: new_segments } Self { segments: new_segments }
} }
/// Obtiene el nombre del archivo (último segmento) /// Gets the file name (last segment)
pub fn file_name(&self) -> Option<String> { pub fn file_name(&self) -> Option<String> {
self.segments.last().cloned() self.segments.last().cloned()
} }
/// Obtiene la ruta del directorio padre /// Gets the parent directory path
pub fn parent(&self) -> Option<Self> { pub fn parent(&self) -> Option<Self> {
if self.segments.is_empty() { if self.segments.is_empty() {
None 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 { pub fn is_empty(&self) -> bool {
self.segments.is_empty() 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 { pub fn to_string(&self) -> String {
if self.segments.is_empty() { if self.segments.is_empty() {
"/".to_string() "/".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 { pub fn as_str(&self) -> &str {
// Nota: La implementación realmente debería almacenar la cadena, // Note: The implementation should really store the string,
// pero aquí hacemos una implementación temporal que siempre devuelve "/" // but here we do a temporary implementation that always returns "/"
// Esto se usa solo para la implementación de get_folder_path_str // 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] { pub fn segments(&self) -> &[String] {
&self.segments &self.segments
} }
+6 -6
View File
@@ -17,21 +17,21 @@ pub async fn create_auth_services(
pool: Arc<PgPool>, pool: Arc<PgPool>,
folder_service: Option<Arc<FolderService>> folder_service: Option<Arc<FolderService>>
) -> Result<AuthServices> { ) -> Result<AuthServices> {
// Crear servicio de tokens JWT (implementación de TokenServicePort) // Create JWT token service (TokenServicePort implementation)
let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new( let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new(
config.auth.jwt_secret.clone(), config.auth.jwt_secret.clone(),
config.auth.access_token_expiry_secs, config.auth.access_token_expiry_secs,
config.auth.refresh_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()); let password_hasher = Arc::new(Argon2PasswordHasher::new());
// Crear repositorios PostgreSQL // Create PostgreSQL repositories
let user_repository = Arc::new(UserPgRepository::new(pool.clone())); let user_repository = Arc::new(UserPgRepository::new(pool.clone()));
let session_repository = Arc::new(SessionPgRepository::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( let mut auth_app_service = AuthApplicationService::new(
user_repository, user_repository,
session_repository, session_repository,
@@ -39,7 +39,7 @@ pub async fn create_auth_services(
token_service.clone(), token_service.clone(),
); );
// Configurar servicio de carpetas si está disponible // Configure folder service if available
if let Some(folder_svc) = folder_service { if let Some(folder_svc) = folder_service {
auth_app_service = auth_app_service.with_folder_service(folder_svc); 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); let auth_application_service = Arc::new(auth_app_service);
Ok(AuthServices { Ok(AuthServices {
+13 -13
View File
@@ -4,7 +4,7 @@ use std::time::Duration;
use crate::common::config::AppConfig; use crate::common::config::AppConfig;
pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> { pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
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]@")); config.database.connection_string.replace("postgres://", "postgres://[user]:[pass]@"));
// Add a more robust connection attempt with retries // Add a more robust connection attempt with retries
@@ -13,9 +13,9 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
while attempt < MAX_ATTEMPTS { while attempt < MAX_ATTEMPTS {
attempt += 1; 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() match PgPoolOptions::new()
.max_connections(config.database.max_connections) .max_connections(config.database.max_connections)
.min_connections(config.database.min_connections) .min_connections(config.database.min_connections)
@@ -25,10 +25,10 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
.connect(&config.database.connection_string) .connect(&config.database.connection_string)
.await { .await {
Ok(pool) => { Ok(pool) => {
// Verificar la conexión // Verify the connection
match sqlx::query("SELECT 1").execute(&pool).await { match sqlx::query("SELECT 1").execute(&pool).await {
Ok(_) => { Ok(_) => {
tracing::info!("Conexión a PostgreSQL establecida correctamente"); tracing::info!("PostgreSQL connection established successfully");
// Verify if migrations have been applied // Verify if migrations have been applied
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')") 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<PgPool> {
Ok(row) => { Ok(row) => {
let tables_exist: bool = row.get(0); let tables_exist: bool = row.get(0);
if !tables_exist { 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(_) => { 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); return Ok(pool);
}, },
Err(e) => { Err(e) => {
tracing::error!("Error al verificar conexión: {}", e); tracing::error!("Error verifying connection: {}", e);
tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations"); tracing::warn!("The database appears to not be configured. Please run migrations with: cargo run --bin migrate --features migrations");
if attempt >= MAX_ATTEMPTS { 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) => { Err(e) => {
tracing::error!("Error al conectar a PostgreSQL: {}", e); tracing::error!("Error connecting to PostgreSQL: {}", e);
if attempt >= MAX_ATTEMPTS { 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; 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))
} }
@@ -10,11 +10,11 @@ use crate::common::errors::DomainError;
use crate::domain::entities::file::File; use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
/// Composite que envuelve `Arc<dyn FileReadPort>` + `Arc<dyn FileWritePort>` /// Composite that wraps `Arc<dyn FileReadPort>` + `Arc<dyn FileWritePort>`
/// y delega cada método al port correspondiente. /// and delegates each method to the corresponding port.
/// ///
/// Gracias al blanket impl `impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}` /// Thanks to the blanket impl `impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}`
/// este tipo obtiene `FileStoragePort` automáticamente. /// this type automatically gets `FileStoragePort`.
pub struct CompositeFileRepository { pub struct CompositeFileRepository {
read: Arc<dyn FileReadPort>, read: Arc<dyn FileReadPort>,
write: Arc<dyn FileWritePort>, write: Arc<dyn FileWritePort>,
@@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::common::config::AppConfig; 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, list_files, get_file_content, get_file_stream,
/// get_file_range_stream, get_file_mmap, get_file_path, get_parent_folder_id. /// get_file_range_stream, get_file_mmap, get_file_path, get_parent_folder_id.
pub struct FileFsReadRepository { pub struct FileFsReadRepository {
@@ -37,7 +37,7 @@ pub struct FileFsReadRepository {
} }
impl FileFsReadRepository { impl FileFsReadRepository {
/// Constructor completo con todas las dependencias de infraestructura. /// Full constructor with all infrastructure dependencies.
pub fn new( pub fn new(
root_path: PathBuf, root_path: PathBuf,
storage_mediator: Arc<dyn StorageMediator>, storage_mediator: Arc<dyn StorageMediator>,
@@ -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 { pub fn default_stub() -> Self {
Self { Self {
root_path: PathBuf::from("./storage"), root_path: PathBuf::from("./storage"),
@@ -75,7 +75,7 @@ impl FileFsReadRepository {
} }
} }
// ─── helpers internos ──────────────────────────────────── // ─── internal helpers ─────────────────────────────────────
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf { fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
self.path_service.resolve_path(storage_path) self.path_service.resolve_path(storage_path)
@@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::common::config::AppConfig; 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, /// save_file, save_file_from_stream, move_file, delete_file,
/// update_file_content, register_file_deferred. /// update_file_content, register_file_deferred.
pub struct FileFsWriteRepository { pub struct FileFsWriteRepository {
@@ -37,7 +37,7 @@ pub struct FileFsWriteRepository {
} }
impl FileFsWriteRepository { impl FileFsWriteRepository {
/// Constructor completo con todas las dependencias. /// Full constructor with all dependencies.
pub fn new( pub fn new(
root_path: PathBuf, root_path: PathBuf,
storage_mediator: Arc<dyn StorageMediator>, storage_mediator: Arc<dyn StorageMediator>,
@@ -50,7 +50,7 @@ impl FileFsWriteRepository {
Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor } 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 { pub fn default_stub() -> Self {
Self { Self {
root_path: PathBuf::from("./storage"), root_path: PathBuf::from("./storage"),
@@ -5,147 +5,147 @@ use tracing::{debug, error};
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult; use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository; use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
// Este archivo contiene la implementación de los métodos relacionados con la papelera // This file contains the implementation of trash-related methods
// para el repositorio de carpetas FolderFsRepository // 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 { impl FolderFsRepository {
// Obtiene la ruta completa a la papelera // Gets the full path to the trash directory
fn get_trash_dir(&self) -> PathBuf { fn get_trash_dir(&self) -> PathBuf {
self.get_root_path().join(".trash").join("folders") 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<PathBuf> { async fn create_trash_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<PathBuf> {
let trash_dir = self.get_trash_dir(); let trash_dir = self.get_trash_dir();
// Asegurarse que el directorio de la papelera existe // Ensure the trash directory exists
if !trash_dir.exists() { if !trash_dir.exists() {
fs::create_dir_all(&trash_dir).await fs::create_dir_all(&trash_dir).await
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?; .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)) 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 // Implementation of internal methods for trash functionality
// These will be enabled when the trash feature is re-enabled // These will be enabled when the trash feature is re-enabled
impl FolderFsRepository { impl FolderFsRepository {
/// Helper method that will be used for trash functionality /// Helper method that will be used for trash functionality
pub(crate) async fn _trash_move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> { 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 { let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => path, Ok(path) => path,
Err(e) => { Err(e) => {
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e); error!("Error getting folder path {}: {:?}", folder_id, e);
return Err(e); return Err(e);
} }
}; };
let folder_path_buf = PathBuf::from(folder_path.to_string()); let folder_path_buf = PathBuf::from(folder_path.to_string());
// Verificamos que la carpeta existe // Verify the folder exists
if !folder_path_buf.exists() { if !folder_path_buf.exists() {
return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id))); 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?; 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 { match fs::rename(&folder_path_buf, &trash_folder_path).await {
Ok(_) => { 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 { 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); return Err(e);
} }
Ok(()) Ok(())
}, },
Err(e) => { Err(e) => {
error!("Error moviendo carpeta a papelera: {}", e); error!("Error moving folder to trash: {}", e);
Err(FolderRepositoryError::StorageError(e.to_string())) 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<()> { 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 { let current_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path), Ok(path) => PathBuf::from(path),
Err(e) => { 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); return Err(e);
} }
}; };
// Convertir la ruta original a PathBuf // Convert the original path to PathBuf
let original_path_buf = PathBuf::from(original_path); 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 let Some(parent) = original_path_buf.parent() {
if !parent.exists() { if !parent.exists() {
fs::create_dir_all(parent).await fs::create_dir_all(parent).await
.map_err(|e| { .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()) 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(&current_path, &original_path_buf).await { match fs::rename(&current_path, &original_path_buf).await {
Ok(_) => { 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 { 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); return Err(e);
} }
Ok(()) Ok(())
}, },
Err(e) => { Err(e) => {
error!("Error restaurando carpeta: {}", e); error!("Error restoring folder: {}", e);
Err(FolderRepositoryError::StorageError(e.to_string())) 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<()> { 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 { let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path), Ok(path) => PathBuf::from(path),
Err(e) => { Err(e) => {
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e); error!("Error getting folder path {}: {:?}", folder_id, e);
return Err(e); return Err(e);
} }
}; };
// Eliminar la carpeta recursivamente // Delete the folder recursively
if folder_path.exists() { if folder_path.exists() {
match fs::remove_dir_all(&folder_path).await { match fs::remove_dir_all(&folder_path).await {
Ok(_) => { Ok(_) => {
debug!("Carpeta eliminada permanentemente: {}", folder_path.display()); debug!("Folder permanently deleted: {}", folder_path.display());
}, },
Err(e) => { Err(e) => {
error!("Error eliminando carpeta permanentemente: {}", e); error!("Error permanently deleting folder: {}", e);
// No reportar error si la carpeta ya no existe // Don't report error if the folder no longer exists
if e.kind() != std::io::ErrorKind::NotFound { if e.kind() != std::io::ErrorKind::NotFound {
return Err(FolderRepositoryError::StorageError(e.to_string())); 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 { 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); return Err(e);
} }
debug!("Carpeta eliminada permanentemente con éxito: {}", folder_id); debug!("Folder permanently deleted successfully: {}", folder_id);
Ok(()) Ok(())
} }
} }
// Re-exportaciones necesarias para el compilador // Re-exports needed by the compiler
use crate::infrastructure::repositories::repository_errors::FolderRepositoryError; use crate::infrastructure::repositories::repository_errors::FolderRepositoryError;
@@ -20,9 +20,9 @@ impl CalendarEventPgRepository {
#[async_trait] #[async_trait]
impl CalendarEventRepository for CalendarEventPgRepository { impl CalendarEventRepository for CalendarEventPgRepository {
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> { async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
// Este método necesitaría una implementación completa que construya el CalendarEvent // This method would need a full implementation that builds the CalendarEvent
// desde el resultado de la query, utilizando métodos del constructor // from the query result, using constructor methods
// Para esta demostración, vamos a retornar el mismo evento // For this demonstration, we return the same event
sqlx::query( sqlx::query(
r#" r#"
@@ -50,7 +50,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.await .await
.map_err(|e| DomainError::database_error(format!("Failed to create calendar event: {}", e)))?; .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) Ok(event)
} }
@@ -86,8 +86,8 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.await .await
.map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?; .map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?;
// En una implementación completa, recuperaríamos el evento actualizado // In a full implementation, we would retrieve the updated event
// Por simplicidad, devolvemos el mismo evento que recibimos // For simplicity, we return the same event we received
Ok(event) 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)))? .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()))?; .ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?;
// En una implementación real, construiríamos un objeto CalendarEvent completo // In a real implementation, we would build a complete CalendarEvent object
// Por simplicidad, creamos un objeto con valores predeterminados para // For simplicity, we create an object with default values to
// demostrar el enfoque sin macros // demonstrate the approach without macros
let event = CalendarEvent::with_id( let event = CalendarEvent::with_id(
row.get("id"), row.get("id"),
@@ -32,14 +32,14 @@ impl CalendarRepository for CalendarPgRepository {
.bind(calendar.owner_id()) .bind(calendar.owner_id())
.bind(calendar.description()) .bind(calendar.description())
.bind(calendar.color()) .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.created_at())
.bind(calendar.updated_at()) .bind(calendar.updated_at())
.fetch_one(&*self.pool) .fetch_one(&*self.pool)
.await .await
.map_err(|e| DomainError::database_error(format!("Failed to create calendar: {}", e)))?; .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( let result = Calendar::with_id(
row.get("id"), row.get("id"),
row.get("name"), row.get("name"),
@@ -66,14 +66,14 @@ impl CalendarRepository for CalendarPgRepository {
.bind(calendar.name()) .bind(calendar.name())
.bind(calendar.description()) .bind(calendar.description())
.bind(calendar.color()) .bind(calendar.color())
.bind(false) // is_public no existe como campo .bind(false) // is_public doesn't exist as a field
.bind(now) .bind(now)
.bind(calendar.id()) .bind(calendar.id())
.fetch_one(&*self.pool) .fetch_one(&*self.pool)
.await .await
.map_err(|e| DomainError::database_error(format!("Failed to update calendar: {}", e)))?; .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( let result = Calendar::with_id(
row.get("id"), row.get("id"),
row.get("name"), row.get("name"),
@@ -8,7 +8,7 @@ use crate::application::dtos::favorites_dto::FavoriteItemDto;
use crate::application::ports::favorites_ports::FavoritesRepositoryPort; use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind}; 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 { pub struct FavoritesPgRepository {
db_pool: Arc<PgPool>, db_pool: Arc<PgPool>,
} }
@@ -8,7 +8,7 @@ use crate::application::dtos::recent_dto::RecentItemDto;
use crate::application::ports::recent_ports::RecentItemsRepositoryPort; use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind}; 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 { pub struct RecentItemsPgRepository {
db_pool: Arc<PgPool>, db_pool: Arc<PgPool>,
} }
@@ -10,7 +10,7 @@ use crate::application::ports::auth_ports::SessionStoragePort;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction; use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implementar From<sqlx::Error> para SessionRepositoryError para permitir conversiones automáticas // Implement From<sqlx::Error> for SessionRepositoryError to allow automatic conversions
impl From<sqlx::Error> for SessionRepositoryError { impl From<sqlx::Error> for SessionRepositoryError {
fn from(err: sqlx::Error) -> Self { fn from(err: sqlx::Error) -> Self {
SessionPgRepository::map_sqlx_error(err) SessionPgRepository::map_sqlx_error(err)
@@ -26,14 +26,14 @@ impl SessionPgRepository {
Self { pool } 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 { pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
match err { match err {
sqlx::Error::RowNotFound => { sqlx::Error::RowNotFound => {
SessionRepositoryError::NotFound("Sesión no encontrada".to_string()) SessionRepositoryError::NotFound("Session not found".to_string())
}, },
_ => SessionRepositoryError::DatabaseError( _ => SessionRepositoryError::DatabaseError(
format!("Error de base de datos: {}", err) format!("Database error: {}", err)
), ),
} }
} }
@@ -41,9 +41,9 @@ impl SessionPgRepository {
#[async_trait] #[async_trait]
impl SessionRepository for SessionPgRepository { 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<Session> { async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
// Crear una copia de la sesión para el closure // Create a copy of the session for the closure
let session_clone = session.clone(); let session_clone = session.clone();
with_transaction( with_transaction(
@@ -51,7 +51,7 @@ impl SessionRepository for SessionPgRepository {
"create_session", "create_session",
|tx| { |tx| {
Box::pin(async move { Box::pin(async move {
// Insertar la sesión // Insert the session
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO auth.sessions ( INSERT INTO auth.sessions (
@@ -74,8 +74,8 @@ impl SessionRepository for SessionPgRepository {
.await .await
.map_err(Self::map_sqlx_error)?; .map_err(Self::map_sqlx_error)?;
// Opcionalmente, actualizar el último login del usuario // Optionally, update the user's last login
// dentro de la misma transacción // within the same transaction
sqlx::query( sqlx::query(
r#" r#"
UPDATE auth.users UPDATE auth.users
@@ -87,12 +87,12 @@ impl SessionRepository for SessionPgRepository {
.execute(&mut **tx) .execute(&mut **tx)
.await .await
.map_err(|e| { .map_err(|e| {
// Convertimos el error pero sin interrumpir la creación // Convert the error but without interrupting session
// de la sesión si falla la actualización // creation if the update fails
tracing::warn!("No se pudo actualizar last_login_at para usuario {}: {}", tracing::warn!("Could not update last_login_at for user {}: {}",
session_clone.user_id(), e); session_clone.user_id(), e);
SessionRepositoryError::DatabaseError(format!( 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) Ok(session)
} }
/// Obtiene una sesión por ID /// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> { async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> {
let row = sqlx::query( let row = sqlx::query(
r#" 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<Session> { async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session> {
let row = sqlx::query( let row = sqlx::query(
r#" 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<Vec<Session>> { async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>> {
let rows = sqlx::query( let rows = sqlx::query(
r#" r#"
@@ -195,16 +195,16 @@ impl SessionRepository for SessionPgRepository {
Ok(sessions) 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<()> { 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( with_transaction(
&self.pool, &self.pool,
"revoke_session", "revoke_session",
|tx| { |tx| {
Box::pin(async move { Box::pin(async move {
// Revocar la sesión // Revoke the session
let result = sqlx::query( let result = sqlx::query(
r#" r#"
UPDATE auth.sessions UPDATE auth.sessions
@@ -218,14 +218,14 @@ impl SessionRepository for SessionPgRepository {
.await .await
.map_err(Self::map_sqlx_error)?; .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 { if let Some(row) = result {
let user_id: String = row.try_get("user_id").unwrap_or_default(); let user_id: String = row.try_get("user_id").unwrap_or_default();
// Registrar evento de seguridad (en una tabla de seguridad) // Log security event (in a security table)
// Esto es opcional pero muestra cómo se puede realizar operaciones // This is optional but shows how additional operations
// adicionales en la misma transacción // can be performed in the same transaction
tracing::info!("Sesión con ID {} del usuario {} revocada", id, user_id); tracing::info!("Session with ID {} for user {} revoked", id, user_id);
} }
Ok(()) Ok(())
@@ -234,16 +234,16 @@ impl SessionRepository for SessionPgRepository {
).await ).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<u64> { async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
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( with_transaction(
&self.pool, &self.pool,
"revoke_all_user_sessions", "revoke_all_user_sessions",
|tx| { |tx| {
Box::pin(async move { Box::pin(async move {
// Revocar todas las sesiones del usuario // Revoke all sessions for the user
let result = sqlx::query( let result = sqlx::query(
r#" r#"
UPDATE auth.sessions UPDATE auth.sessions
@@ -258,9 +258,9 @@ impl SessionRepository for SessionPgRepository {
let affected = result.rows_affected(); let affected = result.rows_affected();
// Registrar evento de seguridad // Log security event
if affected > 0 { 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) Ok(affected)
@@ -269,7 +269,7 @@ impl SessionRepository for SessionPgRepository {
).await ).await
} }
/// Elimina sesiones expiradas /// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64> { async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64> {
let now = Utc::now(); 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] #[async_trait]
impl SessionStoragePort for SessionPgRepository { impl SessionStoragePort for SessionPgRepository {
async fn create_session(&self, session: Session) -> Result<Session, DomainError> { async fn create_session(&self, session: Session) -> Result<Session, DomainError> {
@@ -9,7 +9,7 @@ use crate::application::ports::auth_ports::UserStoragePort;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction; use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implementar From<sqlx::Error> para UserRepositoryError para permitir conversiones automáticas // Implement From<sqlx::Error> for UserRepositoryError to allow automatic conversions
impl From<sqlx::Error> for UserRepositoryError { impl From<sqlx::Error> for UserRepositoryError {
fn from(err: sqlx::Error) -> Self { fn from(err: sqlx::Error) -> Self {
UserPgRepository::map_sqlx_error(err) UserPgRepository::map_sqlx_error(err)
@@ -25,26 +25,26 @@ impl UserPgRepository {
Self { pool } 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 { pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
match err { match err {
sqlx::Error::RowNotFound => { sqlx::Error::RowNotFound => {
UserRepositoryError::NotFound("Usuario no encontrado".to_string()) UserRepositoryError::NotFound("User not found".to_string())
}, },
sqlx::Error::Database(db_err) => { sqlx::Error::Database(db_err) => {
if db_err.code().map_or(false, |code| code == "23505") { 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( UserRepositoryError::AlreadyExists(
"Usuario o email ya existe".to_string() "User or email already exists".to_string()
) )
} else { } else {
UserRepositoryError::DatabaseError( UserRepositoryError::DatabaseError(
format!("Error de base de datos: {}", db_err) format!("Database error: {}", db_err)
) )
} }
}, },
_ => UserRepositoryError::DatabaseError( _ => UserRepositoryError::DatabaseError(
format!("Error de base de datos: {}", err) format!("Database error: {}", err)
), ),
} }
} }
@@ -52,7 +52,7 @@ impl UserPgRepository {
#[async_trait] #[async_trait]
impl UserRepository for UserPgRepository { 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<User> { async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
// Creamos una copia del usuario para el closure // Creamos una copia del usuario para el closure
let user_clone = user.clone(); let user_clone = user.clone();
@@ -61,14 +61,14 @@ impl UserRepository for UserPgRepository {
&self.pool, &self.pool,
"create_user", "create_user",
|tx| { |tx| {
// Necesitamos mover el closure a un BoxFuture para devolver dentro // We need to move the closure into a BoxFuture to return inside
// de la llamada with_transaction // the with_transaction call
Box::pin(async move { Box::pin(async move {
// Usamos los getters para extraer los valores // Use getters to extract the values
// Convertimos user.role() a string para pasarlo como texto plano // Convert user.role() to string to pass it as plain text
let role_str = user_clone.role().to_string(); 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( let _result = sqlx::query(
r#" r#"
INSERT INTO auth.users ( INSERT INTO auth.users (
@@ -87,7 +87,7 @@ impl UserRepository for UserPgRepository {
.bind(user_clone.username()) .bind(user_clone.username())
.bind(user_clone.email()) .bind(user_clone.email())
.bind(user_clone.password_hash()) .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_quota_bytes())
.bind(user_clone.storage_used_bytes()) .bind(user_clone.storage_used_bytes())
.bind(user_clone.created_at()) .bind(user_clone.created_at())
@@ -100,18 +100,18 @@ impl UserRepository for UserPgRepository {
.await .await
.map_err(Self::map_sqlx_error)?; .map_err(Self::map_sqlx_error)?;
// Podríamos realizar operaciones adicionales aquí, // We could perform additional operations here,
// como configurar permisos, roles, etc. // such as configuring permissions, roles, etc.
Ok(user_clone) Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>> }) as BoxFuture<'_, UserRepositoryResult<User>>
} }
).await?; ).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<User> { async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User> {
let row = sqlx::query( let row = sqlx::query(
r#" 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<User> { async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User> {
let row = sqlx::query( let row = sqlx::query(
r#" 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<User> { async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User> {
let row = sqlx::query( let row = sqlx::query(
r#" 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<User> { async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
// Creamos una copia del usuario para el closure // Create a copy of the user for the closure
let user_clone = user.clone(); let user_clone = user.clone();
with_transaction( with_transaction(
@@ -247,7 +247,7 @@ impl UserRepository for UserPgRepository {
"update_user", "update_user",
|tx| { |tx| {
Box::pin(async move { Box::pin(async move {
// Actualizar el usuario // Update the user
sqlx::query( sqlx::query(
r#" r#"
UPDATE auth.users UPDATE auth.users
@@ -278,8 +278,8 @@ impl UserRepository for UserPgRepository {
.await .await
.map_err(Self::map_sqlx_error)?; .map_err(Self::map_sqlx_error)?;
// Podríamos realizar operaciones adicionales aquí dentro // We could perform additional operations here inside
// de la misma transacción, como actualizar permisos, etc. // the same transaction, such as updating permissions, etc.
Ok(user_clone) Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>> }) as BoxFuture<'_, UserRepositoryResult<User>>
@@ -289,7 +289,7 @@ impl UserRepository for UserPgRepository {
Ok(user) 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<()> { async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> {
sqlx::query( sqlx::query(
r#" r#"
@@ -309,7 +309,7 @@ impl UserRepository for UserPgRepository {
Ok(()) 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<()> { async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> {
sqlx::query( sqlx::query(
r#" r#"
@@ -328,7 +328,7 @@ impl UserRepository for UserPgRepository {
Ok(()) Ok(())
} }
/// Lista usuarios con paginación /// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> { async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query( let rows = sqlx::query(
r#" r#"
@@ -378,7 +378,7 @@ impl UserRepository for UserPgRepository {
Ok(users) 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<()> { async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> {
sqlx::query( sqlx::query(
r#" r#"
@@ -398,7 +398,7 @@ impl UserRepository for UserPgRepository {
Ok(()) 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<()> { async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> {
sqlx::query( sqlx::query(
r#" r#"
@@ -418,9 +418,9 @@ impl UserRepository for UserPgRepository {
Ok(()) Ok(())
} }
/// Cambia el rol de un usuario /// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> { 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(); let role_str = role.to_string();
sqlx::query( sqlx::query(
@@ -441,7 +441,7 @@ impl UserRepository for UserPgRepository {
Ok(()) Ok(())
} }
/// Lista usuarios por rol /// Lists users by role
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> { async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query( let rows = sqlx::query(
r#" r#"
@@ -490,7 +490,7 @@ impl UserRepository for UserPgRepository {
Ok(users) Ok(users)
} }
/// Elimina un usuario /// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> { async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
sqlx::query( sqlx::query(
r#" 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<()> { async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> {
sqlx::query( sqlx::query(
r#" r#"
@@ -568,7 +568,7 @@ impl UserRepository for UserPgRepository {
Ok(()) Ok(())
} }
/// Cuenta el número total de usuarios /// Counts the total number of users
async fn count_users(&self) -> UserRepositoryResult<i64> { async fn count_users(&self) -> UserRepositoryResult<i64> {
let row = sqlx::query( let row = sqlx::query(
"SELECT COUNT(*) as count FROM auth.users" "SELECT COUNT(*) as count FROM auth.users"
@@ -581,7 +581,7 @@ impl UserRepository for UserPgRepository {
Ok(count) Ok(count)
} }
/// Obtiene estadísticas de almacenamiento agregadas /// Gets aggregated storage statistics
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats> { async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats> {
let row = sqlx::query( let row = sqlx::query(
r#" 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] #[async_trait]
impl UserStoragePort for UserPgRepository { impl UserStoragePort for UserPgRepository {
async fn create_user(&self, user: User) -> Result<User, DomainError> { async fn create_user(&self, user: User) -> Result<User, DomainError> {
@@ -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)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct ShareRecord { struct ShareRecord {
id: String, id: String,
@@ -38,12 +38,12 @@ impl ShareFsRepository {
Self { config } 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 { fn get_shares_path(&self) -> String {
format!("{}/shares.json", self.config.storage_path.display()) 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<Vec<ShareRecord>, io::Error> { async fn read_shares(&self) -> Result<Vec<ShareRecord>, io::Error> {
let path = self.get_shares_path(); let path = self.get_shares_path();
let path = Path::new(&path); let path = Path::new(&path);
@@ -58,12 +58,12 @@ impl ShareFsRepository {
Ok(shares) 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> { async fn write_shares(&self, shares: &[ShareRecord]) -> Result<(), io::Error> {
let path = self.get_shares_path(); let path = self.get_shares_path();
let json = serde_json::to_string_pretty(shares)?; 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(); let dir = Path::new(&path).parent().unwrap();
if !dir.exists() { if !dir.exists() {
fs::create_dir_all(dir).await? fs::create_dir_all(dir).await?
@@ -72,7 +72,7 @@ impl ShareFsRepository {
fs::write(path, json).await 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 { fn to_entity(&self, record: &ShareRecord) -> Share {
let item_type = ShareItemType::try_from(record.item_type.as_str()) let item_type = ShareItemType::try_from(record.item_type.as_str())
.unwrap_or(ShareItemType::File); .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 { fn to_record(&self, share: &Share) -> ShareRecord {
ShareRecord { ShareRecord {
id: share.id().to_string(), id: share.id().to_string(),
@@ -122,16 +122,16 @@ impl ShareStoragePort for ShareFsRepository {
let mut shares = self.read_shares().await let mut shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?; .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 existing_index = shares.iter().position(|s| s.id == share.id());
let record = self.to_record(share); let record = self.to_record(share);
if let Some(index) = existing_index { if let Some(index) = existing_index {
// Actualización // Update
shares[index] = record; shares[index] = record;
} else { } else {
// Inserción // Insert
shares.push(record); shares.push(record);
} }
@@ -190,16 +190,16 @@ impl ShareStoragePort for ShareFsRepository {
let mut shares = self.read_shares().await let mut shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?; .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()) let index = shares.iter().position(|s| s.id == share.id())
.ok_or_else(|| { .ok_or_else(|| {
DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id())) 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); shares[index] = self.to_record(share);
// Guarda los cambios // Save changes
self.write_shares(&shares).await self.write_shares(&shares).await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?; .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 let mut shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?; .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(); let initial_len = shares.len();
shares.retain(|s| s.id != id); 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 { if shares.len() == initial_len {
return Err(DomainError::not_found("Share", format!("Share with ID {} not found for deletion", id))); 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 self.write_shares(&shares).await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?; .map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
@@ -230,15 +230,15 @@ impl ShareStoragePort for ShareFsRepository {
let shares = self.read_shares().await let shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?; .map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Filtra los enlaces del usuario // Filter the user's links
let user_shares: Vec<ShareRecord> = shares.into_iter() let user_shares: Vec<ShareRecord> = shares.into_iter()
.filter(|s| s.created_by == user_id) .filter(|s| s.created_by == user_id)
.collect(); .collect();
// Calcula el total // Calculate the total
let total = user_shares.len(); let total = user_shares.len();
// Aplica la paginación // Apply pagination
let paginated: Vec<Share> = user_shares.iter() let paginated: Vec<Share> = user_shares.iter()
.skip(offset) .skip(offset)
.take(limit) .take(limit)
@@ -12,7 +12,7 @@ use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::trash_repository::TrashRepository; use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::ports::outbound::IdMappingPort; 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)] #[derive(Debug, Serialize, Deserialize)]
struct TrashedItemEntry { struct TrashedItemEntry {
id: String, id: String,
@@ -25,7 +25,7 @@ struct TrashedItemEntry {
deletion_date: String, deletion_date: String,
} }
/// Implementación del repositorio de papelera usando el sistema de archivos /// Trash repository implementation using the file system
pub struct TrashFsRepository { pub struct TrashFsRepository {
trash_dir: PathBuf, trash_dir: PathBuf,
trash_index_path: 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<()> { async fn ensure_trash_dir(&self) -> Result<()> {
debug!("Checking if trash directory exists: {}", self.trash_dir.display()); debug!("Checking if trash directory exists: {}", self.trash_dir.display());
if !self.trash_dir.exists() { if !self.trash_dir.exists() {
@@ -105,7 +105,7 @@ impl TrashFsRepository {
Ok(()) Ok(())
} }
/// Obtiene todas las entradas del índice de papelera /// Gets all entries from the trash index
async fn get_trash_entries(&self) -> Result<Vec<TrashedItemEntry>> { async fn get_trash_entries(&self) -> Result<Vec<TrashedItemEntry>> {
self.ensure_trash_dir().await?; self.ensure_trash_dir().await?;
@@ -134,7 +134,7 @@ impl TrashFsRepository {
Ok(entries) 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<TrashedItemEntry>) -> Result<()> { async fn save_trash_entries(&self, entries: Vec<TrashedItemEntry>) -> Result<()> {
self.ensure_trash_dir().await?; self.ensure_trash_dir().await?;
@@ -155,7 +155,7 @@ impl TrashFsRepository {
Ok(()) 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<TrashedItem> { fn entry_to_trashed_item(&self, entry: TrashedItemEntry) -> Result<TrashedItem> {
let item_type = match entry.item_type.as_str() { let item_type = match entry.item_type.as_str() {
"file" => TrashedItemType::File, "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 { fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry {
TrashedItemEntry { TrashedItemEntry {
id: item.id().to_string(), id: item.id().to_string(),
@@ -228,9 +228,9 @@ impl TrashFsRepository {
impl TrashRepository for TrashFsRepository { impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> { 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()); let user_trash_dir = self.trash_dir.join("files").join(item.user_id().to_string());
debug!("User trash directory path: {}", user_trash_dir.display()); debug!("User trash directory path: {}", user_trash_dir.display());
@@ -268,7 +268,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> { async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
debug!("Obteniendo elementos en papelera para usuario: {}", user_id); debug!("Getting trash items for user: {}", user_id);
let entries = self.get_trash_entries().await?; let entries = self.get_trash_entries().await?;
@@ -290,7 +290,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> { async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
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?; let entries = self.get_trash_entries().await?;
@@ -311,7 +311,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { 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?; let mut entries = self.get_trash_entries().await?;
@@ -333,16 +333,16 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { 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 // Simply remove the entry from the index
// Los archivos físicos se eliminarán a través del repositorio correspondiente // Physical files will be deleted through the corresponding repository
self.restore_from_trash(id, user_id).await self.restore_from_trash(id, user_id).await
} }
#[instrument(skip(self))] #[instrument(skip(self))]
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> { 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 mut entries = self.get_trash_entries().await?;
let user_id_str = user_id.to_string(); let user_id_str = user_id.to_string();
@@ -355,7 +355,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> { async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
debug!("Buscando elementos de papelera expirados"); debug!("Looking for expired trash items");
let entries = self.get_trash_entries().await?; let entries = self.get_trash_entries().await?;
let now = Utc::now(); let now = Utc::now();
+109 -109
View File
@@ -5,71 +5,71 @@ use tokio::sync::{Mutex, Semaphore};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tracing::debug; 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 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; 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; 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 { pub struct BufferPool {
/// Pool de buffers disponibles /// Pool of available buffers
pool: Mutex<VecDeque<PooledBuffer>>, pool: Mutex<VecDeque<PooledBuffer>>,
/// Semáforo para limitar el número máximo de buffers /// Semaphore to limit the maximum number of buffers
limit: Semaphore, limit: Semaphore,
/// Tamaño de los buffers en el pool /// Size of buffers in the pool
buffer_size: usize, buffer_size: usize,
/// Estadísticas del pool /// Pool statistics
stats: Mutex<BufferPoolStats>, stats: Mutex<BufferPoolStats>,
/// Tiempo de vida de un buffer inactivo /// Time-to-live for an inactive buffer
buffer_ttl: Duration, buffer_ttl: Duration,
} }
/// Estructura para tracking de estadísticas del pool /// Structure for tracking pool statistics
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct BufferPoolStats { pub struct BufferPoolStats {
/// Número total de operaciones de get /// Total number of get operations
pub gets: usize, pub gets: usize,
/// Número de hits del pool (reutilización exitosa) /// Number of pool hits (successful reuse)
pub hits: usize, pub hits: usize,
/// Número de misses (creación de nuevo buffer) /// Number of misses (new buffer creation)
pub misses: usize, pub misses: usize,
/// Número de retornos al pool /// Number of returns to the pool
pub returns: usize, pub returns: usize,
/// Número de eviction por TTL /// Number of TTL evictions
pub evictions: usize, pub evictions: usize,
/// Número máximo de buffers alcanzado /// Maximum number of buffers reached
pub max_buffers_reached: usize, pub max_buffers_reached: usize,
/// Esperas por semáforo /// Semaphore waits
pub waits: usize, pub waits: usize,
} }
/// Buffer del pool con metadatos para gestión /// Pool buffer with management metadata
struct PooledBuffer { struct PooledBuffer {
/// Buffer real de bytes /// Actual byte buffer
buffer: Vec<u8>, buffer: Vec<u8>,
/// Timestamp de cuándo se añadió/retornó al pool /// Timestamp of when it was added/returned to the pool
last_used: Instant, last_used: Instant,
} }
/// Buffer prestado del pool con cleanup automático /// Borrowed buffer from the pool with automatic cleanup
#[derive(Clone)] #[derive(Clone)]
pub struct BorrowedBuffer { pub struct BorrowedBuffer {
/// Buffer actual /// Current buffer
buffer: Vec<u8>, buffer: Vec<u8>,
/// Tamaño real utilizado del buffer /// Actual used size of the buffer
used_size: usize, used_size: usize,
/// Referencia al pool para retornar /// Reference to the pool for returning
pool: Arc<BufferPool>, pool: Arc<BufferPool>,
/// Si el buffer debe o no retornarse al pool /// Whether the buffer should be returned to the pool or not
return_to_pool: bool, return_to_pool: bool,
} }
impl BufferPool { 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<Self> { pub fn new(buffer_size: usize, max_buffers: usize, buffer_ttl_secs: u64) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
pool: Mutex::new(VecDeque::with_capacity(max_buffers)), 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> { pub fn default() -> Arc<Self> {
Self::new( Self::new(
DEFAULT_BUFFER_SIZE, 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<Self> to ensure the BorrowedBuffer keeps a proper /// This version takes an Arc<Self> to ensure the BorrowedBuffer keeps a proper
/// reference to the shared pool (not a clone). /// reference to the shared pool (not a clone).
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn get_buffer(self: &Arc<Self>) -> BorrowedBuffer { pub async fn get_buffer(self: &Arc<Self>) -> BorrowedBuffer {
// Incrementar contador de gets // Increment get counter
{ {
let mut stats = self.stats.lock().await; let mut stats = self.stats.lock().await;
stats.gets += 1; stats.gets += 1;
} }
// Control de concurrencia // Concurrency control
// Acquire a semaphore permit. If none available, wait. // Acquire a semaphore permit. If none available, wait.
// We forget() the permit so it doesn't auto-release on drop. // 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). // Instead, the permit is manually released in return_buffer/Drop via add_permits(1).
match self.limit.try_acquire() { match self.limit.try_acquire() {
Ok(permit) => permit.forget(), Ok(permit) => permit.forget(),
Err(_) => { Err(_) => {
// No hay permisos disponibles, esperamos // No permits available, waiting
{ {
let mut stats = self.stats.lock().await; let mut stats = self.stats.lock().await;
stats.waits += 1; 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 mut pool_locked = self.pool.lock().await;
let pool_arc = Arc::clone(self); let pool_arc = Arc::clone(self);
if let Some(mut pooled_buffer) = pool_locked.pop_front() { 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 { 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; let mut stats = self.stats.lock().await;
stats.evictions += 1; stats.evictions += 1;
stats.misses += 1; stats.misses += 1;
@@ -137,8 +137,8 @@ impl BufferPool {
debug!("Buffer pool: evicted expired buffer"); debug!("Buffer pool: evicted expired buffer");
// Crear nuevo buffer (reutilizando el permiso) // Create new buffer (reusing the permit)
drop(pool_locked); // Liberar el lock antes de retornar drop(pool_locked); // Release the lock before returning
BorrowedBuffer { BorrowedBuffer {
buffer: vec![0; self.buffer_size], buffer: vec![0; self.buffer_size],
@@ -147,15 +147,15 @@ impl BufferPool {
return_to_pool: true, return_to_pool: true,
} }
} else { } else {
// Buffer válido, lo reutilizamos // Valid buffer, reuse it
let mut stats = self.stats.lock().await; let mut stats = self.stats.lock().await;
stats.hits += 1; stats.hits += 1;
drop(stats); drop(stats);
// Liberar el lock antes de retornar // Release the lock before returning
drop(pool_locked); drop(pool_locked);
// Limpiar buffer por seguridad // Clear buffer for security
pooled_buffer.buffer.fill(0); pooled_buffer.buffer.fill(0);
BorrowedBuffer { BorrowedBuffer {
@@ -166,12 +166,12 @@ impl BufferPool {
} }
} }
} else { } else {
// No hay buffers disponibles, creamos uno nuevo // No buffers available, create a new one
let mut stats = self.stats.lock().await; let mut stats = self.stats.lock().await;
stats.misses += 1; stats.misses += 1;
drop(stats); drop(stats);
// Liberar el lock antes de retornar // Release the lock before returning
drop(pool_locked); drop(pool_locked);
debug!("Buffer pool: creating new buffer"); 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<u8>) { async fn return_buffer(&self, mut buffer: Vec<u8>) {
// 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 { if buffer.capacity() != self.buffer_size {
debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})", debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})",
buffer.capacity(), self.buffer_size); buffer.capacity(), self.buffer_size);
@@ -196,10 +196,10 @@ impl BufferPool {
return; return;
} }
// Resize para asegurar capacidad correcta // Resize to ensure correct capacity
buffer.resize(self.buffer_size, 0); buffer.resize(self.buffer_size, 0);
// Añadir al pool // Add to the pool
let mut pool_locked = self.pool.lock().await; let mut pool_locked = self.pool.lock().await;
pool_locked.push_back(PooledBuffer { pool_locked.push_back(PooledBuffer {
@@ -207,7 +207,7 @@ impl BufferPool {
last_used: Instant::now(), last_used: Instant::now(),
}); });
// Actualizar estadísticas // Update statistics
let mut stats = self.stats.lock().await; let mut stats = self.stats.lock().await;
stats.returns += 1; stats.returns += 1;
@@ -217,24 +217,24 @@ impl BufferPool {
self.limit.add_permits(1); self.limit.add_permits(1);
} }
/// Limpia buffers expirados del pool /// Cleans expired buffers from the pool
pub async fn clean_expired_buffers(&self) { pub async fn clean_expired_buffers(&self) {
let _now = Instant::now(); let _now = Instant::now();
let mut pool_locked = self.pool.lock().await; let mut pool_locked = self.pool.lock().await;
// Contar expirados // Count expired
let count_before = pool_locked.len(); let count_before = pool_locked.len();
// Filtrar manteniendo solo los no expirados // Filter keeping only non-expired
pool_locked.retain(|buffer| { pool_locked.retain(|buffer| {
buffer.last_used.elapsed() <= self.buffer_ttl buffer.last_used.elapsed() <= self.buffer_ttl
}); });
// Contar cuántos se eliminaron // Count how many were removed
let removed = count_before - pool_locked.len(); let removed = count_before - pool_locked.len();
if removed > 0 { if removed > 0 {
// Actualizar estadísticas // Update statistics
let mut stats = self.stats.lock().await; let mut stats = self.stats.lock().await;
stats.evictions += removed; 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 { pub async fn get_stats(&self) -> BufferPoolStats {
self.stats.lock().await.clone() self.stats.lock().await.clone()
} }
/// Inicia la tarea periódica de limpieza /// Starts the periodic cleanup task
pub fn start_cleaner(pool: Arc<Self>) { pub fn start_cleaner(pool: Arc<Self>) {
tokio::spawn(async move { 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 { loop {
tokio::time::sleep(interval).await; tokio::time::sleep(interval).await;
pool.clean_expired_buffers().await; pool.clean_expired_buffers().await;
// Loguear estadísticas periódicamente // Log statistics periodically
let stats = pool.get_stats().await; let stats = pool.get_stats().await;
debug!("Buffer pool stats: gets={}, hits={}, misses={}, hit_ratio={:.2}%, returns={}, \ debug!("Buffer pool stats: gets={}, hits={}, misses={}, hit_ratio={:.2}%, returns={}, \
evictions={}, max_reached={}, waits={}", evictions={}, max_reached={}, waits={}",
@@ -286,31 +286,31 @@ impl Clone for BufferPool {
} }
impl BorrowedBuffer { impl BorrowedBuffer {
/// Accede al buffer interno /// Accesses the internal buffer
pub fn as_mut_slice(&mut self) -> &mut [u8] { pub fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.buffer &mut self.buffer
} }
/// Obtiene una referencia a los datos utilizados /// Gets a reference to the used data
pub fn as_slice(&self) -> &[u8] { pub fn as_slice(&self) -> &[u8] {
&self.buffer[..self.used_size] &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) { pub fn set_used(&mut self, size: usize) {
self.used_size = min(size, self.buffer.len()); self.used_size = min(size, self.buffer.len());
} }
/// Convierte en un Vec<u8> que incluye solo los datos utilizados /// Converts into a Vec<u8> that includes only the used data
pub fn into_vec(mut self) -> Vec<u8> { pub fn into_vec(mut self) -> Vec<u8> {
// Marcar para no devolver al pool // Mark to not return to pool
self.return_to_pool = false; 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() 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 { pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
let copy_size = min(data.len(), self.buffer.len()); let copy_size = min(data.len(), self.buffer.len());
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]); self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
@@ -318,32 +318,32 @@ impl BorrowedBuffer {
copy_size 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 { pub fn do_not_return(mut self) -> Self {
self.return_to_pool = false; self.return_to_pool = false;
self self
} }
/// Obtiene el tamaño total del buffer /// Gets the total buffer size
pub fn capacity(&self) -> usize { pub fn capacity(&self) -> usize {
self.buffer.len() self.buffer.len()
} }
/// Obtiene el tamaño usado del buffer /// Gets the used buffer size
pub fn used_size(&self) -> usize { pub fn used_size(&self) -> usize {
self.used_size 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 { impl Drop for BorrowedBuffer {
fn drop(&mut self) { fn drop(&mut self) {
if self.return_to_pool { 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 buffer = std::mem::take(&mut self.buffer);
let pool = self.pool.clone(); 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 // return_buffer will release the semaphore permit
tokio::spawn(async move { tokio::spawn(async move {
pool.return_buffer(buffer).await; pool.return_buffer(buffer).await;
@@ -361,39 +361,39 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_buffer_pooling() { async fn test_buffer_pooling() {
// Crear pool pequeño para testing // Create small pool for testing
let pool = BufferPool::new(1024, 5, 60); let pool = BufferPool::new(1024, 5, 60);
// Obtener un buffer // Get a buffer
let mut buffer1 = pool.get_buffer().await; let mut buffer1 = pool.get_buffer().await;
buffer1.copy_from_slice(b"test data"); buffer1.copy_from_slice(b"test data");
assert_eq!(buffer1.as_slice(), b"test data"); assert_eq!(buffer1.as_slice(), b"test data");
// Obtener otro buffer // Get another buffer
let buffer2 = pool.get_buffer().await; let buffer2 = pool.get_buffer().await;
// Verificar stats // Verify stats
let stats = pool.get_stats().await; let stats = pool.get_stats().await;
assert_eq!(stats.gets, 2); assert_eq!(stats.gets, 2);
assert_eq!(stats.hits, 0); // sin hits todavía assert_eq!(stats.hits, 0); // no hits yet
assert_eq!(stats.misses, 2); // todos son misses 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); drop(buffer1);
// Permitir que el return asíncrono ocurra // Allow the async return to occur
tokio::time::sleep(Duration::from_millis(10)).await; 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; let buffer3 = pool.get_buffer().await;
// Verificar stats actualizados // Verify updated stats
let stats = pool.get_stats().await; let stats = pool.get_stats().await;
assert_eq!(stats.gets, 3); assert_eq!(stats.gets, 3);
assert_eq!(stats.hits, 1); // ahora debería haber un hit assert_eq!(stats.hits, 1); // now there should be a hit
assert_eq!(stats.returns, 1); // un buffer retornado assert_eq!(stats.returns, 1); // one buffer returned
// Limpiar // Cleanup
drop(buffer2); drop(buffer2);
drop(buffer3); drop(buffer3);
} }
@@ -402,19 +402,19 @@ mod tests {
async fn test_buffer_operations() { async fn test_buffer_operations() {
let pool = BufferPool::new(1024, 10, 60); let pool = BufferPool::new(1024, 10, 60);
// Obtener buffer // Get buffer
let mut buffer = pool.get_buffer().await; let mut buffer = pool.get_buffer().await;
// Escribir datos // Write data
buffer.copy_from_slice(b"Hello, world!"); buffer.copy_from_slice(b"Hello, world!");
assert_eq!(buffer.used_size(), 13); assert_eq!(buffer.used_size(), 13);
assert_eq!(buffer.as_slice(), b"Hello, world!"); assert_eq!(buffer.as_slice(), b"Hello, world!");
// Convertir a vec y verificar // Convert to vec and verify
let vec = buffer.into_vec(); // Esto impide retornar al pool let vec = buffer.into_vec(); // This prevents returning to pool
assert_eq!(vec, b"Hello, world!"); 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; tokio::time::sleep(Duration::from_millis(10)).await;
let stats = pool.get_stats().await; let stats = pool.get_stats().await;
assert_eq!(stats.returns, 0); assert_eq!(stats.returns, 0);
@@ -422,76 +422,76 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_pool_limit() { async fn test_pool_limit() {
// Pool con solo 3 buffers // Pool with only 3 buffers
let pool = BufferPool::new(1024, 3, 60); 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 buffer1 = pool.get_buffer().await;
let buffer2 = pool.get_buffer().await; let buffer2 = pool.get_buffer().await;
let buffer3 = pool.get_buffer().await; let buffer3 = pool.get_buffer().await;
// Verificar stats // Verify stats
let stats = pool.get_stats().await; let stats = pool.get_stats().await;
assert_eq!(stats.gets, 3); 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 pool_clone = pool.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
let _buffer4 = pool_clone.get_buffer().await; let _buffer4 = pool_clone.get_buffer().await;
true 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; tokio::time::sleep(Duration::from_millis(50)).await;
// Verificar que hay una espera // Verify there is a wait
let stats = pool.get_stats().await; let stats = pool.get_stats().await;
assert_eq!(stats.waits, 1); assert_eq!(stats.waits, 1);
// Liberar un buffer // Release a buffer
drop(buffer1); 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; 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()); assert!(handle.await.unwrap());
// Limpiar // Cleanup
drop(buffer2); drop(buffer2);
drop(buffer3); drop(buffer3);
} }
#[tokio::test] #[tokio::test]
async fn test_ttl_expiration() { async fn test_ttl_expiration() {
// Pool con TTL muy corto para testing // Pool with very short TTL for testing
let pool = BufferPool::new(1024, 5, 1); // 1 segundo TTL 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; let buffer = pool.get_buffer().await;
drop(buffer); drop(buffer);
// Permitir que el return asíncrono ocurra // Allow the async return to occur
tokio::time::sleep(Duration::from_millis(50)).await; 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; let stats = pool.get_stats().await;
assert_eq!(stats.returns, 1); 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; tokio::time::sleep(Duration::from_secs(2)).await;
// Limpiar expirados // Clean expired
pool.clean_expired_buffers().await; 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; let _buffer2 = pool.get_buffer().await;
// Verificar stats // Verify stats
let stats = pool.get_stats().await; let stats = pool.get_stats().await;
assert_eq!(stats.evictions, 1); // un buffer expirado assert_eq!(stats.evictions, 1); // one expired buffer
assert_eq!(stats.hits, 0); // sin hits (el buffer expiró) assert_eq!(stats.hits, 0); // no hits (the buffer expired)
assert_eq!(stats.misses, 2); // dos misses (1er y 3er get) assert_eq!(stats.misses, 2); // two misses (1st and 3rd get)
} }
} }
@@ -16,16 +16,16 @@ use crate::application::ports::compression_ports::{
use crate::domain::errors::DomainError; use crate::domain::errors::DomainError;
use crate::infrastructure::services::buffer_pool::BufferPool; use crate::infrastructure::services::buffer_pool::BufferPool;
/// Nivel de compresión para ficheros /// Compression level for files
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionLevel { pub enum CompressionLevel {
/// Sin compresión (solo para transferencia) /// No compression (transfer only)
None = 0, None = 0,
/// Compresión rápida con menor ratio /// Fast compression with lower ratio
Fast = 1, Fast = 1,
/// Compresión balanceada (por defecto) /// Balanced compression (default)
Default = 6, Default = 6,
/// Compresión máxima (más lenta) /// Maximum compression (slower)
Best = 9, Best = 9,
} }
@@ -40,49 +40,49 @@ impl From<CompressionLevel> 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 const COMPRESSION_SIZE_THRESHOLD: u64 = 1024 * 50; // 50KB
/// Interfaz para servicios de compresión /// Interface for compression services
#[async_trait] #[async_trait]
pub trait CompressionService: Send + Sync { pub trait CompressionService: Send + Sync {
/// Comprime datos en memoria /// Compresses data in memory
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>>; async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>>;
/// Descomprime datos en memoria /// Decompresses data in memory
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>; async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
/// Comprime un stream de datos /// Compresses a data stream
fn compress_stream<S>(&self, stream: S, level: CompressionLevel) fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
-> impl Stream<Item = io::Result<Bytes>> + Send -> impl Stream<Item = io::Result<Bytes>> + Send
where where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin; S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
/// Descomprime un stream de datos /// Decompresses a data stream
fn decompress_stream<S>(&self, compressed_stream: S) fn decompress_stream<S>(&self, compressed_stream: S)
-> impl Stream<Item = io::Result<Bytes>> + Send -> impl Stream<Item = io::Result<Bytes>> + Send
where where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin; S: Stream<Item = io::Result<Bytes>> + 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; 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 { pub struct GzipCompressionService {
/// Pool de buffers para optimización de memoria /// Buffer pool for memory optimization
buffer_pool: Option<Arc<BufferPool>>, buffer_pool: Option<Arc<BufferPool>>,
} }
impl GzipCompressionService { impl GzipCompressionService {
/// Crea una nueva instancia del servicio /// Creates a new service instance
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
buffer_pool: None, 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<BufferPool>) -> Self { pub fn new_with_buffer_pool(buffer_pool: Arc<BufferPool>) -> Self {
Self { Self {
buffer_pool: Some(buffer_pool), buffer_pool: Some(buffer_pool),
@@ -92,64 +92,64 @@ impl GzipCompressionService {
#[async_trait] #[async_trait]
impl CompressionService for GzipCompressionService { 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<Vec<u8>> { async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>> {
// 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 { 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; 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; 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 { 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_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone(); let buffer_clone = buffer_ptr.clone();
// Comprimir datos // Compress data
// Clonar los datos para evitar problemas de lifetime // Clone the data to avoid lifetime issues
let data_owned = data.to_vec(); let data_owned = data.to_vec();
let result = tokio::task::spawn_blocking(move || { let result = tokio::task::spawn_blocking(move || {
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into()); 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()) { let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer, buffer => buffer,
}; };
// Leer directamente en el buffer // Read directly into the buffer
let read_bytes = encoder.read(buffer_guard.as_mut_slice())?; let read_bytes = encoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes); buffer_guard.set_used(read_bytes);
Ok(()) as io::Result<()> Ok(()) as io::Result<()>
}).await; }).await;
// Verificar resultado // Verify result
match result { match result {
Ok(Ok(())) => { Ok(Ok(())) => {
// Obtener el buffer y convertirlo a Vec<u8> // Get the buffer and convert it to Vec<u8>
let buffer = buffer_ptr.lock().await; let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone(); let cloned_buffer = buffer.clone();
drop(buffer); // Liberar el mutex primero drop(buffer); // Release the mutex first
return Ok(cloned_buffer.into_vec()); return Ok(cloned_buffer.into_vec());
}, },
Ok(Err(e)) => { Ok(Err(e)) => {
error!("Error en compresión con buffer pool: {}", e); error!("Compression error with buffer pool: {}", e);
// Continuar con implementación estándar // Fall back to standard implementation
}, },
Err(e) => { Err(e) => {
error!("Error en task de compresión con buffer pool: {}", e); error!("Compression task error with buffer pool: {}", e);
// Continuar con implementación estándar // Fall back to standard implementation
} }
} }
} }
} }
// Implementación estándar si no hay buffer pool o el buffer es insuficiente // Standard implementation if there is no buffer pool or the buffer is insufficient
// Clonar los datos para evitar problemas de lifetime // Clone the data to avoid lifetime issues
let data_owned = data.to_vec(); let data_owned = data.to_vec();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@@ -158,79 +158,79 @@ impl CompressionService for GzipCompressionService {
encoder.read_to_end(&mut compressed)?; encoder.read_to_end(&mut compressed)?;
Ok(compressed) Ok(compressed)
}).await.unwrap_or_else(|e| { }).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())) 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<Vec<u8>> { async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>> {
// 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 { 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; let estimated_size = compressed_data.len() * 5;
// Obtener un buffer del pool // Get a buffer from the pool
let buffer = pool.get_buffer().await; 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 { 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 data = compressed_data.to_vec();
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer)); let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone(); let buffer_clone = buffer_ptr.clone();
// Descomprimir datos // Decompress data
let result = tokio::task::spawn_blocking(move || { let result = tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]); 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()) { let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer, buffer => buffer,
}; };
// Leer directamente en el buffer // Read directly into the buffer
let read_bytes = decoder.read(buffer_guard.as_mut_slice())?; let read_bytes = decoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes); buffer_guard.set_used(read_bytes);
Ok(()) as io::Result<()> Ok(()) as io::Result<()>
}).await; }).await;
// Verificar resultado // Verify result
match result { match result {
Ok(Ok(())) => { Ok(Ok(())) => {
// Obtener el buffer y convertirlo a Vec<u8> // Get the buffer and convert it to Vec<u8>
let buffer = buffer_ptr.lock().await; let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone(); let cloned_buffer = buffer.clone();
drop(buffer); // Liberar el mutex primero drop(buffer); // Release the mutex first
return Ok(cloned_buffer.into_vec()); return Ok(cloned_buffer.into_vec());
}, },
Ok(Err(e)) => { Ok(Err(e)) => {
error!("Error en descompresión con buffer pool: {}", e); error!("Decompression error with buffer pool: {}", e);
// Continuar con implementación estándar // Fall back to standard implementation
}, },
Err(e) => { Err(e) => {
error!("Error en task de descompresión con buffer pool: {}", e); error!("Decompression task error with buffer pool: {}", e);
// Continuar con implementación estándar // Fall back to standard implementation
} }
} }
} }
} }
// Implementación estándar si no hay buffer pool o el buffer es insuficiente // Standard implementation if there is no buffer pool or the buffer is insufficient
let data = compressed_data.to_vec(); // Clonar para mover al worker let data = compressed_data.to_vec(); // Clone to move to the worker
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]); let mut decoder = GzDecoder::new(&data[..]);
let mut decompressed = Vec::new(); let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?; decoder.read_to_end(&mut decompressed)?;
Ok(decompressed) Ok(decompressed)
}).await.unwrap_or_else(|e| { }).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())) Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
}) })
} }
/// Comprime un stream de bytes /// Compresses a byte stream
fn compress_stream<S>(&self, stream: S, level: CompressionLevel) fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
-> impl Stream<Item = io::Result<Bytes>> + Send -> impl Stream<Item = io::Result<Bytes>> + Send
where where
@@ -271,7 +271,7 @@ impl CompressionService for GzipCompressionService {
}) })
} }
/// Descomprime un stream de bytes /// Decompresses a byte stream
fn decompress_stream<S>(&self, compressed_stream: S) fn decompress_stream<S>(&self, compressed_stream: S)
-> impl Stream<Item = io::Result<Bytes>> + Send -> impl Stream<Item = io::Result<Bytes>> + Send
where 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 { 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 { if size < COMPRESSION_SIZE_THRESHOLD {
return false; return false;
} }
// No comprimir archivos ya comprimidos // Do not compress already compressed files
if mime_type.starts_with("image/") if mime_type.starts_with("image/")
&& !mime_type.contains("svg") && !mime_type.contains("svg")
&& !mime_type.contains("bmp") { && !mime_type.contains("bmp") {
@@ -345,7 +345,7 @@ impl CompressionService for GzipCompressionService {
return false; return false;
} }
// Comprimir archivos de texto, documentos, y otros tipos compresibles // Compress text files, documents, and other compressible types
true true
} }
} }
@@ -389,19 +389,19 @@ mod tests {
async fn test_compress_decompress_data() { async fn test_compress_decompress_data() {
let service = GzipCompressionService::new(); let service = GzipCompressionService::new();
// Datos de prueba // Test data
let data = "Hello, world! ".repeat(1000).into_bytes(); let data = "Hello, world! ".repeat(1000).into_bytes();
// Comprimir // Compress
let compressed = CompressionService::compress_data(&service, &data, CompressionLevel::Default).await.unwrap(); 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()); assert!(compressed.len() < data.len());
// Descomprimir // Decompress
let decompressed = CompressionService::decompress_data(&service, &compressed).await.unwrap(); 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); assert_eq!(decompressed, data);
} }
@@ -409,30 +409,30 @@ mod tests {
async fn test_compress_decompress_stream() { async fn test_compress_decompress_stream() {
let service = GzipCompressionService::new(); let service = GzipCompressionService::new();
// Crear datos de prueba // Create test data
let chunks = vec![ let chunks = vec![
Ok(Bytes::from("Hello, ")), Ok(Bytes::from("Hello, ")),
Ok(Bytes::from("world! ")), Ok(Bytes::from("world! ")),
Ok(Bytes::from("This is a test of streaming compression.")), Ok(Bytes::from("This is a test of streaming compression.")),
]; ];
// Convertir a stream // Convert to stream
let input_stream = futures::stream::iter(chunks); let input_stream = futures::stream::iter(chunks);
// Comprimir el stream // Compress the stream
let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default); let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default);
// Recolectar los bytes comprimidos // Collect the compressed bytes
let compressed_bytes = compressed_stream let compressed_bytes = compressed_stream
.try_fold(Vec::new(), |mut acc, chunk| async move { .try_fold(Vec::new(), |mut acc, chunk| async move {
acc.extend_from_slice(&chunk); acc.extend_from_slice(&chunk);
Ok(acc) Ok(acc)
}).await.unwrap(); }).await.unwrap();
// Descomprimir los datos // Decompress the data
let decompressed = CompressionService::decompress_data(&service, &compressed_bytes).await.unwrap(); 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."; let expected = "Hello, world! This is a test of streaming compression.";
assert_eq!(String::from_utf8(decompressed).unwrap(), expected); assert_eq!(String::from_utf8(decompressed).unwrap(), expected);
} }
@@ -441,17 +441,17 @@ mod tests {
fn test_should_compress() { fn test_should_compress() {
let service = GzipCompressionService::new(); 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, "image/jpeg", 100 * 1024));
assert!(!CompressionService::should_compress(&service, "video/mp4", 10 * 1024 * 1024)); assert!(!CompressionService::should_compress(&service, "video/mp4", 10 * 1024 * 1024));
assert!(!CompressionService::should_compress(&service, "application/zip", 5 * 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, "text/html", 100 * 1024));
assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024)); assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024));
assert!(CompressionService::should_compress(&service, "text/plain", 1024 * 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)); assert!(!CompressionService::should_compress(&service, "text/html", 10 * 1024));
} }
} }
+122 -122
View File
@@ -13,61 +13,61 @@ use crate::domain::entities::file::File;
use crate::common::config::AppConfig; use crate::common::config::AppConfig;
/// Tipos de entradas en caché /// Cache entry types
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheEntryType { pub enum CacheEntryType {
/// Archivo /// File
File, File,
/// Directorio /// Directory
Directory, Directory,
/// Tipo desconocido /// Unknown type
Unknown, Unknown,
} }
/// Estadísticas de caché para monitoreo /// Cache statistics for monitoring
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct CacheStats { pub struct CacheStats {
/// Número de hits en caché /// Number of cache hits
pub hits: usize, pub hits: usize,
/// Número de misses en caché /// Number of cache misses
pub misses: usize, pub misses: usize,
/// Número de invalidaciones manuales /// Number of manual invalidations
pub invalidations: usize, pub invalidations: usize,
/// Número de expiraciones automáticas /// Number of automatic expirations
pub expirations: usize, pub expirations: usize,
/// Número de inserciones en caché /// Number of cache inserts
pub inserts: usize, pub inserts: usize,
/// Tiempo total ahorrado (milisegundos) /// Total time saved (milliseconds)
pub time_saved_ms: u64, pub time_saved_ms: u64,
} }
/// Metadatos completos de archivo en caché /// Complete cached file metadata
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileMetadata { pub struct FileMetadata {
/// Ruta absoluta del archivo /// Absolute file path
pub path: PathBuf, pub path: PathBuf,
/// Si el archivo existe físicamente /// Whether the file physically exists
pub exists: bool, pub exists: bool,
/// Tipo de entrada (archivo, directorio) /// Entry type (file, directory)
pub entry_type: CacheEntryType, pub entry_type: CacheEntryType,
/// Tamaño en bytes (para archivos) /// Size in bytes (for files)
pub size: Option<u64>, pub size: Option<u64>,
/// Tipo MIME (para archivos) /// MIME type (for files)
pub mime_type: Option<String>, pub mime_type: Option<String>,
/// Timestamp de creación (UNIX epoch seconds) /// Creation timestamp (UNIX epoch seconds)
pub created_at: Option<u64>, pub created_at: Option<u64>,
/// Timestamp de modificación (UNIX epoch seconds) /// Modification timestamp (UNIX epoch seconds)
pub modified_at: Option<u64>, pub modified_at: Option<u64>,
/// Acceso previo (usado para LRU) /// Previous access (used for LRU)
pub last_access: Instant, pub last_access: Instant,
/// Tiempo de expiración de la caché /// Cache expiration time
pub expires_at: Instant, pub expires_at: Instant,
/// Número de accesos a esta entrada /// Number of accesses to this entry
pub access_count: usize, pub access_count: usize,
} }
impl FileMetadata { impl FileMetadata {
/// Crea una nueva entrada de metadatos /// Creates a new metadata entry
pub fn new( pub fn new(
path: PathBuf, path: PathBuf,
exists: bool, exists: bool,
@@ -94,56 +94,56 @@ impl FileMetadata {
} }
} }
/// Actualiza el tiempo de último acceso /// Updates the last access time
pub fn touch(&mut self) { pub fn touch(&mut self) {
self.last_access = Instant::now(); self.last_access = Instant::now();
self.access_count += 1; self.access_count += 1;
} }
/// Verifica si la entrada ha expirado /// Checks if the entry has expired
pub fn is_expired(&self) -> bool { pub fn is_expired(&self) -> bool {
Instant::now() > self.expires_at 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) { pub fn update_expiry(&mut self, ttl: Duration) {
self.expires_at = Instant::now() + ttl; self.expires_at = Instant::now() + ttl;
} }
} }
/// Caché avanzada de metadatos de archivos /// Advanced file metadata cache
pub struct FileMetadataCache { pub struct FileMetadataCache {
/// Caché principal de metadatos /// Main metadata cache
metadata_cache: RwLock<HashMap<PathBuf, FileMetadata>>, metadata_cache: RwLock<HashMap<PathBuf, FileMetadata>>,
/// Cola LRU para administración de caché /// LRU queue for cache management
lru_queue: RwLock<VecDeque<PathBuf>>, lru_queue: RwLock<VecDeque<PathBuf>>,
/// Estadísticas de uso del caché /// Cache usage statistics
stats: RwLock<CacheStats>, stats: RwLock<CacheStats>,
/// Configuración global de la aplicación /// Global application configuration
config: AppConfig, config: AppConfig,
/// TTL adaptativo para entradas populares /// Adaptive TTL for popular entries
ttl_multiplier: f64, ttl_multiplier: f64,
/// Umbral de popularidad para TTL extendido /// Popularity threshold for extended TTL
popularity_threshold: usize, popularity_threshold: usize,
/// Tamaño máximo de caché /// Maximum cache size
max_entries: usize, max_entries: usize,
} }
impl FileMetadataCache { 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 { pub fn new(config: AppConfig, max_entries: usize) -> Self {
Self { Self {
metadata_cache: RwLock::new(HashMap::with_capacity(max_entries)), metadata_cache: RwLock::new(HashMap::with_capacity(max_entries)),
lru_queue: RwLock::new(VecDeque::with_capacity(max_entries)), lru_queue: RwLock::new(VecDeque::with_capacity(max_entries)),
stats: RwLock::new(CacheStats::default()), stats: RwLock::new(CacheStats::default()),
config, config,
ttl_multiplier: 5.0, // Entradas populares tienen 5x TTL ttl_multiplier: 5.0, // Popular entries have 5x TTL
popularity_threshold: 10, // Después de 10 accesos se considera popular popularity_threshold: 10, // After 10 accesses it's considered popular
max_entries, 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 { pub fn create_metadata_from_file(file: &File, abs_path: PathBuf) -> FileMetadata {
let entry_type = CacheEntryType::File; let entry_type = CacheEntryType::File;
let size = Some(file.size()); let size = Some(file.size());
@@ -151,8 +151,8 @@ impl FileMetadataCache {
let created_at = Some(file.created_at()); let created_at = Some(file.created_at());
let modified_at = Some(file.modified_at()); let modified_at = Some(file.modified_at());
// Usar un TTL estándar // Use a standard TTL
let ttl = Duration::from_secs(60); // 1 minuto let ttl = Duration::from_secs(60); // 1 minute
FileMetadata::new( FileMetadata::new(
abs_path, abs_path,
@@ -166,28 +166,28 @@ impl FileMetadataCache {
) )
} }
/// Crea una instancia por defecto /// Creates a default instance
pub fn default() -> Self { pub fn default() -> Self {
Self::new(AppConfig::default(), 10_000) 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 { 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<FileMetadata> { pub async fn get_metadata(&self, path: &Path) -> Option<FileMetadata> {
let start_time = Instant::now(); let start_time = Instant::now();
let mut cache = self.metadata_cache.write().await; let mut cache = self.metadata_cache.write().await;
if let Some(metadata) = cache.get_mut(path) { if let Some(metadata) = cache.get_mut(path) {
// Verificar si ha expirado // Check if expired
if metadata.is_expired() { if metadata.is_expired() {
// Eliminar de caché si expiró // Remove from cache if expired
cache.remove(path); cache.remove(path);
// Actualizar estadísticas // Update statistics
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.misses += 1; stats.misses += 1;
stats.expirations += 1; stats.expirations += 1;
@@ -197,10 +197,10 @@ impl FileMetadataCache {
return None; return None;
} }
// Actualizar tiempo de acceso // Update access time
metadata.touch(); metadata.touch();
// Para entradas populares, extender TTL // For popular entries, extend TTL
if metadata.access_count >= self.popularity_threshold { if metadata.access_count >= self.popularity_threshold {
let new_ttl = match metadata.entry_type { let new_ttl = match metadata.entry_type {
CacheEntryType::File => Duration::from_millis( CacheEntryType::File => Duration::from_millis(
@@ -209,33 +209,33 @@ impl FileMetadataCache {
CacheEntryType::Directory => Duration::from_millis( CacheEntryType::Directory => Duration::from_millis(
(self.config.timeouts.dir_operation_ms as f64 * self.ttl_multiplier) as u64 (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); metadata.update_expiry(new_ttl);
debug!("Extended TTL for popular entry: {}", path.display()); 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 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); let time_saved = estimated_io_time.saturating_sub(elapsed);
// Actualizar estadísticas // Update statistics
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.hits += 1; stats.hits += 1;
stats.time_saved_ms += time_saved; stats.time_saved_ms += time_saved;
debug!("Cache hit for: {}", path.display()); 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; self.update_lru(path.to_path_buf()).await;
// Clonar para retornar // Clone to return
return Some(metadata.clone()); return Some(metadata.clone());
} }
// No encontrado en caché // Not found in cache
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.misses += 1; stats.misses += 1;
@@ -243,20 +243,20 @@ impl FileMetadataCache {
None None
} }
/// Actualiza la cola LRU /// Updates the LRU queue
async fn update_lru(&self, path: PathBuf) { async fn update_lru(&self, path: PathBuf) {
let mut lru = self.lru_queue.write().await; 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) { if let Some(pos) = lru.iter().position(|p| p == &path) {
lru.remove(pos); lru.remove(pos);
} }
// Agregar al final (más reciente) // Add to the end (most recent)
lru.push_back(path); lru.push_back(path);
} }
/// Verifica si un archivo existe /// Checks if a file exists
pub async fn exists(&self, path: &Path) -> Option<bool> { pub async fn exists(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await { if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.exists); return Some(metadata.exists);
@@ -265,7 +265,7 @@ impl FileMetadataCache {
None None
} }
/// Verifica si un path es un directorio /// Checks if a path is a directory
pub async fn is_dir(&self, path: &Path) -> Option<bool> { pub async fn is_dir(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await { if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.entry_type == CacheEntryType::Directory); return Some(metadata.entry_type == CacheEntryType::Directory);
@@ -274,7 +274,7 @@ impl FileMetadataCache {
None None
} }
/// Verifica si un path es un archivo /// Checks if a path is a file
pub async fn is_file(&self, path: &Path) -> Option<bool> { pub async fn is_file(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await { if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.entry_type == CacheEntryType::File); return Some(metadata.entry_type == CacheEntryType::File);
@@ -283,7 +283,7 @@ impl FileMetadataCache {
None None
} }
/// Obtiene el tamaño de un archivo /// Gets the size of a file
pub async fn get_size(&self, path: &Path) -> Option<u64> { pub async fn get_size(&self, path: &Path) -> Option<u64> {
if let Some(metadata) = self.get_metadata(path).await { if let Some(metadata) = self.get_metadata(path).await {
return metadata.size; return metadata.size;
@@ -292,7 +292,7 @@ impl FileMetadataCache {
None 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<String> { pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
if let Some(metadata) = self.get_metadata(path).await { if let Some(metadata) = self.get_metadata(path).await {
return metadata.mime_type; return metadata.mime_type;
@@ -301,12 +301,12 @@ impl FileMetadataCache {
None None
} }
/// Refresca los metadatos de un path /// Refreshes metadata for a path
pub async fn refresh_metadata(&self, path: &Path) -> Result<FileMetadata, std::io::Error> { pub async fn refresh_metadata(&self, path: &Path) -> Result<FileMetadata, std::io::Error> {
// Realizar lectura real del sistema de archivos // Perform actual filesystem read
let metadata = fs::metadata(path).await?; let metadata = fs::metadata(path).await?;
// Determinar tipo de entrada // Determine entry type
let entry_type = if metadata.is_dir() { let entry_type = if metadata.is_dir() {
CacheEntryType::Directory CacheEntryType::Directory
} else if metadata.is_file() { } else if metadata.is_file() {
@@ -315,21 +315,21 @@ impl FileMetadataCache {
CacheEntryType::Unknown CacheEntryType::Unknown
}; };
// Obtener tamaño para archivos // Get size for files
let size = if metadata.is_file() { let size = if metadata.is_file() {
Some(metadata.len()) Some(metadata.len())
} else { } else {
None None
}; };
// Obtener tipo MIME para archivos // Get MIME type for files
let mime_type = if metadata.is_file() { let mime_type = if metadata.is_file() {
Some(from_path(path).first_or_octet_stream().to_string()) Some(from_path(path).first_or_octet_stream().to_string())
} else { } else {
None None
}; };
// Obtener timestamps // Get timestamps
let created_at = metadata.created() let created_at = metadata.created()
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()) .map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
.ok(); .ok();
@@ -338,14 +338,14 @@ impl FileMetadataCache {
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()) .map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
.ok(); .ok();
// Determinar TTL apropiado // Determine appropriate TTL
let ttl = if metadata.is_dir() { let ttl = if metadata.is_dir() {
Duration::from_millis(self.config.timeouts.dir_operation_ms) Duration::from_millis(self.config.timeouts.dir_operation_ms)
} else { } else {
Duration::from_millis(self.config.timeouts.file_operation_ms) Duration::from_millis(self.config.timeouts.file_operation_ms)
}; };
// Crear entrada de metadatos // Create metadata entry
let file_metadata = FileMetadata::new( let file_metadata = FileMetadata::new(
path.to_path_buf(), path.to_path_buf(),
true, true,
@@ -357,34 +357,34 @@ impl FileMetadataCache {
ttl, ttl,
); );
// Actualizar caché // Update cache
self.update_cache(file_metadata.clone()).await; self.update_cache(file_metadata.clone()).await;
Ok(file_metadata) Ok(file_metadata)
} }
/// Actualiza la caché con nuevos metadatos /// Updates the cache with new metadata
pub async fn update_cache(&self, metadata: FileMetadata) { pub async fn update_cache(&self, metadata: FileMetadata) {
// Evitar caché llena antes de insertar // Avoid full cache before inserting
self.ensure_capacity().await; self.ensure_capacity().await;
let path = metadata.path.clone(); let path = metadata.path.clone();
// Insertar en caché // Insert into cache
{ {
let mut cache = self.metadata_cache.write().await; let mut cache = self.metadata_cache.write().await;
cache.insert(path.clone(), metadata); cache.insert(path.clone(), metadata);
// Actualizar estadísticas // Update statistics
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.inserts += 1; stats.inserts += 1;
} }
// Actualizar la cola LRU // Update the LRU queue
self.update_lru(path).await; self.update_lru(path).await;
} }
/// Asegura que hay espacio en la caché /// Ensures there is space in the cache
async fn ensure_capacity(&self) { async fn ensure_capacity(&self) {
let cache_size = { let cache_size = {
let cache = self.metadata_cache.read().await; let cache = self.metadata_cache.read().await;
@@ -392,15 +392,15 @@ impl FileMetadataCache {
}; };
if cache_size >= self.max_entries { 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) { async fn evict_lru_entries(&self, count: usize) {
let mut paths_to_remove = Vec::with_capacity(count); 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; let mut lru = self.lru_queue.write().await;
for _ in 0..count { 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; let mut cache = self.metadata_cache.write().await;
for path in paths_to_remove { for path in paths_to_remove {
@@ -423,19 +423,19 @@ impl FileMetadataCache {
debug!("Evicted {} LRU entries from cache", count); 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) { 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; let mut cache = self.metadata_cache.write().await;
cache.remove(path); cache.remove(path);
// Actualizar estadísticas // Update statistics
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.invalidations += 1; stats.invalidations += 1;
} }
// Eliminar de la cola LRU // Remove from the LRU queue
let path_buf = path.to_path_buf(); let path_buf = path.to_path_buf();
{ {
let mut lru = self.lru_queue.write().await; let mut lru = self.lru_queue.write().await;
@@ -447,12 +447,12 @@ impl FileMetadataCache {
debug!("Invalidated cache entry for: {}", path.display()); 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) { pub async fn invalidate_directory(&self, dir_path: &Path) {
let dir_str = dir_path.to_string_lossy().to_string(); let dir_str = dir_path.to_string_lossy().to_string();
let mut paths_to_remove = Vec::new(); 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; let cache = self.metadata_cache.read().await;
for path in cache.keys() { for path in cache.keys() {
@@ -463,13 +463,13 @@ impl FileMetadataCache {
} }
} }
// Actualizar estadísticas // Update statistics
{ {
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.invalidations += paths_to_remove.len(); stats.invalidations += paths_to_remove.len();
} }
// Eliminar cada path encontrado // Remove each found path
for path in paths_to_remove { for path in paths_to_remove {
self.invalidate(&path).await; self.invalidate(&path).await;
} }
@@ -477,18 +477,18 @@ impl FileMetadataCache {
debug!("Invalidated directory and contents: {}", dir_path.display()); 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 { pub async fn get_stats(&self) -> CacheStats {
let stats = self.stats.read().await; let stats = self.stats.read().await;
stats.clone() stats.clone()
} }
/// Limpia todas las entradas expiradas de la caché /// Clears all expired entries from the cache
pub async fn clear_expired(&self) { pub async fn clear_expired(&self) {
let now = Instant::now(); let now = Instant::now();
let mut paths_to_remove = Vec::new(); let mut paths_to_remove = Vec::new();
// Encontrar entradas expiradas // Find expired entries
{ {
let cache = self.metadata_cache.read().await; let cache = self.metadata_cache.read().await;
for (path, metadata) in cache.iter() { for (path, metadata) in cache.iter() {
@@ -498,16 +498,16 @@ impl FileMetadataCache {
} }
} }
// Actualizar estadísticas // Update statistics
{ {
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.expirations += paths_to_remove.len(); 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(); let num_paths = paths_to_remove.len();
// Eliminar entradas expiradas // Remove expired entries
for path in paths_to_remove { for path in paths_to_remove {
self.invalidate(&path).await; self.invalidate(&path).await;
} }
@@ -515,19 +515,19 @@ impl FileMetadataCache {
debug!("Cleared {} expired entries from cache", num_paths); 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<Self>) -> BoxFuture<'static, ()> { pub fn start_cleanup_task(cache: Arc<Self>) -> BoxFuture<'static, ()> {
Box::pin(async move { Box::pin(async move {
let cleanup_interval = Duration::from_secs(60); // Cada minuto let cleanup_interval = Duration::from_secs(60); // Every minute
loop { loop {
// Esperar el intervalo // Wait for the interval
time::sleep(cleanup_interval).await; time::sleep(cleanup_interval).await;
// Limpiar entradas expiradas // Clean expired entries
cache.clear_expired().await; cache.clear_expired().await;
// Registrar estadísticas // Log statistics
let stats = cache.get_stats().await; let stats = cache.get_stats().await;
let cache_size = { let cache_size = {
let cache_map = cache.metadata_cache.read().await; 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<usize, std::io::Error> { pub async fn preload_directory(&self, dir_path: &Path, recursive: bool, max_depth: usize) -> Result<usize, std::io::Error> {
self._preload_directory_internal(dir_path, recursive, max_depth, 0).await 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( async fn _preload_directory_internal(
&self, &self,
dir_path: &Path, dir_path: &Path,
@@ -568,20 +568,20 @@ impl FileMetadataCache {
return Ok(0); return Ok(0);
} }
// Obtener entradas del directorio // Get directory entries
let mut entries = fs::read_dir(dir_path).await?; let mut entries = fs::read_dir(dir_path).await?;
let mut count = 0; let mut count = 0;
// Procesar cada entrada // Process each entry
while let Some(entry) = entries.next_entry().await? { while let Some(entry) = entries.next_entry().await? {
let path = entry.path(); let path = entry.path();
let metadata = fs::metadata(&path).await?; let metadata = fs::metadata(&path).await?;
// Refrescar metadatos de esta entrada // Refresh metadata for this entry
self.refresh_metadata(&path).await?; self.refresh_metadata(&path).await?;
count += 1; count += 1;
// Recursivamente procesar subdirectorios si es necesario // Recursively process subdirectories if needed
if recursive && metadata.is_dir() { if recursive && metadata.is_dir() {
// Box to break recursion // Box to break recursion
count += self._preload_directory_internal( count += self._preload_directory_internal(
@@ -657,37 +657,37 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_cache_operations() { async fn test_cache_operations() {
// Crear directorio temporal para pruebas // Create temporary directory for tests
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt"); 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(); let mut file = File::create(&file_path).await.unwrap();
file.write_all(b"test content").await.unwrap(); file.write_all(b"test content").await.unwrap();
file.flush().await.unwrap(); file.flush().await.unwrap();
drop(file); drop(file);
// Crear caché // Create cache
let config = AppConfig::default(); let config = AppConfig::default();
let cache = FileMetadataCache::new(config, 1000); let cache = FileMetadataCache::new(config, 1000);
// Verificar miss inicial // Verify initial miss
assert!(cache.exists(&file_path).await.is_none()); 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(); let metadata = cache.refresh_metadata(&file_path).await.unwrap();
assert_eq!(metadata.entry_type, CacheEntryType::File); assert_eq!(metadata.entry_type, CacheEntryType::File);
assert_eq!(metadata.size, Some(12)); // "test content" = 12 bytes 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.exists(&file_path).await, Some(true));
assert_eq!(cache.is_file(&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; cache.invalidate(&file_path).await;
assert!(cache.exists(&file_path).await.is_none()); assert!(cache.exists(&file_path).await.is_none());
// Verificar estadísticas // Verify statistics
let stats = cache.get_stats().await; let stats = cache.get_stats().await;
assert_eq!(stats.inserts, 1); assert_eq!(stats.inserts, 1);
assert_eq!(stats.invalidations, 1); assert_eq!(stats.invalidations, 1);
@@ -696,7 +696,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_directory_operations() { async fn test_directory_operations() {
// Crear estructura de directorios para pruebas // Create directory structure for tests
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
// Canonicalize to handle macOS /var -> /private/var symlinks // Canonicalize to handle macOS /var -> /private/var symlinks
let base_path = temp_dir.path().canonicalize().unwrap(); let base_path = temp_dir.path().canonicalize().unwrap();
@@ -709,24 +709,24 @@ mod tests {
File::create(&file1).await.unwrap(); File::create(&file1).await.unwrap();
File::create(&file2).await.unwrap(); File::create(&file2).await.unwrap();
// Crear caché // Create cache
let config = AppConfig::default(); let config = AppConfig::default();
let cache = FileMetadataCache::new(config, 1000); let cache = FileMetadataCache::new(config, 1000);
// Precargar directorio recursivamente // Preload directory recursively
// preload_directory caches the *contents* of the directory, not the root itself // preload_directory caches the *contents* of the directory, not the root itself
let count = cache.preload_directory(&base_path, true, 2).await.unwrap(); let count = cache.preload_directory(&base_path, true, 2).await.unwrap();
assert_eq!(count, 3); // subdir, file1, file2 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_dir(&sub_dir).await, Some(true));
assert_eq!(cache.is_file(&file1).await, Some(true)); assert_eq!(cache.is_file(&file1).await, Some(true));
assert_eq!(cache.is_file(&file2).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; 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(&sub_dir).await.is_none());
assert!(cache.exists(&file1).await.is_none()); assert!(cache.exists(&file1).await.is_none());
assert!(cache.exists(&file2).await.is_none()); assert!(cache.exists(&file2).await.is_none());
@@ -179,7 +179,7 @@ impl IdMappingOptimizer {
{ {
let cache = self.path_to_id_cache.read().await; let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) { if let Some((id, _)) = cache.get(&path_str) {
// Actualizar estadísticas // Update statistics
{ {
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.get_id_hits += 1; 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; let mut batch_queue = self.pending_batch.lock().await;
batch_queue.path_to_id_requests.insert(path_str); 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) Ok(None)
} }
/// Procesa las solicitudes pendientes en batch /// Processes pending requests in batch
async fn process_batch(&self) -> Result<BatchResult, IdMappingError> { async fn process_batch(&self) -> Result<BatchResult, IdMappingError> {
// Adquirir permiso para operación batch // Acquire permit for batch operation
let _permit = self.batch_limiter.acquire().await.unwrap(); let _permit = self.batch_limiter.acquire().await.unwrap();
// Get pending requests // Get pending requests
@@ -214,13 +214,13 @@ impl IdMappingOptimizer {
(paths, ids) (paths, ids)
}; };
// Crear resultados // Create results
let mut result = BatchResult { let mut result = BatchResult {
path_to_id: HashMap::with_capacity(path_requests.len()), path_to_id: HashMap::with_capacity(path_requests.len()),
id_to_path: HashMap::with_capacity(id_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 { for path_str in path_requests {
let path = StoragePath::from_string(&path_str); let path = StoragePath::from_string(&path_str);
match self.base_service.get_or_create_id(&path).await { match self.base_service.get_or_create_id(&path).await {
@@ -230,12 +230,12 @@ impl IdMappingOptimizer {
}, },
Err(e) => { Err(e) => {
error!("Error batch-processing path {}: {}", path_str, 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 { for id in id_requests {
match self.base_service.get_path_by_id(&id).await { match self.base_service.get_path_by_id(&id).await {
Ok(path) => { Ok(path) => {
@@ -245,7 +245,7 @@ impl IdMappingOptimizer {
}, },
Err(e) => { Err(e) => {
error!("Error batch-processing ID {}: {}", id, 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; let mut stats = self.stats.write().await;
stats.batch_operations += 1; stats.batch_operations += 1;
stats.batch_items_processed += result.path_to_id.len() + result.id_to_path.len(); 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(); let service_clone = self.base_service.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = service_clone.save_pending_changes().await { if let Err(e) = service_clone.save_pending_changes().await {
@@ -284,15 +284,15 @@ impl IdMappingOptimizer {
Ok(result) 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> { 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 should_process = {
let batch_queue = self.pending_batch.lock().await; 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 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 { if should_process {
self.process_batch().await?; self.process_batch().await?;
} }
@@ -300,17 +300,17 @@ impl IdMappingOptimizer {
Ok(()) 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<StoragePath>) -> Result<(), IdMappingError> { pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
// Solo proceder si hay rutas para cargar // Only proceed if there are paths to load
if paths.is_empty() { if paths.is_empty() {
return Ok(()); 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(); let mut paths_to_load = Vec::new();
// Verificar primero el caché // Check cache first
{ {
let cache = self.path_to_id_cache.read().await; let cache = self.path_to_id_cache.read().await;
for path in paths { 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() { if paths_to_load.is_empty() {
return Ok(()); 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; let mut batch_queue = self.pending_batch.lock().await;
for path in paths_to_load { for path in paths_to_load {
@@ -334,23 +334,23 @@ impl IdMappingOptimizer {
} }
} }
// Ejecutar procesamiento batch inmediatamente // Execute batch processing immediately
self.process_batch().await?; self.process_batch().await?;
Ok(()) 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<String>) -> Result<(), IdMappingError> { pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
// Solo proceder si hay IDs para cargar // Only proceed if there are IDs to load
if ids.is_empty() { if ids.is_empty() {
return Ok(()); 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(); let mut ids_to_load = Vec::new();
// Verificar primero el caché // Check cache first
{ {
let cache = self.id_to_path_cache.read().await; let cache = self.id_to_path_cache.read().await;
for id in ids { 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() { if ids_to_load.is_empty() {
return Ok(()); 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; let mut batch_queue = self.pending_batch.lock().await;
for id in ids_to_load { for id in ids_to_load {
@@ -373,7 +373,7 @@ impl IdMappingOptimizer {
} }
} }
// Ejecutar procesamiento batch inmediatamente // Execute batch processing immediately
self.process_batch().await?; self.process_batch().await?;
Ok(()) Ok(())
@@ -383,7 +383,7 @@ impl IdMappingOptimizer {
#[async_trait] #[async_trait]
impl IdMappingPort for IdMappingOptimizer { impl IdMappingPort for IdMappingOptimizer {
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> { async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
// Actualizar estadísticas // Update statistics
{ {
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.get_id_queries += 1; stats.get_id_queries += 1;
@@ -391,7 +391,7 @@ impl IdMappingPort for IdMappingOptimizer {
let path_str = path.to_string(); let path_str = path.to_string();
// Verificar primero en el caché // Check cache first
{ {
let cache = self.path_to_id_cache.read().await; let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) { 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<StoragePath, DomainError> { async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
// Actualizar estadísticas // Update statistics
{ {
let mut stats = self.stats.write().await; let mut stats = self.stats.write().await;
stats.path_by_id_queries += 1; 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> { 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 id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_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) { if let Some((old_path, _)) = id_cache.remove(id) {
path_cache.remove(&old_path); 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?; 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 id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_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> { 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 id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_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) { if let Some((path, _)) = id_cache.remove(id) {
path_cache.remove(&path); path_cache.remove(&path);
} }
} }
// Eliminar en el servicio base // Remove from the base service
self.base_service.remove_id(id).await?; self.base_service.remove_id(id).await?;
Ok(()) Ok(())
} }
async fn save_changes(&self) -> Result<(), DomainError> { async fn save_changes(&self) -> Result<(), DomainError> {
// Delegar al servicio base // Delegate to the base service
self.base_service.save_changes().await?; self.base_service.save_changes().await?;
Ok(()) Ok(())
@@ -569,15 +569,15 @@ mod tests {
let path = StoragePath::from_string("/test/file.txt"); 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(); let id = optimizer.get_or_create_id(&path).await.unwrap();
assert!(!id.is_empty(), "ID should not be empty"); 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(); let id2 = optimizer.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Same path should return same ID"); assert_eq!(id, id2, "Same path should return same ID");
// Verificar estadísticas de caché // Verify cache statistics
let stats = optimizer.get_stats().await; let stats = optimizer.get_stats().await;
assert_eq!(stats.get_id_queries, 2, "Should have 2 queries"); assert_eq!(stats.get_id_queries, 2, "Should have 2 queries");
assert_eq!(stats.get_id_hits, 1, "Should have 1 hit"); assert_eq!(stats.get_id_hits, 1, "Should have 1 hit");
@@ -587,27 +587,27 @@ mod tests {
async fn test_batch_processing() { async fn test_batch_processing() {
let (_, optimizer) = create_test_service().await; let (_, optimizer) = create_test_service().await;
// Crear un lote de rutas // Create a batch of paths
let mut paths = Vec::new(); let mut paths = Vec::new();
for i in 0..50 { for i in 0..50 {
paths.push(StoragePath::from_string(&format!("/test/batch/file{}.txt", i))); paths.push(StoragePath::from_string(&format!("/test/batch/file{}.txt", i)));
} }
// Precargar las rutas // Preload the paths
optimizer.preload_paths(paths.clone()).await.unwrap(); optimizer.preload_paths(paths.clone()).await.unwrap();
// Verificar que todas están en caché // Verify all are in cache
for path in &paths { for path in &paths {
let id = optimizer.get_or_create_id(path).await.unwrap(); let id = optimizer.get_or_create_id(path).await.unwrap();
assert!(!id.is_empty(), "ID should be available for path"); assert!(!id.is_empty(), "ID should be available for path");
} }
// Verificar estadísticas // Verify statistics
let stats = optimizer.get_stats().await; let stats = optimizer.get_stats().await;
assert_eq!(stats.batch_operations, 1, "Should have 1 batch operation"); assert_eq!(stats.batch_operations, 1, "Should have 1 batch operation");
assert!(stats.batch_items_processed >= 50, "Should have processed at least 50 items"); 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"); 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() { async fn test_cache_cleanup() {
let (_, optimizer) = create_test_service().await; let (_, optimizer) = create_test_service().await;
// Crear algunas entradas // Create some entries
let path = StoragePath::from_string("/test/cleanup.txt"); let path = StoragePath::from_string("/test/cleanup.txt");
let id = optimizer.get_or_create_id(&path).await.unwrap(); let id = optimizer.get_or_create_id(&path).await.unwrap();
// Verificar estadísticas iniciales // Verify initial statistics
{ {
let stats = optimizer.get_stats().await; let stats = optimizer.get_stats().await;
assert_eq!(stats.get_id_queries, 1, "Should have 1 query"); assert_eq!(stats.get_id_queries, 1, "Should have 1 query");
assert_eq!(stats.get_id_hits, 0, "Should have 0 hits"); 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; 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(); let id2 = optimizer.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Cache should still work after cleanup"); assert_eq!(id, id2, "Cache should still work after cleanup");
@@ -12,7 +12,7 @@ use crate::common::errors::{DomainError, ErrorKind};
use crate::application::ports::outbound::IdMappingPort; use crate::application::ports::outbound::IdMappingPort;
use crate::common::config::TimeoutConfig; 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)] #[derive(Debug, thiserror::Error)]
pub enum IdMappingError { pub enum IdMappingError {
#[error("ID not found: {0}")] #[error("ID not found: {0}")]
@@ -31,7 +31,7 @@ pub enum IdMappingError {
Other(String), Other(String),
} }
// Implementar conversión de IdMappingError a DomainError // Implement conversion from IdMappingError to DomainError
impl From<IdMappingError> for DomainError { impl From<IdMappingError> for DomainError {
fn from(err: IdMappingError) -> Self { fn from(err: IdMappingError) -> Self {
match err { match err {
@@ -59,25 +59,25 @@ impl From<IdMappingError> for DomainError {
} }
} }
/// Estructura para almacenar IDs mapeados a sus rutas /// Structure to store IDs mapped to their paths
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default)]
struct IdMap { struct IdMap {
path_to_id: HashMap<String, String>, path_to_id: HashMap<String, String>,
id_to_path: HashMap<String, String>, // Campo para búsqueda bidireccional eficiente id_to_path: HashMap<String, String>, // Field for efficient bidirectional lookup
version: u32, // Versión para detectar cambios 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 { pub struct IdMappingService {
map_path: PathBuf, map_path: PathBuf,
id_map: RwLock<IdMap>, id_map: RwLock<IdMap>,
save_mutex: Mutex<()>, // Para evitar múltiples guardados concurrentes save_mutex: Mutex<()>, // To prevent multiple concurrent saves
timeouts: TimeoutConfig, timeouts: TimeoutConfig,
pending_save: RwLock<bool>, // Indica si hay cambios pendientes pending_save: RwLock<bool>, // Indicates if there are pending changes
} }
impl IdMappingService { impl IdMappingService {
/// Crea un nuevo servicio de mapeo de IDs /// Creates a new ID mapping service
pub async fn new(map_path: PathBuf) -> Result<Self, DomainError> { pub async fn new(map_path: PathBuf) -> Result<Self, DomainError> {
let timeouts = TimeoutConfig::default(); let timeouts = TimeoutConfig::default();
let id_map = Self::load_id_map(&map_path, &timeouts).await?; 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 /// Similar functionality as new_in_memory but with a simpler signature for dummy use
pub fn dummy() -> Self { 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 { pub fn new_in_memory() -> Self {
Self { Self {
map_path: PathBuf::from("memory"), 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<IdMap, DomainError> { async fn load_id_map(map_path: &PathBuf, timeouts: &TimeoutConfig) -> Result<IdMap, DomainError> {
if map_path.exists() { 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( let read_result = time::timeout(
timeouts.lock_timeout(), timeouts.lock_timeout(),
fs::read_to_string(map_path) 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)))?; 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::<IdMap>(&content) { match serde_json::from_str::<IdMap>(&content) {
Ok(mut map) => { 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() { if map.id_to_path.is_empty() && !map.path_to_id.is_empty() {
let mut rebuild_count = 0; let mut rebuild_count = 0;
for (path, id) in &map.path_to_id { for (path, id) in &map.path_to_id {
@@ -146,7 +146,7 @@ impl IdMappingService {
}, },
Err(e) => { Err(e) => {
tracing::error!("Error parsing ID map: {}", 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"); let backup_path = map_path.with_extension("json.bak");
if let Err(copy_err) = tokio::fs::copy(map_path, &backup_path).await { if let Err(copy_err) = tokio::fs::copy(map_path, &backup_path).await {
tracing::error!("Failed to backup corrupted map file: {}", copy_err); tracing::error!("Failed to backup corrupted map file: {}", copy_err);
@@ -158,18 +158,18 @@ impl IdMappingService {
return Ok(IdMap { return Ok(IdMap {
path_to_id: HashMap::new(), path_to_id: HashMap::new(),
id_to_path: 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"); tracing::info!("No existing ID map found, creating new empty map");
let empty_map = IdMap { let empty_map = IdMap {
path_to_id: HashMap::new(), path_to_id: HashMap::new(),
id_to_path: HashMap::new(), id_to_path: HashMap::new(),
version: 1, // Iniciar con versión 1 version: 1, // Start with version 1
}; };
// Ensure directory exists // Ensure directory exists
@@ -198,16 +198,16 @@ impl IdMappingService {
Ok(empty_map) 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> { async fn save_id_map(&self) -> Result<(), DomainError> {
// Adquirir bloqueo exclusivo para salvar // Acquire exclusive lock for saving
let _lock = time::timeout( let _lock = time::timeout(
self.timeouts.lock_timeout(), self.timeouts.lock_timeout(),
self.save_mutex.lock() self.save_mutex.lock()
).await ).await
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring save lock for ID mapping"))?; .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 json = {
let mut map = time::timeout( let mut map = time::timeout(
self.timeouts.lock_timeout(), self.timeouts.lock_timeout(),
@@ -215,7 +215,7 @@ impl IdMappingService {
).await ).await
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring write lock for ID mapping"))?; .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; let pending = *self.pending_save.read().await;
if pending { if pending {
map.version += 1; 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)))? .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"); let temp_path = self.map_path.with_extension("json.tmp");
fs::write(&temp_path, &json).await 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)))?; .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 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)))?; .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; let mut pending = self.pending_save.write().await;
*pending = false; *pending = false;
@@ -246,22 +246,22 @@ impl IdMappingService {
Ok(()) Ok(())
} }
/// Genera un ID único /// Generates a unique ID
fn generate_id(&self) -> String { fn generate_id(&self) -> String {
Uuid::new_v4().to_string() Uuid::new_v4().to_string()
} }
/// Marca cambios como pendientes /// Marks changes as pending
async fn mark_pending(&self) { async fn mark_pending(&self) {
let mut pending = self.pending_save.write().await; let mut pending = self.pending_save.write().await;
*pending = true; *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<String, IdMappingError> { pub async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, IdMappingError> {
let path_str = path.to_string(); 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( let read_result = match time::timeout(
self.timeouts.lock_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( let write_result = match time::timeout(
self.timeouts.lock_timeout(), self.timeouts.lock_timeout(),
self.id_map.write() self.id_map.write()
@@ -287,18 +287,18 @@ impl IdMappingService {
let mut map = write_result; 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) { if let Some(id) = map.path_to_id.get(&path_str) {
return Ok(id.clone()); return Ok(id.clone());
} }
// Generar un nuevo ID y almacenarlo // Generate a new ID and store it
let id = self.generate_id(); let id = self.generate_id();
map.path_to_id.insert(path_str.clone(), id.clone()); map.path_to_id.insert(path_str.clone(), id.clone());
map.id_to_path.insert(id.clone(), path_str); map.id_to_path.insert(id.clone(), path_str);
// Marcar como pendiente para guardar // Mark as pending for saving
drop(map); // Liberar el write lock antes de adquirir otro drop(map); // Release the write lock before acquiring another
self.mark_pending().await; self.mark_pending().await;
tracing::debug!("Created new ID mapping: {} -> {}", path.to_string(), id); tracing::debug!("Created new ID mapping: {} -> {}", path.to_string(), id);
@@ -306,7 +306,7 @@ impl IdMappingService {
Ok(id) 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<StoragePath, IdMappingError> { pub async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, IdMappingError> {
let read_result = match time::timeout( let read_result = match time::timeout(
self.timeouts.lock_timeout(), self.timeouts.lock_timeout(),
@@ -323,7 +323,7 @@ impl IdMappingService {
Err(IdMappingError::NotFound(id.to_string())) 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> { pub async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), IdMappingError> {
let write_result = match time::timeout( let write_result = match time::timeout(
self.timeouts.lock_timeout(), self.timeouts.lock_timeout(),
@@ -335,17 +335,17 @@ impl IdMappingService {
let mut map = write_result; 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() { if let Some(old_path) = map.id_to_path.get(id).cloned() {
map.path_to_id.remove(&old_path); map.path_to_id.remove(&old_path);
// Registrar la nueva ruta // Register the new path
let new_path_str = new_path.to_string(); let new_path_str = new_path.to_string();
map.path_to_id.insert(new_path_str.clone(), id.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); map.id_to_path.insert(id.to_string(), new_path_str);
// Marcar como pendiente // Mark as pending
drop(map); // Liberar el write lock antes de adquirir otro drop(map); // Release the write lock before acquiring another
self.mark_pending().await; self.mark_pending().await;
tracing::debug!("Updated path mapping for ID {}: {} -> {}", 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> { pub async fn remove_id(&self, id: &str) -> Result<(), IdMappingError> {
let write_result = match time::timeout( let write_result = match time::timeout(
self.timeouts.lock_timeout(), self.timeouts.lock_timeout(),
@@ -369,12 +369,12 @@ impl IdMappingService {
let mut map = write_result; 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) { if let Some(path) = map.id_to_path.remove(id) {
map.path_to_id.remove(&path); map.path_to_id.remove(&path);
// Marcar como pendiente // Mark as pending
drop(map); // Liberar el write lock antes de adquirir otro drop(map); // Release the write lock before acquiring another
self.mark_pending().await; self.mark_pending().await;
tracing::debug!("Removed ID mapping: {} -> {}", id, path); 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> { 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; let pending = self.pending_save.read().await;
if !*pending { 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 { match self.save_id_map().await {
Ok(_) => { Ok(_) => {
tracing::info!("ID mappings saved successfully to disk at {}", self.map_path.display()); 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) { match std::fs::metadata(&self.map_path) {
Ok(metadata) => { Ok(metadata) => {
if metadata.len() > 0 { if metadata.len() > 0 {
@@ -410,7 +410,7 @@ impl IdMappingService {
}, },
Err(e) => { Err(e) => {
tracing::error!("Failed to verify saved map file: {}", 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 { if let Err(retry_err) = self.save_id_map().await {
tracing::error!("Second save attempt also failed: {}", retry_err); tracing::error!("Second save attempt also failed: {}", retry_err);
return Err(IdMappingError::IoError(std::io::Error::new( return Err(IdMappingError::IoError(std::io::Error::new(
@@ -426,7 +426,7 @@ impl IdMappingService {
}, },
Err(e) => { Err(e) => {
tracing::error!("Failed to save ID map to {}: {}", self.map_path.display(), 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; tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
match self.save_id_map().await { match self.save_id_map().await {
Ok(_) => { Ok(_) => {
@@ -448,31 +448,31 @@ impl IdMappingService {
#[async_trait] #[async_trait]
impl IdMappingPort for IdMappingService { 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<String, DomainError> { async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
self.get_or_create_id(path).await 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))) .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<StoragePath, DomainError> { async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
self.get_path_by_id(id).await self.get_path_by_id(id).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get path for ID: {}: {}", id, e))) .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> { async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
self.update_path(id, new_path).await 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))) .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> { async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
self.remove_id(id).await self.remove_id(id).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to remove ID: {}: {}", id, e))) .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> { async fn save_changes(&self) -> Result<(), DomainError> {
self.save_pending_changes().await self.save_pending_changes().await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to save pending ID mapping changes: {}", e))) .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 // 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 /// Synchronous helper for contexts where we can't use async
impl IdMappingService { impl IdMappingService {
/// Create a new service synchronously (only for stubs and initialization) /// Create a new service synchronously (only for stubs and initialization)
@@ -499,13 +499,13 @@ impl IdMappingService {
impl Clone for IdMappingService { impl Clone for IdMappingService {
fn clone(&self) -> Self { fn clone(&self) -> Self {
// No podemos clonar directamente los RwLock/Mutex, // We cannot directly clone the RwLock/Mutex,
// pero podemos crear nuevas instancias que apunten al mismo Arc interno // but we can create new instances that point to the same internal Arc
// Sin embargo, en este caso simplemente necesitamos la map_path // However, in this case we simply need the map_path
Self { Self {
map_path: self.map_path.clone(), map_path: self.map_path.clone(),
id_map: RwLock::new(IdMap::default()), // Esto no se usa en el task asíncrono id_map: RwLock::new(IdMap::default()), // This is not used in the async task
save_mutex: Mutex::new(()), // Esto tampoco save_mutex: Mutex::new(()), // Neither is this
timeouts: self.timeouts.clone(), timeouts: self.timeouts.clone(),
pending_save: RwLock::new(false), pending_save: RwLock::new(false),
} }
@@ -530,7 +530,7 @@ mod tests {
assert!(!id.is_empty(), "ID should not be empty"); 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(); let id2 = service.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Same path should return same ID"); assert_eq!(id, id2, "Same path should return same ID");
} }
@@ -557,7 +557,7 @@ mod tests {
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
let map_path = temp_dir.path().join("id_map.json"); 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 service = IdMappingService::new(map_path.clone()).await.unwrap();
let path1 = StoragePath::from_string("/test/file1.txt"); 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 id1 = service.get_or_create_id(&path1).await.unwrap();
let id2 = service.get_or_create_id(&path2).await.unwrap(); let id2 = service.get_or_create_id(&path2).await.unwrap();
// Guardar cambios // Save changes
service.save_pending_changes().await.unwrap(); 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; 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(); 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_id1 = service2.get_or_create_id(&path1).await.unwrap();
let loaded_id2 = service2.get_or_create_id(&path2).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()); 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(); let mut tasks = Vec::new();
for i in 0..100 { for i in 0..100 {
let path = StoragePath::from_string(&format!("/test/concurrent/file{}.txt", i)); 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; let results = join_all(tasks).await;
// Verificar que todas tuvieron éxito // Verify that all succeeded
for result in results { for result in results {
assert!(result.unwrap().is_ok(), "Concurrent operations should succeed"); assert!(result.unwrap().is_ok(), "Concurrent operations should succeed");
} }
// Guardar cambios // Save changes
service.save_pending_changes().await.unwrap(); service.save_pending_changes().await.unwrap();
} }
} }
+3 -3
View File
@@ -110,7 +110,7 @@ impl TokenServicePort for JwtTokenService {
DomainError::new( DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"TokenService", "TokenService",
format!("Error al generar token: {}", e) format!("Error generating token: {}", e)
) )
}) })
} }
@@ -126,12 +126,12 @@ impl TokenServicePort for JwtTokenService {
.map_err(|e| { .map_err(|e| {
match e.kind() { match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => { jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado") DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expired")
}, },
_ => DomainError::new( _ => DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"TokenService", "TokenService",
format!("Token inválido: {}", e) format!("Invalid token: {}", e)
), ),
} }
})?; })?;
+18 -18
View File
@@ -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 //! This service was moved from domain/services because it implements application traits
//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs). //! (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 std::path::{Path, PathBuf};
use async_trait::async_trait; 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::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath; 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 { pub struct PathService {
root_path: PathBuf, root_path: PathBuf,
} }
impl PathService { 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 { pub fn new(root_path: PathBuf) -> Self {
Self { root_path } 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 { pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
let mut path = self.root_path.clone(); let mut path = self.root_path.clone();
for segment in storage_path.segments() { for segment in storage_path.segments() {
@@ -35,7 +35,7 @@ impl PathService {
path 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<StoragePath> { pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| { physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
let segments: Vec<String> = rel_path let segments: Vec<String> = 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 { pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
folder_path.join(file_name) 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 { pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
if let Some(child_parent) = potential_child.parent() { if let Some(child_parent) = potential_child.parent() {
&child_parent == parent_path &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 { pub fn is_in_root(&self, path: &StoragePath) -> bool {
path.parent().map_or(true, |p| p.is_empty()) path.parent().map_or(true, |p| p.is_empty())
} }
@@ -73,9 +73,9 @@ impl PathService {
&self.root_path &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> { 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()) { if path.segments().iter().any(|s| s.is_empty()) {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
@@ -84,7 +84,7 @@ impl PathService {
)); ));
} }
// Verificar que no haya caracteres peligrosos // Check for dangerous characters
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|']; let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
for segment in path.segments() { for segment in path.segments() {
if segment.contains(&dangerous_chars[..]) { 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" { if segment.starts_with('.') && segment != ".well-known" {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
@@ -120,13 +120,13 @@ impl StoragePort for PathService {
} }
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> { async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
// Primero validar la ruta // First validate the path
self.validate_path(storage_path)?; self.validate_path(storage_path)?;
// Resolver a ruta física // Resolve to physical path
let physical_path = self.resolve_path(storage_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() { if !physical_path.exists() {
fs::create_dir_all(&physical_path).await fs::create_dir_all(&physical_path).await
.map_err(|e| DomainError::new( .map_err(|e| DomainError::new(
@@ -7,7 +7,7 @@ use crate::common::errors::Result;
use crate::domain::repositories::trash_repository::TrashRepository; use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::ports::trash_ports::TrashUseCase; 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 { pub struct TrashCleanupService {
trash_service: Arc<dyn TrashUseCase>, trash_service: Arc<dyn TrashUseCase>,
trash_repository: Arc<dyn TrashRepository>, trash_repository: Arc<dyn TrashRepository>,
@@ -23,75 +23,75 @@ impl TrashCleanupService {
Self { Self {
trash_service, trash_service,
trash_repository, 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))] #[instrument(skip(self))]
pub async fn start_cleanup_job(&self) { pub async fn start_cleanup_job(&self) {
let trash_repository = self.trash_repository.clone(); let trash_repository = self.trash_repository.clone();
let trash_service = self.trash_service.clone(); let trash_service = self.trash_service.clone();
let interval_hours = self.cleanup_interval_hours; 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 { tokio::spawn(async move {
let interval_duration = Duration::from_secs(interval_hours * 60 * 60); let interval_duration = Duration::from_secs(interval_hours * 60 * 60);
let mut interval = time::interval(interval_duration); 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 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 { loop {
interval.tick().await; 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( if let Err(e) = Self::cleanup_expired_items(
trash_repository.clone(), trash_repository.clone(),
trash_service.clone() trash_service.clone()
).await { ).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))] #[instrument(skip(trash_repository, trash_service))]
async fn cleanup_expired_items( async fn cleanup_expired_items(
trash_repository: Arc<dyn TrashRepository>, trash_repository: Arc<dyn TrashRepository>,
trash_service: Arc<dyn TrashUseCase>, trash_service: Arc<dyn TrashUseCase>,
) -> Result<()> { ) -> 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?; let expired_items = trash_repository.get_expired_items().await?;
if expired_items.is_empty() { if expired_items.is_empty() {
debug!("No hay elementos expirados para limpiar"); debug!("No expired items to clean up");
return Ok(()); 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 { for item in expired_items {
let trash_id = item.id().to_string(); let trash_id = item.id().to_string();
let user_id = item.user_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 { 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 { } 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(()) Ok(())
} }
} }
+47 -47
View File
@@ -13,47 +13,47 @@ use crate::{
}; };
use std::sync::Arc; use std::sync::Arc;
/// Error relacionado con la creación de archivos ZIP /// Error related to ZIP file creation
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ZipError { pub enum ZipError {
#[error("Error de IO: {0}")] #[error("IO error: {0}")]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("Error de ZIP: {0}")] #[error("ZIP error: {0}")]
ZipError(#[from] zip::result::ZipError), ZipError(#[from] zip::result::ZipError),
#[error("Error al leer el archivo: {0}")] #[error("Error reading file: {0}")]
FileReadError(String), FileReadError(String),
#[error("Error al obtener contenido de carpeta: {0}")] #[error("Error getting folder contents: {0}")]
FolderContentsError(String), FolderContentsError(String),
#[error("Carpeta no encontrada: {0}")] #[error("Folder not found: {0}")]
FolderNotFound(String), FolderNotFound(String),
} }
// Implementar From<ZipError> para DomainError para permitir el uso de ? // Implement From<ZipError> for DomainError to allow the use of ?
impl From<ZipError> for DomainError { impl From<ZipError> for DomainError {
fn from(err: ZipError) -> Self { fn from(err: ZipError) -> Self {
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string()) DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
} }
} }
// Implementar From<zip::result::ZipError> para DomainError directamente // Implement From<zip::result::ZipError> for DomainError directly
impl From<zip::result::ZipError> for DomainError { impl From<zip::result::ZipError> for DomainError {
fn from(err: zip::result::ZipError) -> Self { fn from(err: zip::result::ZipError) -> Self {
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string()) DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
} }
} }
/// Servicio para crear archivos ZIP /// Service for creating ZIP files
pub struct ZipService { pub struct ZipService {
file_service: Arc<dyn FileRetrievalUseCase>, file_service: Arc<dyn FileRetrievalUseCase>,
folder_service: Arc<dyn FolderUseCase>, folder_service: Arc<dyn FolderUseCase>,
} }
impl ZipService { 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<dyn FileRetrievalUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self { pub fn new(file_service: Arc<dyn FileRetrievalUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
Self { Self {
file_service, file_service,
@@ -61,33 +61,33 @@ impl ZipService {
} }
} }
/// Crea un archivo ZIP con el contenido de una carpeta y todas sus subcarpetas /// Creates a ZIP file with the contents of a folder and all its subfolders
/// Retorna los bytes del ZIP /// Returns the ZIP bytes
pub async fn create_folder_zip(&self, folder_id: &str, folder_name: &str) -> Result<Vec<u8>> { pub async fn create_folder_zip(&self, folder_id: &str, folder_name: &str) -> Result<Vec<u8>> {
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 { let folder = match self.folder_service.get_folder(folder_id).await {
Ok(folder) => folder, Ok(folder) => folder,
Err(e) => { 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()); 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 buf = Cursor::new(Vec::new());
let mut zip = ZipWriter::new(buf); let mut zip = ZipWriter::new(buf);
// Establecer opciones de compresión // Set compression options
let options = SimpleFileOptions::default() let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated) .compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755); .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(); 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( self.process_folder_recursively(
&mut zip, &mut zip,
&folder, &folder,
@@ -96,20 +96,20 @@ impl ZipService {
&mut processed_folders &mut processed_folders
).await?; ).await?;
// Finalizar el ZIP y obtener los bytes // Finalize the ZIP and get the bytes
let mut zip_buf = zip.finish()?; let mut zip_buf = zip.finish()?;
let mut bytes = Vec::new(); let mut bytes = Vec::new();
match zip_buf.read_to_end(&mut bytes) { match zip_buf.read_to_end(&mut bytes) {
Ok(_) => Ok(bytes), Ok(_) => Ok(bytes),
Err(e) => { Err(e) => {
error!("Error al leer ZIP finalizado: {}", e); error!("Error reading finalized ZIP: {}", e);
Err(ZipError::IoError(e).into()) 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( async fn process_folder_recursively(
&self, &self,
zip: &mut ZipWriter<Cursor<Vec<u8>>>, zip: &mut ZipWriter<Cursor<Vec<u8>>>,
@@ -118,63 +118,63 @@ impl ZipService {
options: &SimpleFileOptions, options: &SimpleFileOptions,
processed_folders: &mut std::collections::HashSet<String> processed_folders: &mut std::collections::HashSet<String>
) -> Result<()> { ) -> Result<()> {
// Estructura para representar el trabajo pendiente // Structure to represent pending work
struct PendingFolder { struct PendingFolder {
folder: FolderDto, folder: FolderDto,
path: String, path: String,
} }
// Cola de trabajo para procesamiento iterativo // Work queue for iterative processing
let mut work_queue = vec![PendingFolder { let mut work_queue = vec![PendingFolder {
folder: folder.clone(), folder: folder.clone(),
path: path.to_string(), path: path.to_string(),
}]; }];
// Procesar la cola mientras haya elementos // Process the queue while there are elements
while let Some(current) = work_queue.pop() { while let Some(current) = work_queue.pop() {
let folder_id = current.folder.id.to_string(); let folder_id = current.folder.id.to_string();
// Evitar ciclos // Avoid cycles
if processed_folders.contains(&folder_id) { if processed_folders.contains(&folder_id) {
continue; continue;
} }
processed_folders.insert(folder_id.clone()); 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); let folder_path = format!("{}/", current.path);
match zip.add_directory(&folder_path, *options) { 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) => { Err(e) => {
warn!("No se pudo agregar carpeta al ZIP (puede que ya exista): {}", e); warn!("Could not add folder to ZIP (it may already exist): {}", e);
// Continuamos aunque falle crear el directorio (podría estar duplicado) // 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 { let files = match self.file_service.list_files(Some(&folder_id)).await {
Ok(files) => files, Ok(files) => files,
Err(e) => { Err(e) => {
error!("Error al listar archivos en carpeta {}: {}", folder_id, e); error!("Error listing files in folder {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error al listar archivos: {}", e)).into()); 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 { for file in files {
self.add_file_to_zip(zip, &file, &folder_path, options).await?; 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 { let subfolders = match self.folder_service.list_folders(Some(&folder_id)).await {
Ok(folders) => folders, Ok(folders) => folders,
Err(e) => { Err(e) => {
error!("Error al listar subcarpetas en {}: {}", folder_id, e); error!("Error listing subfolders in {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error al listar subcarpetas: {}", e)).into()); return Err(ZipError::FolderContentsError(format!("Error listing subfolders: {}", e)).into());
} }
}; };
// Agregar subcarpetas a la cola // Add subfolders to the queue
for subfolder in subfolders { for subfolder in subfolders {
let subfolder_path = format!("{}/{}", current.path, subfolder.name); let subfolder_path = format!("{}/{}", current.path, subfolder.name);
work_queue.push(PendingFolder { work_queue.push(PendingFolder {
@@ -187,7 +187,7 @@ impl ZipService {
Ok(()) Ok(())
} }
// Agrega un archivo al ZIP // Adds a file to the ZIP
async fn add_file_to_zip( async fn add_file_to_zip(
&self, &self,
zip: &mut ZipWriter<Cursor<Vec<u8>>>, zip: &mut ZipWriter<Cursor<Vec<u8>>>,
@@ -196,34 +196,34 @@ impl ZipService {
options: &SimpleFileOptions, options: &SimpleFileOptions,
) -> Result<()> { ) -> Result<()> {
let file_path = format!("{}{}", folder_path, file.name); 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 file_id = file.id.to_string();
let content = match self.file_service.get_file_content(&file_id).await { let content = match self.file_service.get_file_content(&file_id).await {
Ok(content) => content, Ok(content) => content,
Err(e) => { Err(e) => {
error!("Error al leer contenido del archivo {}: {}", file_id, e); error!("Error reading file content {}: {}", file_id, e);
return Err(ZipError::FileReadError(format!("Error al leer archivo {}: {}", file_id, e)).into()); 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) { match zip.start_file_from_path(std::path::Path::new(&file_path), *options) {
Ok(_) => { Ok(_) => {
match zip.write_all(&content) { match zip.write_all(&content) {
Ok(_) => { Ok(_) => {
debug!("Archivo agregado al ZIP: {}", file_path); debug!("File added to ZIP: {}", file_path);
Ok(()) Ok(())
}, },
Err(e) => { 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(ZipError::IoError(e).into())
} }
} }
}, },
Err(e) => { 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()) Err(ZipError::ZipError(e).into())
} }
} }
+29 -29
View File
@@ -15,7 +15,7 @@ use crate::application::dtos::user_dto::{
use crate::interfaces::errors::AppError; use crate::interfaces::errors::AppError;
pub fn auth_routes() -> Router<Arc<AppState>> { pub fn auth_routes() -> Router<Arc<AppState>> {
// Rutas que NO requieren autenticación // Routes that do NOT require authentication
let public_routes = Router::new() let public_routes = Router::new()
.route("/register", post(register)) .route("/register", post(register))
.route("/login", post(login)) .route("/login", post(login))
@@ -27,14 +27,14 @@ pub fn auth_routes() -> Router<Arc<AppState>> {
.route("/oidc/callback", get(oidc_callback)) .route("/oidc/callback", get(oidc_callback))
.route("/oidc/exchange", post(oidc_exchange)); .route("/oidc/exchange", post(oidc_exchange));
// Rutas que SÍ requieren autenticación - usamos route_layer para aplicar middleware // Routes that DO require authentication - we use route_layer to apply middleware
// El middleware usará el state que se pase con .with_state() desde main.rs // The middleware will use the state passed with .with_state() from main.rs
let protected_routes = Router::new() let protected_routes = Router::new()
.route("/me", get(get_current_user)) .route("/me", get(get_current_user))
.route("/change-password", put(change_password)) .route("/change-password", put(change_password))
.route("/logout", post(logout)); .route("/logout", post(logout));
// Combinar rutas públicas y protegidas // Combine public and protected routes
public_routes.merge(protected_routes) public_routes.merge(protected_routes)
} }
@@ -53,7 +53,7 @@ async fn register(
}, },
None => { None => {
tracing::error!("Auth service not configured"); 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 => { None => {
tracing::error!("Auth service not configured"); 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 // Ensure the response has the expected fields
if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() { if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() {
tracing::error!("Login response contains empty tokens for user: {}", dto.username); 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))) Ok((StatusCode::OK, Json(auth_response)))
@@ -198,7 +198,7 @@ async fn refresh_token(
// Normal process for real tokens // Normal process for real tokens
let auth_service = state.auth_service.as_ref() 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?; let auth_response = auth_service.auth_application_service.refresh_token(dto).await?;
@@ -214,37 +214,37 @@ async fn get_current_user(
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
// Normal process for all users // Normal process for all users
let auth_service = state.auth_service.as_ref() 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 let token = headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok()) .and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer ")) .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) 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; let user_id = claims.sub;
// Primero, actualizar las estadísticas de uso de almacenamiento // First, update the storage usage statistics
// IMPORTANTE: Esperamos el cálculo para devolver datos actualizados // IMPORTANT: We await the calculation to return updated data
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() { 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 { match storage_usage_service.update_user_storage_usage(&user_id).await {
Ok(usage) => { Ok(usage) => {
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage); tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
}, },
Err(e) => { 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); 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?; let user = auth_service.auth_application_service.get_user_by_id(&user_id).await?;
Ok((StatusCode::OK, Json(user))) Ok((StatusCode::OK, Json(user)))
@@ -256,18 +256,18 @@ async fn change_password(
Json(dto): Json<ChangePasswordDto>, Json(dto): Json<ChangePasswordDto>,
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref() 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 let token = headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok()) .and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer ")) .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) 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?; auth_service.auth_application_service.change_password(&claims.sub, dto).await?;
@@ -279,18 +279,18 @@ async fn logout(
headers: HeaderMap, headers: HeaderMap,
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref() 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 let token = headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok()) .and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer ")) .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) 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) // Use access token for logout (we don't have refresh token in headers)
auth_service.auth_application_service.logout(&claims.sub, token).await?; auth_service.auth_application_service.logout(&claims.sub, token).await?;
@@ -314,7 +314,7 @@ async fn get_system_status(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref() 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 // Count admin users to determine if system is initialized
let admin_count = auth_service.auth_application_service.count_admin_users().await let admin_count = auth_service.auth_application_service.count_admin_users().await
+84 -84
View File
@@ -13,86 +13,86 @@ use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto; use crate::application::dtos::folder_dto::FolderDto;
use crate::interfaces::api::handlers::ApiResult; use crate::interfaces::api::handlers::ApiResult;
/// Estado compartido para el handler de batch /// Shared state for the batch handler
#[derive(Clone)] #[derive(Clone)]
pub struct BatchHandlerState { pub struct BatchHandlerState {
pub batch_service: Arc<BatchOperationService>, pub batch_service: Arc<BatchOperationService>,
} }
/// DTO para las solicitudes de operaciones en lote de archivos /// DTO for batch file operation requests
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BatchFileOperationRequest { pub struct BatchFileOperationRequest {
/// IDs de los archivos a procesar /// IDs of the files to process
pub file_ids: Vec<String>, pub file_ids: Vec<String>,
/// ID de la carpeta destino (opcional) /// Target folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>, pub target_folder_id: Option<String>,
} }
/// DTO para las solicitudes de operaciones en lote de carpetas /// DTO for batch folder operation requests
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BatchFolderOperationRequest { pub struct BatchFolderOperationRequest {
/// IDs de las carpetas a procesar /// IDs of the folders to process
pub folder_ids: Vec<String>, pub folder_ids: Vec<String>,
/// Si la operación debe ser recursiva /// Whether the operation should be recursive
#[serde(default)] #[serde(default)]
pub recursive: bool, pub recursive: bool,
/// ID de la carpeta destino (opcional) /// Target folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>, pub target_folder_id: Option<String>,
} }
/// DTO para las solicitudes de creación en lote de carpetas /// DTO for batch folder creation requests
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BatchCreateFoldersRequest { pub struct BatchCreateFoldersRequest {
/// Detalles de las carpetas a crear /// Details of the folders to create
pub folders: Vec<CreateFolderDetail>, pub folders: Vec<CreateFolderDetail>,
} }
/// Detalle para creación de una carpeta /// Detail for folder creation
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CreateFolderDetail { pub struct CreateFolderDetail {
/// Nombre de la carpeta /// Folder name
pub name: String, pub name: String,
/// ID de la carpeta padre (opcional) /// Parent folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>, pub parent_id: Option<String>,
} }
/// DTO para los resultados de operaciones en lote /// DTO for batch operation results
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct BatchOperationResponse<T> { pub struct BatchOperationResponse<T> {
/// Entidades procesadas exitosamente /// Successfully processed entities
pub successful: Vec<T>, pub successful: Vec<T>,
/// Operaciones fallidas con sus mensajes de error /// Failed operations with their error messages
pub failed: Vec<FailedOperation>, pub failed: Vec<FailedOperation>,
/// Estadísticas de la operación /// Operation statistics
pub stats: BatchOperationStats, pub stats: BatchOperationStats,
} }
/// Operación fallida en un lote /// Failed operation in a batch
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct FailedOperation { pub struct FailedOperation {
/// Identificador de la entidad que falló /// Identifier of the entity that failed
pub id: String, pub id: String,
/// Mensaje de error /// Error message
pub error: String, pub error: String,
} }
/// Estadísticas de una operación por lotes /// Statistics for a batch operation
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct BatchOperationStats { pub struct BatchOperationStats {
/// Número total de operaciones /// Total number of operations
pub total: usize, pub total: usize,
/// Número de operaciones exitosas /// Number of successful operations
pub successful: usize, pub successful: usize,
/// Número de operaciones fallidas /// Number of failed operations
pub failed: usize, pub failed: usize,
/// Tiempo total de ejecución en milisegundos /// Total execution time in milliseconds
pub execution_time_ms: u128, pub execution_time_ms: u128,
} }
/// Convierte BatchStats del dominio a DTO /// Converts domain BatchStats to DTO
impl From<BatchStats> for BatchOperationStats { impl From<BatchStats> for BatchOperationStats {
fn from(stats: BatchStats) -> Self { fn from(stats: BatchStats) -> Self {
Self { Self {
@@ -104,7 +104,7 @@ impl From<BatchStats> for BatchOperationStats {
} }
} }
/// Convierte BatchResult<T> del dominio a DTO /// Converts domain BatchResult<T> to DTO
impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U> impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U>
where where
U: From<T>, U: From<T>,
@@ -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( pub async fn move_files_batch(
State(state): State<BatchHandlerState>, State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>, Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> { ) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar // Verify there are files to process
if request.file_ids.is_empty() { if request.file_ids.is_empty() {
return Ok(( return Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -139,35 +139,35 @@ pub async fn move_files_batch(
).into_response()); ).into_response());
} }
// Ejecutar operación de lote // Execute batch operation
let result = state.batch_service let result = state.batch_service
.move_files(request.file_ids, request.target_folder_id) .move_files(request.file_ids, request.target_folder_id)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO // Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into(); let response: BatchOperationResponse<FileDto> = 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 { let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 { if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else { } else {
StatusCode::BAD_REQUEST // Todas fallaron StatusCode::BAD_REQUEST // All failed
} }
} else { } else {
StatusCode::OK // Todas exitosas StatusCode::OK // All successful
}; };
Ok((status_code, Json(response)).into_response()) 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( pub async fn copy_files_batch(
State(state): State<BatchHandlerState>, State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>, Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> { ) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar // Verify there are files to process
if request.file_ids.is_empty() { if request.file_ids.is_empty() {
return Ok(( return Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -177,35 +177,35 @@ pub async fn copy_files_batch(
).into_response()); ).into_response());
} }
// Ejecutar operación de lote // Execute batch operation
let result = state.batch_service let result = state.batch_service
.copy_files(request.file_ids, request.target_folder_id) .copy_files(request.file_ids, request.target_folder_id)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO // Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into(); let response: BatchOperationResponse<FileDto> = 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 { let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 { if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else { } else {
StatusCode::BAD_REQUEST // Todas fallaron StatusCode::BAD_REQUEST // All failed
} }
} else { } else {
StatusCode::OK // Todas exitosas StatusCode::OK // All successful
}; };
Ok((status_code, Json(response)).into_response()) 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( pub async fn delete_files_batch(
State(state): State<BatchHandlerState>, State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>, Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> { ) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar // Verify there are files to process
if request.file_ids.is_empty() { if request.file_ids.is_empty() {
return Ok(( return Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -215,13 +215,13 @@ pub async fn delete_files_batch(
).into_response()); ).into_response());
} }
// Ejecutar operación de lote // Execute batch operation
let result = state.batch_service let result = state.batch_service
.delete_files(request.file_ids) .delete_files(request.file_ids)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .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 { let response = BatchOperationResponse {
successful: result.successful, successful: result.successful,
failed: result.failed.into_iter() failed: result.failed.into_iter()
@@ -230,26 +230,26 @@ pub async fn delete_files_batch(
stats: result.stats.into(), 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 { let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 { if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else { } else {
StatusCode::BAD_REQUEST // Todas fallaron StatusCode::BAD_REQUEST // All failed
} }
} else { } else {
StatusCode::OK // Todas exitosas StatusCode::OK // All successful
}; };
Ok((status_code, Json(response)).into_response()) 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( pub async fn delete_folders_batch(
State(state): State<BatchHandlerState>, State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>, Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> { ) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar // Verify there are folders to process
if request.folder_ids.is_empty() { if request.folder_ids.is_empty() {
return Ok(( return Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -259,13 +259,13 @@ pub async fn delete_folders_batch(
).into_response()); ).into_response());
} }
// Ejecutar operación de lote // Execute batch operation
let result = state.batch_service let result = state.batch_service
.delete_folders(request.folder_ids, request.recursive) .delete_folders(request.folder_ids, request.recursive)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .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 { let response = BatchOperationResponse {
successful: result.successful, successful: result.successful,
failed: result.failed.into_iter() failed: result.failed.into_iter()
@@ -274,26 +274,26 @@ pub async fn delete_folders_batch(
stats: result.stats.into(), 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 { let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 { if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else { } else {
StatusCode::BAD_REQUEST // Todas fallaron StatusCode::BAD_REQUEST // All failed
} }
} else { } else {
StatusCode::OK // Todas exitosas StatusCode::OK // All successful
}; };
Ok((status_code, Json(response)).into_response()) 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( pub async fn create_folders_batch(
State(state): State<BatchHandlerState>, State(state): State<BatchHandlerState>,
Json(request): Json<BatchCreateFoldersRequest>, Json(request): Json<BatchCreateFoldersRequest>,
) -> ApiResult<impl IntoResponse> { ) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar // Verify there are folders to process
if request.folders.is_empty() { if request.folders.is_empty() {
return Ok(( return Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -303,41 +303,41 @@ pub async fn create_folders_batch(
).into_response()); ).into_response());
} }
// Transformar el formato para el servicio // Transform the format for the service
let folders = request.folders let folders = request.folders
.into_iter() .into_iter()
.map(|detail| (detail.name, detail.parent_id)) .map(|detail| (detail.name, detail.parent_id))
.collect(); .collect();
// Ejecutar operación de lote // Execute batch operation
let result = state.batch_service let result = state.batch_service
.create_folders(folders) .create_folders(folders)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO // Convert result to DTO
let response: BatchOperationResponse<FolderDto> = result.into(); let response: BatchOperationResponse<FolderDto> = 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 { let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 { if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else { } else {
StatusCode::BAD_REQUEST // Todas fallaron StatusCode::BAD_REQUEST // All failed
} }
} else { } else {
StatusCode::CREATED // Todas exitosas StatusCode::CREATED // All successful
}; };
Ok((status_code, Json(response)).into_response()) 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( pub async fn get_files_batch(
State(state): State<BatchHandlerState>, State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>, Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> { ) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar // Verify there are files to process
if request.file_ids.is_empty() { if request.file_ids.is_empty() {
return Ok(( return Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -347,35 +347,35 @@ pub async fn get_files_batch(
).into_response()); ).into_response());
} }
// Ejecutar operación de lote // Execute batch operation
let result = state.batch_service let result = state.batch_service
.get_multiple_files(request.file_ids) .get_multiple_files(request.file_ids)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO // Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into(); let response: BatchOperationResponse<FileDto> = 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 { let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 { if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else { } else {
StatusCode::BAD_REQUEST // Todas fallaron StatusCode::BAD_REQUEST // All failed
} }
} else { } else {
StatusCode::OK // Todas exitosas StatusCode::OK // All successful
}; };
Ok((status_code, Json(response)).into_response()) 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( pub async fn get_folders_batch(
State(state): State<BatchHandlerState>, State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>, Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> { ) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar // Verify there are folders to process
if request.folder_ids.is_empty() { if request.folder_ids.is_empty() {
return Ok(( return Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -385,24 +385,24 @@ pub async fn get_folders_batch(
).into_response()); ).into_response());
} }
// Ejecutar operación de lote // Execute batch operation
let result = state.batch_service let result = state.batch_service
.get_multiple_folders(request.folder_ids) .get_multiple_folders(request.folder_ids)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO // Convert result to DTO
let response: BatchOperationResponse<FolderDto> = result.into(); let response: BatchOperationResponse<FolderDto> = 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 { let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 { if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else { } else {
StatusCode::BAD_REQUEST // Todas fallaron StatusCode::BAD_REQUEST // All failed
} }
} else { } else {
StatusCode::OK // Todas exitosas StatusCode::OK // All successful
}; };
Ok((status_code, Json(response)).into_response()) Ok((status_code, Json(response)).into_response())
+24 -24
View File
@@ -11,14 +11,14 @@ use tracing::{error, info};
use crate::application::ports::recent_ports::RecentItemsUseCase; use crate::application::ports::recent_ports::RecentItemsUseCase;
use crate::interfaces::middleware::auth::AuthUser; use crate::interfaces::middleware::auth::AuthUser;
/// Parámetros de consulta para obtener elementos recientes /// Query parameters for getting recent items
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct GetRecentParams { pub struct GetRecentParams {
#[serde(default)] #[serde(default)]
limit: Option<i32>, limit: Option<i32>,
} }
/// Obtener elementos recientes del usuario /// Get user's recent items
pub async fn get_recent_items( pub async fn get_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>, State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser, auth_user: AuthUser,
@@ -28,22 +28,22 @@ pub async fn get_recent_items(
match recent_service.get_recent_items(user_id, params.limit).await { match recent_service.get_recent_items(user_id, params.limit).await {
Ok(items) => { Ok(items) => {
info!("Recuperados {} elementos recientes para usuario", items.len()); info!("Retrieved {} recent items for user", items.len());
(StatusCode::OK, Json(items)).into_response() (StatusCode::OK, Json(items)).into_response()
}, },
Err(err) => { Err(err) => {
error!("Error al recuperar elementos recientes: {}", err); error!("Error retrieving recent items: {}", err);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ Json(serde_json::json!({
"error": format!("Fallo al recuperar elementos recientes: {}", err) "error": format!("Failed to retrieve recent items: {}", err)
})) }))
).into_response() ).into_response()
} }
} }
} }
/// Registrar acceso a un elemento /// Record access to an item
pub async fn record_item_access( pub async fn record_item_access(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>, State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser, auth_user: AuthUser,
@@ -51,39 +51,39 @@ pub async fn record_item_access(
) -> impl IntoResponse { ) -> impl IntoResponse {
let user_id = &auth_user.id; let user_id = &auth_user.id;
// Validar tipo de elemento // Validate item type
if item_type != "file" && item_type != "folder" { if item_type != "file" && item_type != "folder" {
return ( return (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Json(serde_json::json!({ Json(serde_json::json!({
"error": "El tipo de elemento debe ser 'file' o 'folder'" "error": "Item type must be 'file' or 'folder'"
})) }))
).into_response(); ).into_response();
} }
match recent_service.record_item_access(user_id, &item_id, &item_type).await { match recent_service.record_item_access(user_id, &item_id, &item_type).await {
Ok(_) => { Ok(_) => {
info!("Registrado acceso a {} '{}' en recientes", item_type, item_id); info!("Recorded access to {} '{}' in recents", item_type, item_id);
( (
StatusCode::OK, StatusCode::OK,
Json(serde_json::json!({ Json(serde_json::json!({
"message": "Acceso registrado correctamente" "message": "Access recorded successfully"
})) }))
).into_response() ).into_response()
}, },
Err(err) => { Err(err) => {
error!("Error al registrar acceso en recientes: {}", err); error!("Error recording access in recents: {}", err);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ Json(serde_json::json!({
"error": format!("Fallo al registrar acceso: {}", err) "error": format!("Failed to record access: {}", err)
})) }))
).into_response() ).into_response()
} }
} }
} }
/// Eliminar un elemento de recientes /// Remove an item from recents
pub async fn remove_from_recent( pub async fn remove_from_recent(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>, State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser, 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 { match recent_service.remove_from_recent(user_id, &item_id, &item_type).await {
Ok(removed) => { Ok(removed) => {
if removed { if removed {
info!("Eliminado {} '{}' de recientes", item_type, item_id); info!("Removed {} '{}' from recents", item_type, item_id);
( (
StatusCode::OK, StatusCode::OK,
Json(serde_json::json!({ Json(serde_json::json!({
"message": "Elemento eliminado de recientes" "message": "Item removed from recents"
})) }))
).into_response() ).into_response()
} else { } else {
info!("Elemento {} '{}' no estaba en recientes", item_type, item_id); info!("Item {} '{}' was not in recents", item_type, item_id);
( (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
Json(serde_json::json!({ Json(serde_json::json!({
"message": "Elemento no estaba en recientes" "message": "Item was not in recents"
})) }))
).into_response() ).into_response()
} }
}, },
Err(err) => { Err(err) => {
error!("Error al eliminar de recientes: {}", err); error!("Error removing from recents: {}", err);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ Json(serde_json::json!({
"error": format!("Fallo al eliminar de recientes: {}", err) "error": format!("Failed to remove from recents: {}", err)
})) }))
).into_response() ).into_response()
} }
} }
} }
/// Limpiar todos los elementos recientes /// Clear all recent items
pub async fn clear_recent_items( pub async fn clear_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>, State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser, auth_user: AuthUser,
@@ -132,20 +132,20 @@ pub async fn clear_recent_items(
match recent_service.clear_recent_items(user_id).await { match recent_service.clear_recent_items(user_id).await {
Ok(_) => { Ok(_) => {
info!("Limpiados todos los elementos recientes para usuario"); info!("Cleared all recent items for user");
( (
StatusCode::OK, StatusCode::OK,
Json(serde_json::json!({ Json(serde_json::json!({
"message": "Elementos recientes limpiados correctamente" "message": "Recent items cleared successfully"
})) }))
).into_response() ).into_response()
}, },
Err(err) => { Err(err) => {
error!("Error al limpiar elementos recientes: {}", err); error!("Error clearing recent items: {}", err);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ Json(serde_json::json!({
"error": format!("Fallo al limpiar elementos recientes: {}", err) "error": format!("Failed to clear recent items: {}", err)
})) }))
).into_response() ).into_response()
} }
+51 -51
View File
@@ -10,34 +10,34 @@ use crate::application::dtos::search_dto::SearchCriteriaDto;
use crate::common::di::AppState; 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, * This handler exposes endpoints related to search functionality,
* permitiendo a los usuarios buscar archivos y carpetas usando diversos criterios. * allowing users to search for files and folders using various criteria.
*/ */
pub struct SearchHandler; pub struct SearchHandler;
impl 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 state Application state with services
* @param query_params Parámetros de búsqueda como query string * @param query_params Search parameters as query string
* @return Respuesta HTTP con los resultados de la búsqueda * @return HTTP response with the search results
*/ */
pub async fn search_files_get( pub async fn search_files_get(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<SearchParams>, Query(params): Query<SearchParams>,
) -> impl IntoResponse { ) -> 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 { let search_service = match &state.applications.search_service {
Some(service) => service, Some(service) => service,
None => { None => {
error!("Servicio de búsqueda no disponible"); error!("Search service not available");
return ( return (
StatusCode::SERVICE_UNAVAILABLE, StatusCode::SERVICE_UNAVAILABLE,
Json(json!({ 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 { let search_criteria = SearchCriteriaDto {
name_contains: params.query, name_contains: params.query,
file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()), 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), offset: params.offset.unwrap_or(0),
}; };
// Realizar la búsqueda // Perform the search
match search_service.search(search_criteria).await { match search_service.search(search_criteria).await {
Ok(results) => { Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados", info!("Search completed, {} files and {} folders found",
results.files.len(), results.folders.len()); results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response() (StatusCode::OK, Json(results)).into_response()
}, },
Err(err) => { Err(err) => {
error!("Error en búsqueda: {}", err); error!("Search error: {}", err);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ 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 * This endpoint allows more complex searches with all possible criteria
* proporcionados en el cuerpo de la solicitud. * provided in the request body.
* *
* @param state Estado de la aplicación con servicios * @param state Application state with services
* @param criteria Criterios de búsqueda completos * @param criteria Complete search criteria
* @return Respuesta HTTP con los resultados de la búsqueda * @return HTTP response with the search results
*/ */
pub async fn search_files_post( pub async fn search_files_post(
State(state): State<AppState>, State(state): State<AppState>,
Json(criteria): Json<SearchCriteriaDto>, Json(criteria): Json<SearchCriteriaDto>,
) -> impl IntoResponse { ) -> 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 { let search_service = match &state.applications.search_service {
Some(service) => service, Some(service) => service,
None => { None => {
error!("Servicio de búsqueda no disponible"); error!("Search service not available");
return ( return (
StatusCode::SERVICE_UNAVAILABLE, StatusCode::SERVICE_UNAVAILABLE,
Json(json!({ Json(json!({
@@ -112,15 +112,15 @@ impl SearchHandler {
} }
}; };
// Realizar la búsqueda // Perform the search
match search_service.search(criteria).await { match search_service.search(criteria).await {
Ok(results) => { Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados", info!("Search completed, {} files and {} folders found",
results.files.len(), results.folders.len()); results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response() (StatusCode::OK, Json(results)).into_response()
}, },
Err(err) => { Err(err) => {
error!("Error en búsqueda: {}", err); error!("Search error: {}", err);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ 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 * This endpoint is useful for forcing fresh searches after significant
* significativos en el sistema de archivos. * changes in the file system.
* *
* @param state Estado de la aplicación con servicios * @param state Application state with services
* @return Respuesta HTTP indicando éxito o error * @return HTTP response indicating success or error
*/ */
pub async fn clear_search_cache( pub async fn clear_search_cache(
State(state): State<AppState>, State(state): State<AppState>,
) -> impl IntoResponse { ) -> 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 { let search_service = match &state.applications.search_service {
Some(service) => service, Some(service) => service,
None => { None => {
error!("Servicio de búsqueda no disponible"); error!("Search service not available");
return ( return (
StatusCode::SERVICE_UNAVAILABLE, StatusCode::SERVICE_UNAVAILABLE,
Json(json!({ Json(json!({
@@ -159,10 +159,10 @@ impl SearchHandler {
} }
}; };
// Limpiar la caché // Clear the cache
match search_service.clear_search_cache().await { match search_service.clear_search_cache().await {
Ok(_) => { Ok(_) => {
info!("Caché de búsqueda limpiada correctamente"); info!("Search cache cleared successfully");
( (
StatusCode::OK, StatusCode::OK,
Json(json!({ Json(json!({
@@ -171,7 +171,7 @@ impl SearchHandler {
).into_response() ).into_response()
}, },
Err(err) => { Err(err) => {
error!("Error al limpiar caché de búsqueda: {}", err); error!("Error clearing search cache: {}", err);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ 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)] #[derive(Debug, serde::Deserialize)]
pub struct SearchParams { pub struct SearchParams {
/// Texto a buscar en nombres de archivos y carpetas /// Text to search for in file and folder names
pub query: Option<String>, pub query: Option<String>,
/// Filtro por tipos de archivo (extensiones separadas por comas) /// Filter by file types (comma-separated extensions)
#[serde(rename = "type")] #[serde(rename = "type")]
pub type_filter: Option<String>, pub type_filter: Option<String>,
/// Filtrar elementos creados después de esta fecha (timestamp) /// Filter items created after this date (timestamp)
pub created_after: Option<u64>, pub created_after: Option<u64>,
/// Filtrar elementos creados antes de esta fecha (timestamp) /// Filter items created before this date (timestamp)
pub created_before: Option<u64>, pub created_before: Option<u64>,
/// Filtrar elementos modificados después de esta fecha (timestamp) /// Filter items modified after this date (timestamp)
pub modified_after: Option<u64>, pub modified_after: Option<u64>,
/// Filtrar elementos modificados antes de esta fecha (timestamp) /// Filter items modified before this date (timestamp)
pub modified_before: Option<u64>, pub modified_before: Option<u64>,
/// Tamaño mínimo en bytes /// Minimum size in bytes
pub min_size: Option<u64>, pub min_size: Option<u64>,
/// Tamaño máximo en bytes /// Maximum size in bytes
pub max_size: Option<u64>, pub max_size: Option<u64>,
/// ID de carpeta para limitar la búsqueda /// Folder ID to limit the search scope
pub folder_id: Option<String>, pub folder_id: Option<String>,
/// Búsqueda recursiva en subcarpetas /// Recursive search in subfolders
pub recursive: Option<bool>, pub recursive: Option<bool>,
/// Límite de resultados para paginación /// Result limit for pagination
pub limit: Option<usize>, pub limit: Option<usize>,
/// Desplazamiento para paginación /// Offset for pagination
pub offset: Option<usize>, pub offset: Option<usize>,
} }
+30 -30
View File
@@ -8,7 +8,7 @@ use tracing::{debug, error, warn, instrument};
use crate::common::di::AppState; use crate::common::di::AppState;
use crate::interfaces::middleware::auth::AuthUser; 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)] #[instrument(skip_all)]
pub async fn get_trash_items( pub async fn get_trash_items(
State(state): State<AppState>, State(state): State<AppState>,
@@ -19,7 +19,7 @@ pub async fn get_trash_items(
// privilege escalation attacks. // privilege escalation attacks.
let effective_user = auth_user.id.clone(); 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() { let trash_service = match state.trash_service.as_ref() {
Some(service) => service, Some(service) => service,
@@ -34,11 +34,11 @@ pub async fn get_trash_items(
match result { match result {
Ok(items) => { Ok(items) => {
debug!("Encontrados {} elementos en la papelera", items.len()); debug!("Found {} items in trash", items.len());
(StatusCode::OK, Json(json!(items))) (StatusCode::OK, Json(json!(items)))
}, },
Err(e) => { Err(e) => {
error!("Error al obtener elementos de la papelera: {:?}", e); error!("Error retrieving trash items: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error retrieving trash items: {}", e) "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)] #[instrument(skip_all)]
pub async fn move_to_trash( pub async fn move_to_trash(
State(state): State<AppState>, State(state): State<AppState>,
auth_user: AuthUser, auth_user: AuthUser,
Path((item_type, item_id)): Path<(String, String)>, Path((item_type, item_id)): Path<(String, String)>,
) -> (StatusCode, Json<serde_json::Value>) { ) -> (StatusCode, Json<serde_json::Value>) {
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); item_type, item_id, auth_user.id);
let trash_service = match state.trash_service.as_ref() { let trash_service = match state.trash_service.as_ref() {
@@ -68,14 +68,14 @@ pub async fn move_to_trash(
match result { match result {
Ok(_) => { Ok(_) => {
debug!("Elemento movido a papelera con éxito"); debug!("Item moved to trash successfully");
(StatusCode::OK, Json(json!({ (StatusCode::OK, Json(json!({
"success": true, "success": true,
"message": "Item moved to trash successfully" "message": "Item moved to trash successfully"
}))) })))
}, },
Err(e) => { Err(e) => {
error!("Error al mover elemento a papelera: {:?}", e); error!("Error moving item to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving item to trash: {}", e) "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)] #[instrument(skip_all)]
pub async fn move_file_to_trash( pub async fn move_file_to_trash(
State(state): State<AppState>, State(state): State<AppState>,
auth_user: AuthUser, auth_user: AuthUser,
Path(item_id): Path<String>, Path(item_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) { ) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para mover archivo a papelera: id={}, usuario={}", debug!("Request to move file to trash: id={}, user={}",
item_id, auth_user.id); item_id, auth_user.id);
let trash_service = match state.trash_service.as_ref() { 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; let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await;
match result { match result {
Ok(_) => { Ok(_) => {
debug!("Archivo movido a papelera con éxito"); debug!("File moved to trash successfully");
(StatusCode::OK, Json(json!({ (StatusCode::OK, Json(json!({
"success": true, "success": true,
"message": "File moved to trash successfully" "message": "File moved to trash successfully"
}))) })))
}, },
Err(e) => { Err(e) => {
error!("Error al mover archivo a papelera: {:?}", e); error!("Error moving file to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving file to trash: {}", e) "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)] #[instrument(skip_all)]
pub async fn move_folder_to_trash( pub async fn move_folder_to_trash(
State(state): State<AppState>, State(state): State<AppState>,
auth_user: AuthUser, auth_user: AuthUser,
Path(item_id): Path<String>, Path(item_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) { ) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para mover carpeta a papelera: id={}, usuario={}", debug!("Request to move folder to trash: id={}, user={}",
item_id, auth_user.id); item_id, auth_user.id);
let trash_service = match state.trash_service.as_ref() { 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; let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await;
match result { match result {
Ok(_) => { Ok(_) => {
debug!("Carpeta movida a papelera con éxito"); debug!("Folder moved to trash successfully");
(StatusCode::OK, Json(json!({ (StatusCode::OK, Json(json!({
"success": true, "success": true,
"message": "Folder moved to trash successfully" "message": "Folder moved to trash successfully"
}))) })))
}, },
Err(e) => { Err(e) => {
error!("Error al mover carpeta a papelera: {:?}", e); error!("Error moving folder to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving folder to trash: {}", e) "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)] #[instrument(skip_all)]
pub async fn restore_from_trash( pub async fn restore_from_trash(
State(state): State<AppState>, State(state): State<AppState>,
auth_user: AuthUser, auth_user: AuthUser,
Path(trash_id): Path<String>, Path(trash_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) { ) -> (StatusCode, Json<serde_json::Value>) {
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() { let trash_service = match state.trash_service.as_ref() {
Some(service) => service, Some(service) => service,
@@ -182,7 +182,7 @@ pub async fn restore_from_trash(
match result { match result {
Ok(_) => { Ok(_) => {
debug!("Elemento restaurado con éxito"); debug!("Item restored successfully");
(StatusCode::OK, Json(json!({ (StatusCode::OK, Json(json!({
"success": true, "success": true,
"message": "Item restored successfully" "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!({ (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error restoring item from trash: {}", e) "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)] #[instrument(skip_all)]
pub async fn delete_permanently( pub async fn delete_permanently(
State(state): State<AppState>, State(state): State<AppState>,
auth_user: AuthUser, auth_user: AuthUser,
Path(trash_id): Path<String>, Path(trash_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) { ) -> (StatusCode, Json<serde_json::Value>) {
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() { let trash_service = match state.trash_service.as_ref() {
Some(service) => service, Some(service) => service,
@@ -228,7 +228,7 @@ pub async fn delete_permanently(
match result { match result {
Ok(_) => { Ok(_) => {
debug!("Elemento eliminado permanentemente"); debug!("Item permanently deleted");
(StatusCode::OK, Json(json!({ (StatusCode::OK, Json(json!({
"success": true, "success": true,
"message": "Item deleted permanently" "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!({ (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error deleting item permanently: {}", e) "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)] #[instrument(skip_all)]
pub async fn empty_trash( pub async fn empty_trash(
State(state): State<AppState>, State(state): State<AppState>,
auth_user: AuthUser, auth_user: AuthUser,
) -> (StatusCode, Json<serde_json::Value>) { ) -> (StatusCode, Json<serde_json::Value>) {
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() { let trash_service = match state.trash_service.as_ref() {
Some(service) => service, Some(service) => service,
@@ -273,14 +273,14 @@ pub async fn empty_trash(
match result { match result {
Ok(_) => { Ok(_) => {
debug!("Papelera vaciada con éxito"); debug!("Trash emptied successfully");
(StatusCode::OK, Json(json!({ (StatusCode::OK, Json(json!({
"success": true, "success": true,
"message": "Trash emptied successfully" "message": "Trash emptied successfully"
}))) })))
}, },
Err(e) => { Err(e) => {
error!("Error al vaciar papelera: {:?}", e); error!("Error emptying trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error emptying trash: {}", e) "error": format!("Error emptying trash: {}", e)
}))) })))
+6 -6
View File
@@ -90,14 +90,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
let favorites_service = app_state.favorites_service.clone(); let favorites_service = app_state.favorites_service.clone();
let recent_service = app_state.recent_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( let batch_service = Arc::new(BatchOperationService::default(
file_retrieval_service.clone(), file_retrieval_service.clone(),
file_management_service.clone(), file_management_service.clone(),
folder_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 { let batch_handler_state = BatchHandlerState {
batch_service: batch_service.clone(), batch_service: batch_service.clone(),
}; };
@@ -154,14 +154,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
// Merge the routers // Merge the routers
let files_router = basic_file_router.merge(file_operations_router); 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() let batch_router = Router::new()
// Operaciones de archivos // File operations
.route("/files/move", post(batch_handler::move_files_batch)) .route("/files/move", post(batch_handler::move_files_batch))
.route("/files/copy", post(batch_handler::copy_files_batch)) .route("/files/copy", post(batch_handler::copy_files_batch))
.route("/files/delete", post(batch_handler::delete_files_batch)) .route("/files/delete", post(batch_handler::delete_files_batch))
.route("/files/get", post(batch_handler::get_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/delete", post(batch_handler::delete_folders_batch))
.route("/folders/create", post(batch_handler::create_folders_batch)) .route("/folders/create", post(batch_handler::create_folders_batch))
.route("/folders/get", post(batch_handler::get_folders_batch)) .route("/folders/get", post(batch_handler::get_folders_batch))
@@ -183,7 +183,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
Router::new() 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) // Create routes for shared resources management (requires auth)
let share_router = if let Some(share_service) = share_service.clone() { let share_router = if let Some(share_service) = share_service.clone() {
+31 -31
View File
@@ -11,24 +11,24 @@ use crate::common::di::AppState;
// Re-export CurrentUser from application layer for use in handlers // Re-export CurrentUser from application layer for use in handlers
pub use crate::application::dtos::user_dto::CurrentUser; pub use crate::application::dtos::user_dto::CurrentUser;
// Estructura para usar en extractores de Axum // Structure for use in Axum extractors
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AuthUser { pub struct AuthUser {
pub id: String, pub id: String,
pub username: String, pub username: String,
} }
/// Extractor reutilizable que obtiene el user_id del usuario autenticado. /// Reusable extractor that gets the user_id of the authenticated user.
/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware. /// Automatically extracted from the `CurrentUser` inserted by the auth middleware.
/// ///
/// Uso en handlers: /// Usage in handlers:
/// ```ignore /// ```ignore
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... } /// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CurrentUserId(pub String); 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<S> FromRequestParts<S> for AuthUser impl<S> FromRequestParts<S> for AuthUser
where where
S: Send + Sync, 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<S> FromRequestParts<S> for CurrentUserId impl<S> FromRequestParts<S> for CurrentUserId
where where
S: Send + Sync, S: Send + Sync,
@@ -63,37 +63,37 @@ where
} }
} }
// Error para las operaciones de autenticación // Error for authentication operations
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AuthError { pub enum AuthError {
#[error("Token no proporcionado")] #[error("Token not provided")]
TokenNotProvided, TokenNotProvided,
#[error("Token inválido: {0}")] #[error("Invalid token: {0}")]
InvalidToken(String), InvalidToken(String),
#[error("Token expirado")] #[error("Token expired")]
TokenExpired, TokenExpired,
#[error("Usuario no encontrado")] #[error("User not found")]
UserNotFound, UserNotFound,
#[error("Acceso denegado: {0}")] #[error("Access denied: {0}")]
AccessDenied(String), AccessDenied(String),
#[error("Servicio de autenticación no disponible")] #[error("Authentication service unavailable")]
AuthServiceUnavailable, AuthServiceUnavailable,
} }
impl IntoResponse for AuthError { impl IntoResponse for AuthError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let (status, error_message) = match self { 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::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()), AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired".to_string()),
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()), AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "User not found".to_string()),
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg), 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!({ 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. /// Validates the JWT token against the configured authentication service.
/// No acepta bypasses, tokens mock, ni parámetros de URL para saltar validación. /// Does not accept bypasses, mock tokens, or URL parameters to skip validation.
pub async fn auth_middleware( pub async fn auth_middleware(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
headers: HeaderMap, headers: HeaderMap,
mut request: Request, mut request: Request,
next: Next, next: Next,
) -> Result<Response, AuthError> { ) -> Result<Response, AuthError> {
// Extraer el token Bearer del header Authorization // Extract the Bearer token from the Authorization header
let token_str = headers let token_str = headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok()) .and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer ")) .and_then(|value| value.strip_prefix("Bearer "))
.ok_or(AuthError::TokenNotProvided)?; .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(); let token_str = token_str.trim();
if token_str.is_empty() { if token_str.is_empty() {
return Err(AuthError::TokenNotProvided); return Err(AuthError::TokenNotProvided);
@@ -129,7 +129,7 @@ pub async fn auth_middleware(
tracing::debug!("Processing authentication token"); 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() { if let Some(auth_service) = state.auth_service.as_ref() {
let token_service = &auth_service.token_service; let token_service = &auth_service.token_service;
match token_service.validate_token(token_str) { match token_service.validate_token(token_str) {
@@ -146,25 +146,25 @@ pub async fn auth_middleware(
}, },
Err(e) => { Err(e) => {
tracing::warn!("Token validation failed: {}", 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"); tracing::error!("Auth middleware invoked but auth service is not configured");
Err(AuthError::AuthServiceUnavailable) 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 /// Must be applied AFTER auth_middleware, as it depends on
/// `CurrentUser` esté presente en las extensiones de la request. /// `CurrentUser` being present in the request extensions.
pub async fn require_admin( pub async fn require_admin(
request: Request, request: Request,
next: Next, next: Next,
) -> Response { ) -> Response {
// Obtener el CurrentUser insertado por auth_middleware // Get the CurrentUser inserted by auth_middleware
if let Some(current_user) = request.extensions().get::<CurrentUser>() { if let Some(current_user) = request.extensions().get::<CurrentUser>() {
if current_user.role == "admin" { if current_user.role == "admin" {
tracing::debug!("Admin access granted for user: {}", current_user.username); 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"); tracing::warn!("Admin check failed: no authenticated user in request");
} }
// Acceso denegado // Access denied
let error = AuthError::AccessDenied("Se requiere rol de administrador".to_string()); let error = AuthError::AccessDenied("Admin role required".to_string());
error.into_response() error.into_response()
} }
+82 -82
View File
@@ -17,39 +17,39 @@ use std::future::Future;
use bytes::Bytes; use bytes::Bytes;
use tracing::{debug, info}; use tracing::{debug, info};
const MAX_CACHE_ENTRIES: usize = 1000; // Máximo número de entradas en caché const MAX_CACHE_ENTRIES: usize = 1000; // Maximum number of cache entries
const DEFAULT_MAX_AGE: u64 = 60; // Tiempo de vida por defecto en segundos 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 CacheKey = String;
type EntityTag = String; type EntityTag = String;
/// Un valor almacenado en caché /// A cached value
#[derive(Clone)] #[derive(Clone)]
struct CacheEntry { struct CacheEntry {
/// El ETag calculado para este valor /// The ETag calculated for this value
etag: EntityTag, etag: EntityTag,
/// Los datos serializados en bytes /// The serialized data in bytes
data: Option<Bytes>, data: Option<Bytes>,
/// Las cabeceras originales /// The original headers
headers: HeaderMap, headers: HeaderMap,
/// Timestamp de cuando fue almacenado /// Timestamp of when it was stored
timestamp: SystemTime, timestamp: SystemTime,
/// Tiempo de vida en segundos /// Time-to-live in seconds
max_age: u64, max_age: u64,
} }
/// Cache para respuestas HTTP con soporte para ETag /// Cache for HTTP responses with ETag support
#[derive(Clone)] #[derive(Clone)]
pub struct HttpCache { pub struct HttpCache {
/// Almacenamiento de entradas en caché /// Cache entry storage
cache: Arc<Mutex<HashMap<CacheKey, CacheEntry>>>, cache: Arc<Mutex<HashMap<CacheKey, CacheEntry>>>,
/// Tiempo de vida por defecto para las entradas /// Default time-to-live for entries
default_max_age: u64, default_max_age: u64,
} }
impl HttpCache { impl HttpCache {
/// Crea una nueva instancia del caché /// Creates a new cache instance
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))), 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 { pub fn with_max_age(max_age: u64) -> Self {
Self { Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))), 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) { pub fn stats(&self) -> (usize, usize) {
let lock = self.cache.lock().unwrap(); let lock = self.cache.lock().unwrap();
let total = lock.len(); let total = lock.len();
// Contar entradas válidas // Count valid entries
let _now = SystemTime::now(); let _now = SystemTime::now();
let valid = lock.values().filter(|entry| { let valid = lock.values().filter(|entry| {
match entry.timestamp.elapsed() { match entry.timestamp.elapsed() {
@@ -82,12 +82,12 @@ impl HttpCache {
(total, valid) (total, valid)
} }
/// Limpia entradas expiradas /// Cleans up expired entries
pub fn cleanup(&self) -> usize { pub fn cleanup(&self) -> usize {
let mut lock = self.cache.lock().unwrap(); let mut lock = self.cache.lock().unwrap();
let initial_count = lock.len(); let initial_count = lock.len();
// Eliminar entradas expiradas // Remove expired entries
let _now = SystemTime::now(); let _now = SystemTime::now();
lock.retain(|_, entry| { lock.retain(|_, entry| {
match entry.timestamp.elapsed() { match entry.timestamp.elapsed() {
@@ -102,18 +102,18 @@ impl HttpCache {
removed removed
} }
/// Establece una entrada en el caché /// Sets an entry in the cache
fn set(&self, key: &str, etag: EntityTag, data: Option<Bytes>, headers: HeaderMap, max_age: Option<u64>) { fn set(&self, key: &str, etag: EntityTag, data: Option<Bytes>, headers: HeaderMap, max_age: Option<u64>) {
let mut lock = self.cache.lock().unwrap(); 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 { if lock.len() >= MAX_CACHE_ENTRIES {
debug!("Cache full, removing oldest 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); self.evict_oldest(&mut lock, MAX_CACHE_ENTRIES / 10);
} }
// Almacenar la nueva entrada // Store the new entry
lock.insert(key.to_string(), CacheEntry { lock.insert(key.to_string(), CacheEntry {
etag, etag,
data, 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<CacheKey, CacheEntry>, count: usize) { fn evict_oldest(&self, cache: &mut HashMap<CacheKey, CacheEntry>, count: usize) {
// Ordenar por timestamp // Sort by timestamp
let mut entries: Vec<(CacheKey, SystemTime)> = cache let mut entries: Vec<(CacheKey, SystemTime)> = cache
.iter() .iter()
.map(|(key, entry)| (key.clone(), entry.timestamp)) .map(|(key, entry)| (key.clone(), entry.timestamp))
.collect(); .collect();
// Ordenar por timestamp (más antiguo primero) // Sort by timestamp (oldest first)
entries.sort_by(|a, b| a.1.cmp(&b.1)); 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) { for (key, _) in entries.iter().take(count) {
cache.remove(key); cache.remove(key);
} }
} }
/// Obtiene una entrada del caché /// Gets an entry from the cache
fn get(&self, key: &str) -> Option<CacheEntry> { fn get(&self, key: &str) -> Option<CacheEntry> {
let lock = self.cache.lock().unwrap(); let lock = self.cache.lock().unwrap();
// Buscar la entrada // Look up the entry
if let Some(entry) = lock.get(key) { if let Some(entry) = lock.get(key) {
// Verificar si ha expirado // Check if it has expired
match entry.timestamp.elapsed() { match entry.timestamp.elapsed() {
Ok(elapsed) if elapsed.as_secs() < entry.max_age => { Ok(elapsed) if elapsed.as_secs() < entry.max_age => {
// Entry is still valid // Entry is still valid
@@ -162,9 +162,9 @@ impl HttpCache {
None 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 { fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
// Calcular hash // Calculate hash
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher); bytes.hash(&mut hasher);
let hash = hasher.finish(); let hash = hasher.finish();
@@ -173,7 +173,7 @@ impl HttpCache {
} }
} }
/// Middleware de caché HTTP /// HTTP cache middleware
pub async fn cache_middleware<T>( pub async fn cache_middleware<T>(
cache: HttpCache, cache: HttpCache,
cache_key: &str, cache_key: &str,
@@ -184,65 +184,65 @@ pub async fn cache_middleware<T>(
where where
T: Serialize T: Serialize
{ {
// Solo aplicar caché para solicitudes GET // Only apply cache for GET requests
if req.method() != Method::GET { if req.method() != Method::GET {
return Ok(next.run(req).await); 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() let if_none_match = req.headers()
.get("if-none-match") .get("if-none-match")
.and_then(|v| v.to_str().ok()); .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) { 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 let Some(client_etag) = if_none_match {
if client_etag == cache_entry.etag { 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); debug!("Cache hit (304) for key: {}", cache_key);
return Ok(create_not_modified_response(&cache_entry)); 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 { if let Some(data) = &cache_entry.data {
debug!("Cache hit (200) for key: {}", cache_key); 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())); let mut response = Response::new(Body::from(data.clone()));
// Copiar cabeceras originales // Copy original headers
for (key, value) in &cache_entry.headers { for (key, value) in &cache_entry.headers {
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") { if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
response.headers_mut().insert(key.clone(), value.clone()); 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)); set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
return Ok(response); 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); debug!("Cache miss for key: {}", cache_key);
let response = next.run(req).await; let response = next.run(req).await;
// No cachear errores // Don't cache errors
if !response.status().is_success() { if !response.status().is_success() {
return Ok(response); return Ok(response);
} }
// Convertir la respuesta para calcular el ETag // Convert the response to calculate the ETag
let (parts, _body) = response.into_parts(); let (parts, _body) = response.into_parts();
let bytes = axum::body::to_bytes(_body, 1024 * 1024 * 10).await.unwrap_or_default(); 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); let etag = cache.calculate_etag_for_bytes(&bytes);
// Guardar en caché // Save to cache
cache.set( cache.set(
cache_key, cache_key,
etag.clone(), etag.clone(),
@@ -251,26 +251,26 @@ where
max_age max_age
); );
// Crear la respuesta con ETag // Create the response with ETag
let mut response = Response::from_parts(parts, Body::from(bytes)); let mut response = Response::from_parts(parts, Body::from(bytes));
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age)); set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age));
Ok(response) Ok(response)
} }
/// Crea una respuesta 304 Not Modified /// Creates a 304 Not Modified response
fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> { fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
let mut response = Response::builder() let mut response = Response::builder()
.status(StatusCode::NOT_MODIFIED) .status(StatusCode::NOT_MODIFIED)
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
// Copiar cabeceras de caché // Copy cache headers
if let Some(cache_control) = entry.headers.get("cache-control") { if let Some(cache_control) = entry.headers.get("cache-control") {
response.headers_mut().insert("cache-control", cache_control.clone()); response.headers_mut().insert("cache-control", cache_control.clone());
} }
// Añadir ETag // Add ETag
response.headers_mut().insert( response.headers_mut().insert(
"etag", "etag",
HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static("")) HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static(""))
@@ -279,22 +279,22 @@ fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
response response
} }
/// Configura las cabeceras de caché para una respuesta /// Configures cache headers for a response
fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) { fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
// Añadir ETag // Add ETag
response.headers_mut().insert( response.headers_mut().insert(
"etag", "etag",
HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static("")) HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static(""))
); );
// Configurar Cache-Control // Configure Cache-Control
let cache_control = format!("public, max-age={}", max_age); let cache_control = format!("public, max-age={}", max_age);
response.headers_mut().insert( response.headers_mut().insert(
"cache-control", "cache-control",
HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static("")) HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static(""))
); );
// Añadir cabecera Last-Modified // Add Last-Modified header
let now: DateTime<Utc> = Utc::now(); let now: DateTime<Utc> = Utc::now();
let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
response.headers_mut().insert( response.headers_mut().insert(
@@ -303,7 +303,7 @@ fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
); );
} }
/// Layer para aplicar middleware de caché /// Layer for applying cache middleware
#[derive(Clone)] #[derive(Clone)]
pub struct HttpCacheLayer { pub struct HttpCacheLayer {
cache: HttpCache, cache: HttpCache,
@@ -311,7 +311,7 @@ pub struct HttpCacheLayer {
} }
impl HttpCacheLayer { impl HttpCacheLayer {
/// Crea una nueva capa de caché /// Creates a new cache layer
pub fn new(cache: HttpCache) -> Self { pub fn new(cache: HttpCache) -> Self {
Self { Self {
cache, 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 { pub fn with_max_age(mut self, max_age: u64) -> Self {
self.max_age = Some(max_age); self.max_age = Some(max_age);
self self
@@ -338,7 +338,7 @@ impl<S> Layer<S> for HttpCacheLayer {
} }
} }
/// Servicio que implementa la lógica de caché /// Service that implements cache logic
#[derive(Clone)] #[derive(Clone)]
pub struct HttpCacheService<S> { pub struct HttpCacheService<S> {
inner: S, inner: S,
@@ -365,10 +365,10 @@ where
} }
fn call(&mut self, req: Request<ReqBody>) -> Self::Future { fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
// Generar clave de caché // Generate cache key
let cache_key = req.uri().path().to_string(); 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 { if req.method() != Method::GET {
let future = self.inner.call(req); let future = self.inner.call(req);
return Box::pin(async move { return Box::pin(async move {
@@ -377,42 +377,42 @@ where
}); });
} }
// Obtener ETag del cliente // Get client ETag
let if_none_match = req.headers() let if_none_match = req.headers()
.get("if-none-match") .get("if-none-match")
.and_then(|v| v.to_str().ok()); .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 cache_clone = self.cache.clone();
let max_age = self.max_age; let max_age = self.max_age;
let entry = cache_clone.get(&cache_key); let entry = cache_clone.get(&cache_key);
match entry { match entry {
Some(cache_entry) if if_none_match == Some(&cache_entry.etag) => { 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); debug!("Cache HIT (304): {}", cache_key);
let response = create_not_modified_response(&cache_entry); let response = create_not_modified_response(&cache_entry);
return Box::pin(async move { Ok(response) }); return Box::pin(async move { Ok(response) });
}, },
Some(cache_entry) if cache_entry.data.is_some() => { 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); debug!("Cache HIT (200): {}", cache_key);
let mut response = Response::new(Body::from(cache_entry.data.clone().unwrap())); 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 { for (key, value) in &cache_entry.headers {
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") { if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
response.headers_mut().insert(key.clone(), value.clone()); 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)); set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
return Box::pin(async move { Ok(response) }); return Box::pin(async move { Ok(response) });
}, },
_ => { _ => {
// No está en caché o ha expirado // Not cached or expired
debug!("Cache MISS: {}", cache_key); debug!("Cache MISS: {}", cache_key);
let future = self.inner.call(req); let future = self.inner.call(req);
let cache_clone = self.cache.clone(); let cache_clone = self.cache.clone();
@@ -423,19 +423,19 @@ where
let response = future.await.map_err(|e| e.into())?; let response = future.await.map_err(|e| e.into())?;
let response = response_map_body(response).await; let response = response_map_body(response).await;
// No cachear errores // Don't cache errors
if !response.status().is_success() { if !response.status().is_success() {
return Ok(response); return Ok(response);
} }
// Obtener el cuerpo y calcular ETag // Get the body and calculate ETag
let (parts, body) = response.into_parts(); let (parts, body) = response.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?; let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?;
// Calcular ETag // Calculate ETag
let etag = cache_clone.calculate_etag_for_bytes(&bytes); let etag = cache_clone.calculate_etag_for_bytes(&bytes);
// Guardar en caché // Save to cache
cache_clone.set( cache_clone.set(
&cache_key, &cache_key,
etag.clone(), etag.clone(),
@@ -444,7 +444,7 @@ where
max_age max_age
); );
// Crear la respuesta con ETag // Create the response with ETag
let mut response = Response::from_parts(parts, Body::from(bytes)); 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)); 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. // Helper function to convert any body into Body preserving its content.
// Anteriormente esta función descartaba el body con Body::empty(), causando // Previously this function discarded the body with Body::empty(), causing
// pérdida de datos en respuestas no cacheadas. // data loss in non-cached responses.
async fn response_map_body<B>(response: Response<B>) -> Response<Body> async fn response_map_body<B>(response: Response<B>) -> Response<Body>
where where
B: http_body::Body + Send + 'static, B: http_body::Body + Send + 'static,
@@ -478,10 +478,10 @@ where
Response::from_parts(parts, Body::from(collected)) 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) { pub fn start_cache_cleanup_task(cache: HttpCache) {
tokio::spawn(async move { 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 { loop {
interval.tick().await; interval.tick().await;
@@ -516,10 +516,10 @@ mod tests {
let etag2 = cache.calculate_etag_for_bytes(&data2); let etag2 = cache.calculate_etag_for_bytes(&data2);
let etag3 = cache.calculate_etag_for_bytes(&data3); 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); assert_eq!(etag1, etag2);
// Datos diferentes deben generar ETags diferentes // Different data should generate different ETags
assert_ne!(etag1, etag3); assert_ne!(etag1, etag3);
} }
@@ -527,19 +527,19 @@ mod tests {
async fn test_cache_hit_miss() { async fn test_cache_hit_miss() {
let cache = HttpCache::new(); 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 bytes1 = Bytes::from(r#"{"id":1,"name":"Test"}"#);
let headers1 = HeaderMap::new(); let headers1 = HeaderMap::new();
let etag1 = cache.calculate_etag_for_bytes(&bytes1); let etag1 = cache.calculate_etag_for_bytes(&bytes1);
cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None); cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None);
// Verificar cache hit // Verify cache hit
let entry = cache.get("test").unwrap(); let entry = cache.get("test").unwrap();
assert_eq!(entry.etag, etag1); assert_eq!(entry.etag, etag1);
assert_eq!(entry.data.unwrap(), bytes1); assert_eq!(entry.data.unwrap(), bytes1);
// Verificar cache miss // Verify cache miss
assert!(cache.get("nonexistent").is_none()); assert!(cache.get("nonexistent").is_none());
} }
} }
+2 -2
View File
@@ -1,11 +1,11 @@
// Exportar los módulos principales del proyecto // Export the main project modules
pub mod common; pub mod common;
pub mod domain; pub mod domain;
pub mod application; pub mod application;
pub mod infrastructure; pub mod infrastructure;
pub mod interfaces; pub mod interfaces;
// Re-exportaciones públicas comunes // Common public re-exports
pub use application::services::folder_service::FolderService; pub use application::services::folder_service::FolderService;
pub use application::services::i18n_application_service::I18nApplicationService; pub use application::services::i18n_application_service::I18nApplicationService;
pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator}; pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
+7 -7
View File
@@ -1,6 +1,6 @@
/* Estilos para la funcionalidad de favoritos */ /* Styles for the favorites feature */
/* Indicador de favorito */ /* Favorite indicator */
.favorite-indicator { .favorite-indicator {
position: absolute; position: absolute;
top: 10px; top: 10px;
@@ -31,12 +31,12 @@
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5); 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 { .favorite-item {
position: relative; position: relative;
} }
/* Ajustes para la vista de cuadrícula */ /* Adjustments for grid view */
.file-card.favorite-item { .file-card.favorite-item {
border-left: 3px solid #ffc107; border-left: 3px solid #ffc107;
@@ -46,7 +46,7 @@
} }
} }
/* Ajustes para la vista de lista */ /* Adjustments for list view */
.file-item.favorite-item { .file-item.favorite-item {
position: relative; position: relative;
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px; grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
@@ -60,7 +60,7 @@
height: 30px; height: 30px;
} }
/* Estilos para el estado vacío específico de favoritos */ /* Styles for the favorites-specific empty state */
.favorites-empty-state { .favorites-empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -83,7 +83,7 @@
max-width: 400px; max-width: 400px;
} }
/* Animación para la estrella de favorito */ /* Animation for the favorite star */
@keyframes favorite-pulse { @keyframes favorite-pulse {
0% { transform: scale(1); } 0% { transform: scale(1); }
50% { transform: scale(1.2); } 50% { transform: scale(1.2); }
+8 -8
View File
@@ -1,6 +1,6 @@
/* Estilos para la funcionalidad de archivos recientes */ /* Styles for the recent files feature */
/* Indicador de reciente */ /* Recent indicator */
.recent-indicator { .recent-indicator {
position: absolute; position: absolute;
top: 10px; top: 10px;
@@ -20,12 +20,12 @@
} }
} }
/* Estilos para los elementos de la vista de recientes */ /* Styles for the recent view items */
.recent-item { .recent-item {
position: relative; position: relative;
} }
/* Ajustes para la vista de cuadrícula */ /* Adjustments for grid view */
.file-card.recent-item { .file-card.recent-item {
border-left: 3px solid #6c757d; border-left: 3px solid #6c757d;
@@ -35,7 +35,7 @@
} }
} }
/* Ajustes para la vista de lista */ /* Adjustments for list view */
.file-item.recent-item { .file-item.recent-item {
position: relative; position: relative;
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px; grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
@@ -49,7 +49,7 @@
height: 30px; height: 30px;
} }
/* Estilos para el estado vacío específico de recientes */ /* Styles for the recent-specific empty state */
.recents-empty-state { .recents-empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -72,12 +72,12 @@
max-width: 400px; max-width: 400px;
} }
/* Tooltip para tiempo de acceso */ /* Tooltip for access time */
.recent-item .file-info { .recent-item .file-info {
cursor: help; cursor: help;
} }
/* Animación para archivos recientes */ /* Animation for recent files */
.recent-item { .recent-item {
animation: recent-fade-in 0.3s ease; animation: recent-fade-in 0.3s ease;
} }
+10 -10
View File
@@ -1271,7 +1271,7 @@ select:focus {
.file-icon.doc-icon { .file-icon.doc-icon {
width: 100px; width: 100px;
height: 70px; height: 70px;
background-color: #e2e8f0; /* Fondo gris claro */ background-color: #e2e8f0; /* Light gray background */
border-radius: 4px; border-radius: 4px;
position: relative; position: relative;
margin-bottom: 10px; margin-bottom: 10px;
@@ -1436,7 +1436,7 @@ select:focus {
.file-icon.code-icon { .file-icon.code-icon {
width: 100px; width: 100px;
height: 70px; height: 70px;
background-color: #e2e8f0; /* Fondo gris claro */ background-color: #e2e8f0; /* Light gray background */
border-radius: 4px; border-radius: 4px;
position: relative; position: relative;
margin-bottom: 10px; margin-bottom: 10px;
@@ -1770,7 +1770,7 @@ select:focus {
text-align: center; text-align: center;
} }
/* Lista de archivos - Estilo mejorado */ /* File list - Improved style */
.files-list-view { .files-list-view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1802,7 +1802,7 @@ select:focus {
background-color: white; background-color: white;
} }
/* Para modo papelera, ajustar columnas */ /* For trash mode, adjust columns */
.trash-item.file-item { .trash-item.file-item {
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 120px 100px; grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 120px 100px;
} }
@@ -1851,7 +1851,7 @@ select:focus {
display: none; display: none;
} }
/* Iconos específicos para archivos en vista de lista */ /* Specific icons for files in list view */
.file-item .file-icon.pdf-icon { .file-item .file-icon.pdf-icon {
background-color: #fee2e2; background-color: #fee2e2;
} }
@@ -2129,7 +2129,7 @@ select:focus {
transition: width 0.3s; transition: width 0.3s;
} }
/* Notificación */ /* Notification */
.notification { .notification {
position: absolute; position: absolute;
top: 70px; top: 70px;
@@ -3327,7 +3327,7 @@ header {
} }
} }
/* Estilos específicos para la página Shared */ /* Specific styles for the Shared page */
.page-description { .page-description {
color: #718096; color: #718096;
font-size: 16px; font-size: 16px;
@@ -3335,7 +3335,7 @@ header {
margin-bottom: 25px; margin-bottom: 25px;
} }
/* Mejoras para los filtros en la página Shared */ /* Improvements for the Shared page filters */
.shared-filters { .shared-filters {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -3383,7 +3383,7 @@ header {
width: 250px; width: 250px;
} }
/* Mejoras en la tabla de elementos compartidos */ /* Improvements in the shared items table */
.shared-list-container { .shared-list-container {
background-color: white; background-color: white;
border-radius: 10px; border-radius: 10px;
@@ -3412,7 +3412,7 @@ header {
vertical-align: middle; vertical-align: middle;
} }
/* Estado vacío mejorado */ /* Improved empty state */
.empty-state { .empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+28 -28
View File
@@ -27,7 +27,7 @@
<script src="/js/fileRenderer.js"></script> <script src="/js/fileRenderer.js"></script>
<script src="/js/fileSharing.js"></script> <script src="/js/fileSharing.js"></script>
<script src="/js/components/sharedView.js"></script> <script src="/js/components/sharedView.js"></script>
<!-- Los viewers se cargan al final para asegurar que document.body esté disponible --> <!-- Viewers are loaded last to ensure document.body is available -->
<script defer src="/js/fileViewer.js"></script> <script defer src="/js/fileViewer.js"></script>
<script defer src="/js/inlineViewer.js"></script> <script defer src="/js/inlineViewer.js"></script>
<script src="/js/app.js"></script> <script src="/js/app.js"></script>
@@ -94,7 +94,7 @@
<div class="top-bar"> <div class="top-bar">
<div class="search-container"> <div class="search-container">
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
<input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas..."> <input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Search files, folders...">
<button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search"> <button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
@@ -110,8 +110,8 @@
<div class="user-menu-header"> <div class="user-menu-header">
<div class="user-menu-avatar" id="user-menu-avatar">AD</div> <div class="user-menu-avatar" id="user-menu-avatar">AD</div>
<div class="user-menu-info"> <div class="user-menu-info">
<div class="user-menu-name" id="user-menu-name">Usuario</div> <div class="user-menu-name" id="user-menu-name">User</div>
<div class="user-menu-email" id="user-menu-email">usuario@oxicloud.app</div> <div class="user-menu-email" id="user-menu-email">user@oxicloud.app</div>
</div> </div>
</div> </div>
<div class="user-menu-role-badge" id="user-menu-role-badge" style="display:none"> <div class="user-menu-role-badge" id="user-menu-role-badge" style="display:none">
@@ -120,38 +120,38 @@
<div class="user-menu-storage"> <div class="user-menu-storage">
<div class="user-menu-storage-label"> <div class="user-menu-storage-label">
<i class="fas fa-database"></i> <i class="fas fa-database"></i>
<span data-i18n="storage.title">Almacenamiento</span> <span data-i18n="storage.title">Storage</span>
</div> </div>
<div class="user-menu-storage-bar"> <div class="user-menu-storage-bar">
<div class="user-menu-storage-fill" id="user-menu-storage-fill"></div> <div class="user-menu-storage-fill" id="user-menu-storage-fill"></div>
</div> </div>
<div class="user-menu-storage-text" id="user-menu-storage-text">0% usado</div> <div class="user-menu-storage-text" id="user-menu-storage-text">0% used</div>
</div> </div>
<div class="user-menu-divider"></div> <div class="user-menu-divider"></div>
<button class="user-menu-item user-menu-admin" id="user-menu-admin" style="display:none"> <button class="user-menu-item user-menu-admin" id="user-menu-admin" style="display:none">
<i class="fas fa-cogs"></i> <i class="fas fa-cogs"></i>
<span data-i18n="user_menu.admin_panel">Panel de administración</span> <span data-i18n="user_menu.admin_panel">Admin panel</span>
</button> </button>
<button class="user-menu-item" id="user-menu-profile"> <button class="user-menu-item" id="user-menu-profile">
<i class="fas fa-user-circle"></i> <i class="fas fa-user-circle"></i>
<span data-i18n="user_menu.profile">Mi perfil</span> <span data-i18n="user_menu.profile">My profile</span>
</button> </button>
<div class="user-menu-divider" id="user-menu-admin-divider" style="display:none"></div> <div class="user-menu-divider" id="user-menu-admin-divider" style="display:none"></div>
<button class="user-menu-item" id="user-menu-theme"> <button class="user-menu-item" id="user-menu-theme">
<i class="fas fa-moon"></i> <i class="fas fa-moon"></i>
<span data-i18n="user_menu.appearance">Apariencia</span> <span data-i18n="user_menu.appearance">Appearance</span>
<div class="theme-toggle-pill" id="theme-toggle-pill"> <div class="theme-toggle-pill" id="theme-toggle-pill">
<div class="theme-toggle-knob"></div> <div class="theme-toggle-knob"></div>
</div> </div>
</button> </button>
<button class="user-menu-item" id="user-menu-about"> <button class="user-menu-item" id="user-menu-about">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
<span data-i18n="user_menu.about">Acerca de OxiCloud</span> <span data-i18n="user_menu.about">About OxiCloud</span>
</button> </button>
<div class="user-menu-divider"></div> <div class="user-menu-divider"></div>
<button class="user-menu-item user-menu-logout" id="user-menu-logout"> <button class="user-menu-item user-menu-logout" id="user-menu-logout">
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt"></i>
<span data-i18n="actions.logout">Cerrar sesión</span> <span data-i18n="actions.logout">Log out</span>
</button> </button>
</div> </div>
</div> </div>
@@ -159,38 +159,38 @@
</div> </div>
<div class="content-area"> <div class="content-area">
<h1 class="page-title" data-i18n="nav.files">Archivos</h1> <h1 class="page-title" data-i18n="nav.files">Files</h1>
<div class="actions-bar"> <div class="actions-bar">
<div class="action-buttons"> <div class="action-buttons">
<div class="upload-dropdown" id="upload-dropdown"> <div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn"> <button class="btn btn-primary" id="upload-btn">
<i class="fas fa-cloud-upload-alt"></i> <i class="fas fa-cloud-upload-alt"></i>
<span data-i18n="actions.upload">Subir</span> <span data-i18n="actions.upload">Upload</span>
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i> <i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
</button> </button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu"> <div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn"> <button class="upload-dropdown-item" id="upload-files-btn">
<i class="fas fa-file"></i> <i class="fas fa-file"></i>
<span data-i18n="actions.upload_files">Subir archivos</span> <span data-i18n="actions.upload_files">Upload files</span>
</button> </button>
<button class="upload-dropdown-item" id="upload-folder-btn"> <button class="upload-dropdown-item" id="upload-folder-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
<span data-i18n="actions.upload_folder">Subir carpeta</span> <span data-i18n="actions.upload_folder">Upload folder</span>
</button> </button>
</div> </div>
</div> </div>
<button class="btn btn-secondary" id="new-folder-btn"> <button class="btn btn-secondary" id="new-folder-btn">
<i class="fas fa-folder-plus"></i> <i class="fas fa-folder-plus"></i>
<span data-i18n="actions.new_folder">Nueva carpeta</span> <span data-i18n="actions.new_folder">New folder</span>
</button> </button>
</div> </div>
<div class="view-toggle"> <div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula"> <button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i> <i class="fas fa-th"></i>
</button> </button>
<button class="toggle-btn" id="list-view-btn" title="Vista de lista"> <button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
</button> </button>
</div> </div>
@@ -198,7 +198,7 @@
<div class="dropzone" id="dropzone"> <div class="dropzone" id="dropzone">
<i class="fas fa-cloud-upload-alt" style="font-size: 32px; margin-bottom: 10px;"></i> <i class="fas fa-cloud-upload-alt" style="font-size: 32px; margin-bottom: 10px;"></i>
<p data-i18n="dropzone.drag_files">Arrastra archivos aquí o haz clic para seleccionar</p> <p data-i18n="dropzone.drag_files">Drag files here or click to select</p>
<input type="file" id="file-input" style="display: none;" multiple> <input type="file" id="file-input" style="display: none;" multiple>
<input type="file" id="folder-input" style="display: none;" webkitdirectory directory multiple> <input type="file" id="folder-input" style="display: none;" webkitdirectory directory multiple>
<div class="upload-progress"> <div class="upload-progress">
@@ -222,10 +222,10 @@
<!-- List View (hidden by default) --> <!-- List View (hidden by default) -->
<div class="files-list-view" id="files-list-view" style="display: none;"> <div class="files-list-view" id="files-list-view" style="display: none;">
<div class="list-header"> <div class="list-header">
<div data-i18n="files.name">Nombre</div> <div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Tipo</div> <div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Tamaño</div> <div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modificado</div> <div data-i18n="files.modified">Modified</div>
</div> </div>
<!-- Files will be populated here in list view --> <!-- Files will be populated here in list view -->
</div> </div>
@@ -240,18 +240,18 @@
<div class="modal-icon"> <div class="modal-icon">
<i id="modal-icon" class="fas fa-folder-plus"></i> <i id="modal-icon" class="fas fa-folder-plus"></i>
</div> </div>
<h3 id="modal-title" data-i18n="dialogs.new_folder_title">Nueva carpeta</h3> <h3 id="modal-title" data-i18n="dialogs.new_folder_title">New folder</h3>
<button class="modal-close-btn" id="modal-close-btn"> <button class="modal-close-btn" id="modal-close-btn">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<label id="modal-label" for="modal-input" data-i18n="dialogs.folder_name">Nombre de la carpeta</label> <label id="modal-label" for="modal-input" data-i18n="dialogs.folder_name">Folder name</label>
<input type="text" id="modal-input" class="modal-input" placeholder="" autocomplete="off"> <input type="text" id="modal-input" class="modal-input" placeholder="" autocomplete="off">
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" id="modal-cancel-btn" data-i18n="actions.cancel">Cancelar</button> <button class="btn btn-secondary" id="modal-cancel-btn" data-i18n="actions.cancel">Cancel</button>
<button class="btn btn-primary" id="modal-confirm-btn" data-i18n="actions.confirm">Crear</button> <button class="btn btn-primary" id="modal-confirm-btn" data-i18n="actions.confirm">Create</button>
</div> </div>
</div> </div>
</div> </div>
@@ -281,7 +281,7 @@
<i class="fas fa-file-alt"></i> MIT License <i class="fas fa-file-alt"></i> MIT License
</a> </a>
</div> </div>
<button class="about-close-btn" id="about-close-btn" data-i18n="actions.close">Cerrar</button> <button class="about-close-btn" id="about-close-btn" data-i18n="actions.close">Close</button>
</div> </div>
</div> </div>
</body> </body>
+69 -69
View File
@@ -197,7 +197,7 @@ function setupUserMenu() {
// Theme switching could be expanded here in the future // Theme switching could be expanded here in the future
window.ui.showNotification( window.ui.showNotification(
dark ? '🌙' : '☀️', 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'; if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
// Update UI // 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 = ` elements.actionsBar.innerHTML = `
<div class="action-buttons"> <div class="action-buttons">
<button class="btn btn-danger" id="empty-trash-btn"> <button class="btn btn-danger" id="empty-trash-btn">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Vaciar papelera'}</span> <span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Empty trash'}</span>
</button> </button>
</div> </div>
`; `;
@@ -519,35 +519,35 @@ function setupEventListeners() {
app.currentSection = 'files'; app.currentSection = 'files';
// Reset UI // 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 = ` elements.actionsBar.innerHTML = `
<div class="action-buttons"> <div class="action-buttons">
<div class="upload-dropdown" id="upload-dropdown"> <div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn"> <button class="btn btn-primary" id="upload-btn">
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i> <i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
<span data-i18n="actions.upload">Subir</span> <span data-i18n="actions.upload">Upload</span>
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i> <i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
</button> </button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu"> <div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn"> <button class="upload-dropdown-item" id="upload-files-btn">
<i class="fas fa-file"></i> <i class="fas fa-file"></i>
<span data-i18n="actions.upload_files">Subir archivos</span> <span data-i18n="actions.upload_files">Upload files</span>
</button> </button>
<button class="upload-dropdown-item" id="upload-folder-btn"> <button class="upload-dropdown-item" id="upload-folder-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
<span data-i18n="actions.upload_folder">Subir carpeta</span> <span data-i18n="actions.upload_folder">Upload folder</span>
</button> </button>
</div> </div>
</div> </div>
<button class="btn btn-secondary" id="new-folder-btn"> <button class="btn btn-secondary" id="new-folder-btn">
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">Nueva carpeta</span> <i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
</button> </button>
</div> </div>
<div class="view-toggle"> <div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula"> <button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i> <i class="fas fa-th"></i>
</button> </button>
<button class="toggle-btn" id="list-view-btn" title="Vista de lista"> <button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
</button> </button>
</div> </div>
@@ -623,14 +623,14 @@ function setupEventListeners() {
*/ */
async function loadFiles(options = {}) { async function loadFiles(options = {}) {
try { 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; const forceRefresh = options.forceRefresh || false;
// Prevenir múltiples solicitudes de carga simultáneas // Prevent multiple simultaneous load requests
if (window.isLoadingFiles) { 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; return;
} }
@@ -640,7 +640,7 @@ async function loadFiles(options = {}) {
elements.filesGrid.innerHTML = ` elements.filesGrid.innerHTML = `
<div class="files-loading-spinner"> <div class="files-loading-spinner">
<div class="spinner"></div> <div class="spinner"></div>
<span>${window.i18n ? window.i18n.t('files.loading') : 'Cargando archivos…'}</span> <span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
</div> </div>
`; `;
@@ -651,12 +651,12 @@ async function loadFiles(options = {}) {
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
if (userData.username) { if (userData.username) {
// Find user's home folder // 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); await findUserHomeFolder(userData.username);
} }
} }
// Agregar timestamp para evitar caché // Add timestamp to avoid cache
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
let url; let url;
@@ -667,7 +667,7 @@ async function loadFiles(options = {}) {
url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`; url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`;
app.currentPath = app.userHomeFolderId; app.currentPath = app.userHomeFolderId;
ui.updateBreadcrumb(app.userHomeFolderName || 'Home'); 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 { } else {
// Emergency fallback - this should rarely happen but prevents errors // Emergency fallback - this should rarely happen but prevents errors
url = `/api/folders?t=${timestamp}`; url = `/api/folders?t=${timestamp}`;
@@ -676,7 +676,7 @@ async function loadFiles(options = {}) {
} else { } else {
// Normal case - viewing subfolder contents // Normal case - viewing subfolder contents
url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`; 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'); const token = localStorage.getItem('oxicloud_token');
@@ -686,14 +686,14 @@ async function loadFiles(options = {}) {
'Cache-Control': 'no-cache, no-store, must-revalidate', 'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache' '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) { if (forceRefresh) {
url += `&force_refresh=true`; url += `&force_refresh=true`;
requestOptions.headers['X-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}`); console.log(`Loading files from ${url}`);
@@ -703,13 +703,13 @@ async function loadFiles(options = {}) {
if (response.status === 401 || response.status === 403) { if (response.status === 401 || response.status === 403) {
console.warn("Auth error when loading files, showing empty list"); console.warn("Auth error when loading files, showing empty list");
// Just show empty state instead of causing redirect loops // Just show empty state instead of causing redirect loops
elements.filesGrid.innerHTML = '<div class="empty-state"><p>No se pudieron cargar los archivos</p></div>'; elements.filesGrid.innerHTML = '<div class="empty-state"><p>Could not load files</p></div>';
elements.filesListView.innerHTML = ` elements.filesListView.innerHTML = `
<div class="list-header"> <div class="list-header">
<div>Nombre</div> <div>Name</div>
<div>Tipo</div> <div>Type</div>
<div>Tamaño</div> <div>Size</div>
<div>Modificado</div> <div>Modified</div>
</div> </div>
`; `;
return; return;
@@ -724,10 +724,10 @@ async function loadFiles(options = {}) {
elements.filesGrid.innerHTML = ''; elements.filesGrid.innerHTML = '';
elements.filesListView.innerHTML = ` elements.filesListView.innerHTML = `
<div class="list-header"> <div class="list-header">
<div data-i18n="files.name">Nombre</div> <div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Tipo</div> <div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Tamaño</div> <div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modificado</div> <div data-i18n="files.modified">Modified</div>
</div> </div>
`; `;
@@ -766,11 +766,11 @@ async function loadFiles(options = {}) {
// Also load files in this folder // Also load files in this folder
const cacheTimestamp = new Date().getTime(); 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) { if (app.currentPath) {
filesUrl += `&folder_id=${app.currentPath}`; filesUrl += `&folder_id=${app.currentPath}`;
} }
console.log(`Cargando archivos desde: ${filesUrl}`); console.log(`Loading files from: ${filesUrl}`);
try { try {
console.log(`Fetching files from: ${filesUrl}`); console.log(`Fetching files from: ${filesUrl}`);
@@ -810,7 +810,7 @@ async function loadFiles(options = {}) {
console.error('Error loading folders:', error); console.error('Error loading folders:', error);
ui.showNotification('Error', 'Could not load files and folders'); ui.showNotification('Error', 'Could not load files and folders');
} finally { } 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; window.isLoadingFiles = false;
} }
} }
@@ -839,11 +839,11 @@ async function loadTrashItems() {
elements.filesGrid.innerHTML = ''; elements.filesGrid.innerHTML = '';
elements.filesListView.innerHTML = ` elements.filesListView.innerHTML = `
<div class="list-header"> <div class="list-header">
<div data-i18n="files.name">Nombre</div> <div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Tipo</div> <div data-i18n="files.type">Type</div>
<div data-i18n="trash.original_location">Ubicación original</div> <div data-i18n="trash.original_location">Original location</div>
<div data-i18n="trash.deleted_date">Fecha eliminación</div> <div data-i18n="trash.deleted_date">Deletion date</div>
<div data-i18n="trash.actions">Acciones</div> <div data-i18n="trash.actions">Actions</div>
</div> </div>
`; `;
@@ -864,7 +864,7 @@ async function loadTrashItems() {
emptyState.className = 'empty-state'; emptyState.className = 'empty-state';
emptyState.innerHTML = ` emptyState.innerHTML = `
<i class="fas fa-trash" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i> <i class="fas fa-trash" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<p>${window.i18n ? window.i18n.t('trash.empty_state') : 'La papelera está vacía'}</p> <p>${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}</p>
`; `;
elements.filesGrid.appendChild(emptyState); elements.filesGrid.appendChild(emptyState);
return; return;
@@ -877,7 +877,7 @@ async function loadTrashItems() {
} catch (error) { } catch (error) {
console.error('Error loading trash items:', 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 // Item type label
const typeLabel = isFile ? const typeLabel = isFile ?
(window.i18n ? window.i18n.t('files.file_types.file') : 'Archivo') : (window.i18n ? window.i18n.t('files.file_types.file') : 'File') :
(window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'); (window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder');
// Grid view element // Grid view element
const gridElement = document.createElement('div'); const gridElement = document.createElement('div');
@@ -912,10 +912,10 @@ function addTrashItemToView(item) {
<div class="file-name">${item.name}</div> <div class="file-name">${item.name}</div>
<div class="file-info">${typeLabel} - ${formattedDate}</div> <div class="file-info">${typeLabel} - ${formattedDate}</div>
<div class="trash-actions"> <div class="trash-actions">
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}"> <button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</button> </button>
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}"> <button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</div> </div>
@@ -956,10 +956,10 @@ function addTrashItemToView(item) {
<div class="path-cell">${item.original_path || '--'}</div> <div class="path-cell">${item.original_path || '--'}</div>
<div class="date-cell">${formattedDate}</div> <div class="date-cell">${formattedDate}</div>
<div class="actions-cell"> <div class="actions-cell">
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}"> <button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</button> </button>
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}"> <button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</div> </div>
@@ -995,7 +995,7 @@ async function performSearch(query) {
app.isSearchMode = true; app.isSearchMode = true;
// Set breadcrumb for search // Set breadcrumb for search
ui.updateBreadcrumb(`Búsqueda: "${query}"`); ui.updateBreadcrumb(`Search: "${query}"`);
// Prepare search options // Prepare search options
const options = { const options = {
@@ -1037,7 +1037,7 @@ async function performSearch(query) {
} catch (error) { } catch (error) {
console.error('Search error:', 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 // 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 // Clear breadcrumb and show root
ui.updateBreadcrumb(''); ui.updateBreadcrumb('');
@@ -1105,7 +1105,7 @@ function switchToFilesView() {
app.currentSection = 'files'; app.currentSection = 'files';
// Update UI // 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 // Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active')); elements.navItems.forEach(navItem => navItem.classList.remove('active'));
@@ -1122,29 +1122,29 @@ function switchToFilesView() {
<div class="upload-dropdown" id="upload-dropdown"> <div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn"> <button class="btn btn-primary" id="upload-btn">
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i> <i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
<span data-i18n="actions.upload">Subir</span> <span data-i18n="actions.upload">Upload</span>
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i> <i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
</button> </button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu"> <div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn"> <button class="upload-dropdown-item" id="upload-files-btn">
<i class="fas fa-file"></i> <i class="fas fa-file"></i>
<span data-i18n="actions.upload_files">Subir archivos</span> <span data-i18n="actions.upload_files">Upload files</span>
</button> </button>
<button class="upload-dropdown-item" id="upload-folder-btn"> <button class="upload-dropdown-item" id="upload-folder-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
<span data-i18n="actions.upload_folder">Subir carpeta</span> <span data-i18n="actions.upload_folder">Upload folder</span>
</button> </button>
</div> </div>
</div> </div>
<button class="btn btn-secondary" id="new-folder-btn"> <button class="btn btn-secondary" id="new-folder-btn">
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">Nueva carpeta</span> <i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
</button> </button>
</div> </div>
<div class="view-toggle"> <div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula"> <button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i> <i class="fas fa-th"></i>
</button> </button>
<button class="toggle-btn" id="list-view-btn" title="Vista de lista"> <button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
</button> </button>
</div> </div>
@@ -1219,7 +1219,7 @@ function switchToFavoritesView() {
} }
// Update UI // 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 // Clear breadcrumb and show root
ui.updateBreadcrumb(''); ui.updateBreadcrumb('');
@@ -1235,10 +1235,10 @@ function switchToFavoritesView() {
<!-- No actions needed for favorites view --> <!-- No actions needed for favorites view -->
</div> </div>
<div class="view-toggle"> <div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula"> <button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i> <i class="fas fa-th"></i>
</button> </button>
<button class="toggle-btn" id="list-view-btn" title="Vista de lista"> <button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
</button> </button>
</div> </div>
@@ -1278,7 +1278,7 @@ function switchToFavoritesView() {
filesGrid.innerHTML = ` filesGrid.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i> <i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
<p>Error al cargar el módulo de favoritos</p> <p>Error loading the favorites module</p>
</div> </div>
`; `;
} }
@@ -1308,7 +1308,7 @@ function switchToRecentFilesView() {
} }
// Update UI // 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 // Clear breadcrumb and show root
ui.updateBreadcrumb(''); ui.updateBreadcrumb('');
@@ -1322,14 +1322,14 @@ function switchToRecentFilesView() {
elements.actionsBar.innerHTML = ` elements.actionsBar.innerHTML = `
<div class="action-buttons"> <div class="action-buttons">
<button class="btn btn-secondary" id="clear-recent-btn"> <button class="btn btn-secondary" id="clear-recent-btn">
<i class="fas fa-broom" style="margin-right: 5px;"></i> <span data-i18n="actions.clear_recent">Limpiar recientes</span> <i class="fas fa-broom" style="margin-right: 5px;"></i> <span data-i18n="actions.clear_recent">Clear recent</span>
</button> </button>
</div> </div>
<div class="view-toggle"> <div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula"> <button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i> <i class="fas fa-th"></i>
</button> </button>
<button class="toggle-btn" id="list-view-btn" title="Vista de lista"> <button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
</button> </button>
</div> </div>
@@ -1341,7 +1341,7 @@ function switchToRecentFilesView() {
if (window.recent) { if (window.recent) {
window.recent.clearRecentFiles(); window.recent.clearRecentFiles();
window.recent.displayRecentFiles(); 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 = ` filesGrid.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i> <i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
<p>Error al cargar el módulo de archivos recientes</p> <p>Error loading the recent files module</p>
</div> </div>
`; `;
} }
@@ -1450,7 +1450,7 @@ window.refreshUserData = refreshUserData;
function showUserProfileModal() { function showUserProfileModal() {
const USER_DATA_KEY = 'oxicloud_user'; const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const username = userData.username || 'Usuario'; const username = userData.username || 'User';
const email = userData.email || ''; const email = userData.email || '';
const role = userData.role || 'user'; const role = userData.role || 'user';
const initials = username.substring(0, 2).toUpperCase(); const initials = username.substring(0, 2).toUpperCase();
@@ -1847,7 +1847,7 @@ async function findUserHomeFolder(username) {
* Logout - clear all auth data and redirect to login * Logout - clear all auth data and redirect to login
*/ */
function logout() { function logout() {
// Nombres de variables según auth.js // Variable names as per auth.js
const TOKEN_KEY = 'oxicloud_token'; const TOKEN_KEY = 'oxicloud_token';
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token'; const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry'; const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
+20 -20
View File
@@ -34,7 +34,7 @@ const LANGUAGE_TEXTS = {
subtitle: 'Por favor, selecciona tu idioma', subtitle: 'Por favor, selecciona tu idioma',
continue: 'Continuar', continue: 'Continuar',
autodetected: 'Hemos detectado tu idioma', autodetected: 'Hemos detectado tu idioma',
moreLanguages: 'Más idiomas...', moreLanguages: 'More languages...',
modalTitle: 'Seleccionar idioma', modalTitle: 'Seleccionar idioma',
searchPlaceholder: 'Buscar idioma...' searchPlaceholder: 'Buscar idioma...'
}, },
@@ -494,8 +494,8 @@ document.addEventListener('DOMContentLoaded', () => {
console.error('Error showing initial panel:', err); console.error('Error showing initial panel:', err);
}); });
// Siempre limpiar los contadores al cargar la página de login // Always clear counters when loading the login page
// para asegurar que no quedamos atrapados en un bucle // to ensure we don't get trapped in a loop
console.log('Login page loaded, clearing all counters'); console.log('Login page loaded, clearing all counters');
sessionStorage.removeItem('redirect_count'); sessionStorage.removeItem('redirect_count');
localStorage.removeItem('refresh_attempts'); localStorage.removeItem('refresh_attempts');
@@ -589,14 +589,14 @@ if (isLoginPage && loginForm) {
localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); 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; let parsedExpiry = false;
const tokenParts = token.split('.'); const tokenParts = token.split('.');
if (tokenParts.length === 3) { if (tokenParts.length === 3) {
try { try {
const payload = JSON.parse(atob(tokenParts[1])); const payload = JSON.parse(atob(tokenParts[1]));
if (payload.exp) { if (payload.exp) {
// payload.exp está en segundos desde epoch // payload.exp is in seconds since epoch
const expiryDate = new Date(payload.exp * 1000); const expiryDate = new Date(payload.exp * 1000);
// Verify the date is valid // Verify the date is valid
@@ -642,7 +642,7 @@ if (isLoginPage && loginForm) {
// Redirect to main app // Redirect to main app
redirectToMainApp(); redirectToMainApp();
} catch (error) { } catch (error) {
loginError.textContent = error.message || 'Error al iniciar sesión'; loginError.textContent = error.message || 'Error logging in';
loginError.style.display = 'block'; loginError.style.display = 'block';
} }
}); });
@@ -664,7 +664,7 @@ if (isLoginPage && registerForm) {
// Validate passwords match // Validate passwords match
if (password !== confirmPassword) { 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.textContent = errorMsg;
registerError.style.display = 'block'; registerError.style.display = 'block';
return; return;
@@ -674,7 +674,7 @@ if (isLoginPage && registerForm) {
const data = await register(username, email, password); const data = await register(username, email, password);
// Show success message // 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.textContent = successMsg;
registerSuccess.style.display = 'block'; registerSuccess.style.display = 'block';
@@ -687,7 +687,7 @@ if (isLoginPage && registerForm) {
registerPanel.style.display = 'none'; registerPanel.style.display = 'none';
}, 2000); }, 2000);
} catch (error) { } 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.textContent = error.message || errorMsg;
registerError.style.display = 'block'; registerError.style.display = 'block';
} }
@@ -710,7 +710,7 @@ if (isLoginPage && adminSetupForm) {
// Validate passwords match // Validate passwords match
if (password !== confirmPassword) { 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.textContent = errorMsg;
adminSetupError.style.display = 'block'; adminSetupError.style.display = 'block';
return; return;
@@ -721,7 +721,7 @@ if (isLoginPage && adminSetupForm) {
const data = await register('admin', email, password, 'admin'); const data = await register('admin', email, password, 'admin');
// Show success message in the GUI instead of alert // 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) { if (adminSetupSuccess) {
adminSetupSuccess.textContent = successMsg; adminSetupSuccess.textContent = successMsg;
@@ -736,7 +736,7 @@ if (isLoginPage && adminSetupForm) {
}, 2000); }, 2000);
} catch (error) { } 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.textContent = error.message || errorMsg;
adminSetupError.style.display = 'block'; adminSetupError.style.display = 'block';
} }
@@ -792,10 +792,10 @@ async function login(username, password) {
if (!response.ok) { if (!response.ok) {
try { try {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || 'Falló la autenticación'); throw new Error(errorData.error || 'Authentication failed');
} catch (jsonError) { } catch (jsonError) {
// If the error response is not valid JSON // 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; return data;
} catch (jsonError) { } catch (jsonError) {
console.error('Error parsing login response:', 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) { } catch (error) {
console.error('Login error:', error); console.error('Login error:', error);
@@ -848,10 +848,10 @@ async function register(username, email, password, role = 'user') {
if (!response.ok) { if (!response.ok) {
try { try {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || 'Error en el registro'); throw new Error(errorData.error || 'Registration error');
} catch (jsonError) { } catch (jsonError) {
// If the error response is not valid JSON // 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; return data;
} catch (jsonError) { } catch (jsonError) {
console.error('Error parsing registration response:', 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) { } catch (error) {
console.error('Registration error:', error); console.error('Registration error:', error);
@@ -883,7 +883,7 @@ async function fetchUserData(token) {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Error al obtener datos del usuario'); throw new Error('Error fetching user data');
} }
return await response.json(); return await response.json();
@@ -1092,7 +1092,7 @@ function redirectToMainApp() {
window.location.href = '/login.html?error=redirect_fatal'; window.location.href = '/login.html?error=redirect_fatal';
} catch (e) { } catch (e) {
// Nothing more we can do // 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.');
} }
} }
+26 -26
View File
@@ -30,7 +30,7 @@ const contextMenus = {
window.favorites.removeFromFavorites(folder.id, 'folder'); window.favorites.removeFromFavorites(folder.id, 'folder');
// Update menu item text // Update menu item text
document.getElementById('favorite-folder-option').querySelector('span').textContent = 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 { } else {
// Add to favorites // Add to favorites
window.favorites.addToFavorites( window.favorites.addToFavorites(
@@ -41,7 +41,7 @@ const contextMenus = {
); );
// Update menu item text // Update menu item text
document.getElementById('favorite-folder-option').querySelector('span').textContent = 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(); window.ui.closeContextMenu();
@@ -143,7 +143,7 @@ const contextMenus = {
window.favorites.removeFromFavorites(file.id, 'file'); window.favorites.removeFromFavorites(file.id, 'file');
// Update menu item text // Update menu item text
document.getElementById('favorite-file-option').querySelector('span').textContent = 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 { } else {
// Add to favorites // Add to favorites
window.favorites.addToFavorites( window.favorites.addToFavorites(
@@ -154,7 +154,7 @@ const contextMenus = {
); );
// Update menu item text // Update menu item text
document.getElementById('favorite-file-option').querySelector('span').textContent = 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(); window.ui.closeFileContextMenu();
@@ -246,7 +246,7 @@ const contextMenus = {
renameInput.value = folder.name; renameInput.value = folder.name;
// Update header text // Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span'); 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'; renameDialog.style.display = 'flex';
renameInput.focus(); renameInput.focus();
renameInput.select(); renameInput.select();
@@ -264,7 +264,7 @@ const contextMenus = {
renameInput.value = file.name; renameInput.value = file.name;
// Update header text // Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span'); 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'; renameDialog.style.display = 'flex';
renameInput.focus(); renameInput.focus();
renameInput.select(); renameInput.select();
@@ -293,8 +293,8 @@ const contextMenus = {
// Update dialog title (preserve icon) // Update dialog title (preserve icon)
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header'); const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
const titleText = mode === 'file' ? const titleText = mode === 'file' ?
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Mover archivo') : (window.i18n ? window.i18n.t('dialogs.move_file') : 'Move file') :
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Mover carpeta'); (window.i18n ? window.i18n.t('dialogs.move_folder') : 'Move folder');
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i> <span>${titleText}</span>`; dialogHeader.innerHTML = `<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i> <span>${titleText}</span>`;
// Load all available folders // Load all available folders
@@ -319,7 +319,7 @@ const contextMenus = {
async renameItem() { async renameItem() {
const newName = document.getElementById('rename-input').value.trim(); const newName = document.getElementById('rename-input').value.trim();
if (!newName) { 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; return;
} }
@@ -361,7 +361,7 @@ const contextMenus = {
// Clear container except root option // Clear container except root option
folderSelectContainer.innerHTML = ` folderSelectContainer.innerHTML = `
<div class="folder-select-item selected" data-folder-id=""> <div class="folder-select-item selected" data-folder-id="">
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Raíz</span> <i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Root</span>
</div> </div>
`; `;
@@ -438,8 +438,8 @@ const contextMenus = {
// Update dialog content // Update dialog content
dialogHeader.textContent = itemType === 'file' ? dialogHeader.textContent = itemType === 'file' ?
(window.i18n ? window.i18n.t('dialogs.share_file') : 'Compartir archivo') : (window.i18n ? window.i18n.t('dialogs.share_file') : 'Share file') :
(window.i18n ? window.i18n.t('dialogs.share_folder') : 'Compartir carpeta'); (window.i18n ? window.i18n.t('dialogs.share_folder') : 'Share folder');
itemName.textContent = item.name; itemName.textContent = item.name;
@@ -470,21 +470,21 @@ const contextMenus = {
shareEl.className = 'existing-share-item'; shareEl.className = 'existing-share-item';
const expiresText = share.expires_at ? const expiresText = share.expires_at ?
`Vence: ${window.fileSharing.formatExpirationDate(share.expires_at)}` : `Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
'Sin vencimiento'; 'No expiration';
shareEl.innerHTML = ` shareEl.innerHTML = `
<div class="share-url">${share.url}</div> <div class="share-url">${share.url}</div>
<div class="share-info"> <div class="share-info">
${share.password_protected ? '<span class="share-protected"><i class="fas fa-lock"></i> Con contraseña</span>' : ''} ${share.password_protected ? '<span class="share-protected"><i class="fas fa-lock"></i> Password protected</span>' : ''}
<span class="share-expiration">${expiresText}</span> <span class="share-expiration">${expiresText}</span>
</div> </div>
<div class="share-actions"> <div class="share-actions">
<button class="btn btn-small copy-link-btn" data-share-url="${share.url}"> <button class="btn btn-small copy-link-btn" data-share-url="${share.url}">
<i class="fas fa-copy"></i> Copiar <i class="fas fa-copy"></i> Copy
</button> </button>
<button class="btn btn-small btn-danger delete-link-btn" data-share-id="${share.id}"> <button class="btn btn-small btn-danger delete-link-btn" data-share-id="${share.id}">
<i class="fas fa-trash"></i> Eliminar <i class="fas fa-trash"></i> Delete
</button> </button>
</div> </div>
`; `;
@@ -507,9 +507,9 @@ const contextMenus = {
const shareId = btn.getAttribute('data-share-id'); const shareId = btn.getAttribute('data-share-id');
showConfirmDialog({ showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Eliminar enlace', title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Delete link',
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : '¿Estás seguro de que quieres eliminar este enlace compartido?', 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') : 'Eliminar', confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
}).then(confirmed => { }).then(confirmed => {
if (confirmed) { if (confirmed) {
window.fileSharing.removeSharedLink(shareId); window.fileSharing.removeSharedLink(shareId);
@@ -534,7 +534,7 @@ const contextMenus = {
*/ */
createSharedLink() { createSharedLink() {
if (!window.app.shareDialogItem || !window.app.shareDialogItemType) { 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; return;
} }
@@ -577,14 +577,14 @@ const contextMenus = {
shareUrl.select(); shareUrl.select();
// Show success message // Show success message
window.ui.showNotification('Enlace creado', 'Enlace compartido creado correctamente'); window.ui.showNotification('Link created', 'Shared link created successfully');
// Reload existing shares // Reload existing shares
this.showShareDialog(item, itemType); this.showShareDialog(item, itemType);
} catch (error) { } catch (error) {
console.error('Error creating shared link:', 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; const shareUrl = window.app.notificationShareUrl;
if (!email || !shareUrl) { 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; return;
} }
// Validate email format // Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) { 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; return;
} }
@@ -630,7 +630,7 @@ const contextMenus = {
document.getElementById('notification-dialog').style.display = 'none'; document.getElementById('notification-dialog').style.display = 'none';
} catch (error) { } catch (error) {
console.error('Error sending notification:', 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');
} }
}, },
+17 -17
View File
@@ -230,8 +230,8 @@ const favorites = {
// Show success notification // Show success notification
window.ui.showNotification( window.ui.showNotification(
'Añadido a favoritos', 'Added to favorites',
`"${name}" añadido a favoritos` `"${name}" added to favorites`
); );
return true; return true;
@@ -271,8 +271,8 @@ const favorites = {
// Show success notification if item was found // Show success notification if item was found
if (item) { if (item) {
window.ui.showNotification( window.ui.showNotification(
'Eliminado de favoritos', 'Removed from favorites',
`"${item.name}" eliminado de favoritos` `"${item.name}" removed from favorites`
); );
} }
return true; return true;
@@ -310,10 +310,10 @@ const favorites = {
filesGrid.innerHTML = ''; filesGrid.innerHTML = '';
filesListView.innerHTML = ` filesListView.innerHTML = `
<div class="list-header"> <div class="list-header">
<div data-i18n="files.name">Nombre</div> <div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Tipo</div> <div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Tamaño</div> <div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modificado</div> <div data-i18n="files.modified">Modified</div>
</div> </div>
`; `;
@@ -326,8 +326,8 @@ const favorites = {
emptyState.className = 'empty-state'; emptyState.className = 'empty-state';
emptyState.innerHTML = ` emptyState.innerHTML = `
<i class="fas fa-star" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i> <i class="fas fa-star" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No hay elementos favoritos'}</p> <p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}</p>
<p>${window.i18n ? window.i18n.t('favorites.empty_hint') : 'Para marcar como favorito, haz clic derecho en cualquier archivo o carpeta'}</p> <p>${window.i18n ? window.i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
`; `;
filesGrid.appendChild(emptyState); filesGrid.appendChild(emptyState);
return; return;
@@ -359,7 +359,7 @@ const favorites = {
} catch (error) { } catch (error) {
console.error('Error displaying favorites:', 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 = {
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
</div> </div>
<div class="file-name">${folder.name}</div> <div class="file-name">${folder.name}</div>
<div class="file-info">Carpeta</div> <div class="file-info">Folder</div>
`; `;
// Click to navigate // Click to navigate
@@ -518,7 +518,7 @@ const favorites = {
</div> </div>
<span>${folder.name}</span> <span>${folder.name}</span>
</div> </div>
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'}</div> <div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
<div class="size-cell">--</div> <div class="size-cell">--</div>
<div class="date-cell">${formattedDate}</div> <div class="date-cell">${formattedDate}</div>
`; `;
@@ -559,17 +559,17 @@ const favorites = {
// Determine icon and type // Determine icon and type
let iconClass = 'fas fa-file'; let iconClass = 'fas fa-file';
let iconSpecialClass = ''; let iconSpecialClass = '';
let typeLabel = 'Documento'; let typeLabel = 'Document';
if (file.mime_type) { if (file.mime_type) {
if (file.mime_type.startsWith('image/')) { if (file.mime_type.startsWith('image/')) {
iconClass = 'fas fa-file-image'; iconClass = 'fas fa-file-image';
iconSpecialClass = 'image-icon'; 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/')) { } else if (file.mime_type.startsWith('text/')) {
iconClass = 'fas fa-file-alt'; iconClass = 'fas fa-file-alt';
iconSpecialClass = 'text-icon'; 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/')) { } else if (file.mime_type.startsWith('video/')) {
iconClass = 'fas fa-file-video'; iconClass = 'fas fa-file-video';
iconSpecialClass = 'video-icon'; iconSpecialClass = 'video-icon';
@@ -606,7 +606,7 @@ const favorites = {
<i class="${iconClass}"></i> <i class="${iconClass}"></i>
</div> </div>
<div class="file-name">${file.name}</div> <div class="file-name">${file.name}</div>
<div class="file-info">Modificado ${formattedDate.split(' ')[0]}</div> <div class="file-info">Modified ${formattedDate.split(' ')[0]}</div>
`; `;
// Download on click // Download on click
+91 -91
View File
@@ -49,8 +49,8 @@ const fileOps = {
try { try {
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`); console.log(`Uploading file to folder: ${targetFolderId || 'root'}`);
// Usamos la URL correcta para la subida de archivos // We use the correct URL for file upload
console.log('Formulario a enviar:', { console.log('Form to submit:', {
file: file.name, file: file.name,
size: file.size, size: file.size,
folder_id: targetFolderId || 'root' folder_id: targetFolderId || 'root'
@@ -59,16 +59,16 @@ const fileOps = {
const response = await fetch('/api/files/upload', { const response = await fetch('/api/files/upload', {
method: 'POST', method: 'POST',
body: formData, 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', cache: 'no-store',
headers: { headers: {
...getAuthHeaders(), ...getAuthHeaders(),
// Agregar este encabezado para forzar recargas frescas // Add this header to force fresh reloads
'Cache-Control': 'no-cache, no-store, must-revalidate' 'Cache-Control': 'no-cache, no-store, must-revalidate'
} }
}); });
console.log('Respuesta del servidor:', { console.log('Server response:', {
status: response.status, status: response.status,
statusText: response.statusText statusText: response.statusText
}); });
@@ -83,11 +83,11 @@ const fileOps = {
console.log(`Successfully uploaded ${file.name}`, responseData); console.log(`Successfully uploaded ${file.name}`, responseData);
// Show success notification immediately // Show success notification immediately
window.ui.showNotification('Archivo subido', `${file.name} completado`); window.ui.showNotification('File uploaded', `${file.name} completed`);
if (i === totalFiles - 1) { if (i === totalFiles - 1) {
// Last file uploaded - wait and reload once // 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 // Wait for backend to persist
await new Promise(resolve => setTimeout(resolve, 800)); await new Promise(resolve => setTimeout(resolve, 800));
@@ -96,7 +96,7 @@ const fileOps = {
try { try {
await window.loadFiles({forceRefresh: true}); await window.loadFiles({forceRefresh: true});
} catch (reloadError) { } catch (reloadError) {
console.error("Error recargando archivos:", reloadError); console.error("Error reloading files:", reloadError);
} }
// Hide upload UI // Hide upload UI
@@ -109,11 +109,11 @@ const fileOps = {
} else { } else {
const errorData = await response.text(); const errorData = await response.text();
console.error('Upload error:', errorData); 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) { } catch (error) {
console.error('Network error during upload:', 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}`); console.log(`Created folder: ${folderPath} -> ${folder.id}`);
} else { } else {
console.error(`Error creating folder ${folderPath}:`, await response.text()); 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) { } catch (error) {
console.error(`Network error creating folder ${folderPath}:`, error); console.error(`Network error creating folder ${folderPath}:`, error);
@@ -228,7 +228,7 @@ const fileOps = {
} }
// Finish up // 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)); await new Promise(resolve => setTimeout(resolve, 800));
@@ -253,7 +253,7 @@ const fileOps = {
try { try {
console.log('Creating folder with name:', name); 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', { const response = await fetch('/api/folders', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -268,28 +268,28 @@ const fileOps = {
}); });
if (response.ok) { if (response.ok) {
// Obtener la carpeta creada del backend // Get the created folder from the backend
const folder = await response.json(); const folder = await response.json();
console.log('Folder created successfully:', folder); 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); 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)); 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}); await window.loadFiles({forceRefresh: true});
window.ui.showNotification('Carpeta creada', `"${name}" creada correctamente`); window.ui.showNotification('Folder created', `"${name}" created successfully`);
} else { } else {
const errorData = await response.text(); const errorData = await response.text();
console.error('Create folder error:', errorData); 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) { } catch (error) {
console.error('Error creating folder:', 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) { if (response.ok) {
// Reload files after moving // Reload files after moving
await window.loadFiles(); await window.loadFiles();
window.ui.showNotification('Archivo movido', 'Archivo movido correctamente'); window.ui.showNotification('File moved', 'File moved successfully');
return true; return true;
} else { } else {
let errorMessage = 'Error desconocido'; let errorMessage = 'Unknown error';
try { try {
const errorData = await response.json(); const errorData = await response.json();
errorMessage = errorData.error || 'Error desconocido'; errorMessage = errorData.error || 'Unknown error';
} catch (e) { } 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; return false;
} }
} catch (error) { } catch (error) {
console.error('Error moving file:', 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; return false;
} }
}, },
@@ -357,22 +357,22 @@ const fileOps = {
if (response.ok) { if (response.ok) {
// Reload files after moving // Reload files after moving
await window.loadFiles(); await window.loadFiles();
window.ui.showNotification('Carpeta movida', 'Carpeta movida correctamente'); window.ui.showNotification('Folder moved', 'Folder moved successfully');
return true; return true;
} else { } else {
let errorMessage = 'Error desconocido'; let errorMessage = 'Unknown error';
try { try {
const errorData = await response.json(); const errorData = await response.json();
errorMessage = errorData.error || 'Error desconocido'; errorMessage = errorData.error || 'Unknown error';
} catch (e) { } 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; return false;
} }
} catch (error) { } catch (error) {
console.error('Error moving folder:', 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; return false;
} }
}, },
@@ -400,26 +400,26 @@ const fileOps = {
if (response.ok) { if (response.ok) {
window.ui.showNotification( window.ui.showNotification(
window.i18n ? window.i18n.t('notifications.file_renamed') : 'Archivo renombrado', window.i18n ? window.i18n.t('notifications.file_renamed') : 'File renamed',
window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `Archivo renombrado a "${newName}"` window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
); );
return true; return true;
} else { } else {
const errorText = await response.text(); const errorText = await response.text();
console.error('Error response:', errorText); console.error('Error response:', errorText);
let errorMessage = 'Error desconocido'; let errorMessage = 'Unknown error';
try { try {
const errorData = JSON.parse(errorText); const errorData = JSON.parse(errorText);
errorMessage = errorData.error || response.statusText; errorMessage = errorData.error || response.statusText;
} catch (e) { } catch (e) {
errorMessage = errorText || response.statusText; 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; return false;
} }
} catch (error) { } catch (error) {
console.error('Error renaming file:', 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; return false;
} }
}, },
@@ -446,13 +446,13 @@ const fileOps = {
console.log('Response status:', response.status); console.log('Response status:', response.status);
if (response.ok) { if (response.ok) {
window.ui.showNotification('Carpeta renombrada', `Carpeta renombrada a "${newName}"`); window.ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
return true; return true;
} else { } else {
const errorText = await response.text(); const errorText = await response.text();
console.error('Error response:', errorText); console.error('Error response:', errorText);
let errorMessage = 'Error desconocido'; let errorMessage = 'Unknown error';
try { try {
// Try to parse as JSON // Try to parse as JSON
const errorData = JSON.parse(errorText); const errorData = JSON.parse(errorText);
@@ -462,12 +462,12 @@ const fileOps = {
errorMessage = errorText || response.statusText; 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; return false;
} }
} catch (error) { } catch (error) {
console.error('Error renaming folder:', 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; return false;
} }
}, },
@@ -480,9 +480,9 @@ const fileOps = {
*/ */
async deleteFile(fileId, fileName) { async deleteFile(fileId, fileName) {
const confirmed = await showConfirmDialog({ const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera', title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
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}"?`, 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') : 'Eliminar', confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
}); });
if (!confirmed) return false; if (!confirmed) return false;
@@ -495,7 +495,7 @@ const fileOps = {
if (response.ok) { if (response.ok) {
window.loadFiles(); 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; return true;
} else { } else {
// Fallback to direct deletion if trash fails // Fallback to direct deletion if trash fails
@@ -506,16 +506,16 @@ const fileOps = {
if (fallbackResponse.ok) { if (fallbackResponse.ok) {
window.loadFiles(); window.loadFiles();
window.ui.showNotification('Archivo eliminado', `"${fileName}" eliminado correctamente`); window.ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
return true; return true;
} else { } else {
window.ui.showNotification('Error', 'Error al eliminar el archivo'); window.ui.showNotification('Error', 'Error deleting the file');
return false; return false;
} }
} }
} catch (error) { } catch (error) {
console.error('Error deleting file:', 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; return false;
} }
}, },
@@ -528,9 +528,9 @@ const fileOps = {
*/ */
async deleteFolder(folderId, folderName) { async deleteFolder(folderId, folderName) {
const confirmed = await showConfirmDialog({ const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera', title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
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?`, 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') : 'Eliminar', confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
}); });
if (!confirmed) return false; if (!confirmed) return false;
@@ -548,7 +548,7 @@ const fileOps = {
window.ui.updateBreadcrumb(''); window.ui.updateBreadcrumb('');
} }
window.loadFiles(); 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; return true;
} else { } else {
// Fallback to direct deletion if trash fails // Fallback to direct deletion if trash fails
@@ -564,23 +564,23 @@ const fileOps = {
window.ui.updateBreadcrumb(''); window.ui.updateBreadcrumb('');
} }
window.loadFiles(); window.loadFiles();
window.ui.showNotification('Carpeta eliminada', `"${folderName}" eliminada correctamente`); window.ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
return true; return true;
} else { } else {
window.ui.showNotification('Error', 'Error al eliminar la carpeta'); window.ui.showNotification('Error', 'Error deleting the folder');
return false; return false;
} }
} }
} catch (error) { } catch (error) {
console.error('Error deleting folder:', 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; return false;
} }
}, },
/** /**
* Obtener elementos de la papelera * Get trash items
* @returns {Promise<Array>} - Lista de elementos en la papelera * @returns {Promise<Array>} - List of trash items
*/ */
async getTrashItems() { async getTrashItems() {
try { try {
@@ -601,9 +601,9 @@ const fileOps = {
}, },
/** /**
* Restaurar un elemento desde la papelera * Restore an item from trash
* @param {string} trashId - ID del elemento en la papelera * @param {string} trashId - Trash item ID
* @returns {Promise<boolean>} - Éxito de la operación * @returns {Promise<boolean>} - Operation success
*/ */
async restoreFromTrash(trashId) { async restoreFromTrash(trashId) {
try { try {
@@ -617,29 +617,29 @@ const fileOps = {
}); });
if (response.ok) { if (response.ok) {
window.ui.showNotification('Elemento restaurado', 'Elemento restaurado correctamente'); window.ui.showNotification('Item restored', 'Item restored successfully');
return true; return true;
} else { } else {
window.ui.showNotification('Error', 'Error al restaurar el elemento'); window.ui.showNotification('Error', 'Error restoring the item');
return false; return false;
} }
} catch (error) { } catch (error) {
console.error('Error restoring item from trash:', 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; return false;
} }
}, },
/** /**
* Eliminar permanentemente un elemento de la papelera * Permanently delete a trash item
* @param {string} trashId - ID del elemento en la papelera * @param {string} trashId - Trash item ID
* @returns {Promise<boolean>} - Éxito de la operación * @returns {Promise<boolean>} - Operation success
*/ */
async deletePermanently(trashId) { async deletePermanently(trashId) {
const confirmed = await showConfirmDialog({ const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : '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') : '¿Estás seguro de que quieres eliminar permanentemente este elemento? Esta acción no se puede deshacer.', 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') : 'Eliminar permanentemente', confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently',
}); });
if (!confirmed) return false; if (!confirmed) return false;
@@ -650,28 +650,28 @@ const fileOps = {
}); });
if (response.ok) { if (response.ok) {
window.ui.showNotification('Elemento eliminado', 'Elemento eliminado permanentemente'); window.ui.showNotification('Item deleted', 'Item permanently deleted');
return true; return true;
} else { } else {
window.ui.showNotification('Error', 'Error al eliminar el elemento'); window.ui.showNotification('Error', 'Error deleting the item');
return false; return false;
} }
} catch (error) { } catch (error) {
console.error('Error deleting item permanently:', 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; return false;
} }
}, },
/** /**
* Vaciar la papelera * Empty the trash
* @returns {Promise<boolean>} - Éxito de la operación * @returns {Promise<boolean>} - Operation success
*/ */
async emptyTrash() { async emptyTrash() {
const confirmed = await showConfirmDialog({ const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_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') : '¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos.', 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') : 'Vaciar papelera', confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash',
}); });
if (!confirmed) return false; if (!confirmed) return false;
@@ -682,23 +682,23 @@ const fileOps = {
}); });
if (response.ok) { 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; return true;
} else { } else {
window.ui.showNotification('Error', 'Error al vaciar la papelera'); window.ui.showNotification('Error', 'Error emptying the trash');
return false; return false;
} }
} catch (error) { } catch (error) {
console.error('Error emptying trash:', 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; return false;
} }
}, },
/** /**
* Descargar un archivo * Download a file
* @param {string} fileId - ID del archivo * @param {string} fileId - File ID
* @param {string} fileName - Nombre del archivo * @param {string} fileName - File name
*/ */
async downloadFile(fileId, fileName) { async downloadFile(fileId, fileName) {
try { try {
@@ -716,23 +716,23 @@ const fileOps = {
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} else { } else {
window.ui.showNotification('Error', 'Error al descargar el archivo'); window.ui.showNotification('Error', 'Error downloading the file');
} }
} catch (error) { } catch (error) {
console.error('Error downloading file:', 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 * Download a folder as ZIP
* @param {string} folderId - ID de la carpeta * @param {string} folderId - Folder ID
* @param {string} folderName - Nombre de la carpeta * @param {string} folderName - Folder name
*/ */
async downloadFolder(folderId, folderName) { async downloadFolder(folderId, folderName) {
try { try {
// Show notification to user // 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`, { const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
headers: getAuthHeaders() headers: getAuthHeaders()
@@ -748,11 +748,11 @@ const fileOps = {
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} else { } else {
window.ui.showNotification('Error', 'Error al descargar la carpeta'); window.ui.showNotification('Error', 'Error downloading the folder');
} }
} catch (error) { } catch (error) {
console.error('Error downloading folder:', error); console.error('Error downloading folder:', error);
window.ui.showNotification('Error', 'Error al descargar la carpeta'); window.ui.showNotification('Error', 'Error downloading the folder');
} }
} }
}; };
+5 -5
View File
@@ -214,11 +214,11 @@ const fileSharing = {
copyLinkToClipboard(url) { copyLinkToClipboard(url) {
try { try {
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
window.ui.showNotification('Enlace copiado', 'Enlace copiado al portapapeles'); window.ui.showNotification('Link copied', 'Link copied to clipboard');
return true; return true;
} catch (error) { } catch (error) {
console.error('Error copying to clipboard:', 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; return false;
} }
}, },
@@ -229,7 +229,7 @@ const fileSharing = {
* @returns {string} - Formatted date string * @returns {string} - Formatted date string
*/ */
formatExpirationDate(dateString) { formatExpirationDate(dateString) {
if (!dateString) return 'Sin vencimiento'; if (!dateString) return 'No expiration';
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
@@ -252,11 +252,11 @@ const fileSharing = {
// Simulate network delay // Simulate network delay
//await new Promise(resolve => setTimeout(resolve, 800)); //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; return true;
} catch (error) { } catch (error) {
console.error('Error sending share notification:', 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; return false;
} }
}, },
+11 -11
View File
@@ -110,10 +110,10 @@ const recent = {
filesGrid.innerHTML = ''; filesGrid.innerHTML = '';
filesListView.innerHTML = ` filesListView.innerHTML = `
<div class="list-header"> <div class="list-header">
<div data-i18n="files.name">Nombre</div> <div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Tipo</div> <div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Tamaño</div> <div data-i18n="files.size">Size</div>
<div data-i18n="files.last_accessed">Último acceso</div> <div data-i18n="files.last_accessed">Last accessed</div>
</div> </div>
`; `;
@@ -126,8 +126,8 @@ const recent = {
emptyState.className = 'empty-state'; emptyState.className = 'empty-state';
emptyState.innerHTML = ` emptyState.innerHTML = `
<i class="fas fa-clock" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i> <i class="fas fa-clock" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No hay archivos recientes'}</p> <p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No recent files'}</p>
<p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Los archivos que abras aparecerán aquí'}</p> <p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
`; `;
filesGrid.appendChild(emptyState); filesGrid.appendChild(emptyState);
return; return;
@@ -143,7 +143,7 @@ const recent = {
} catch (error) { } catch (error) {
console.error('Error displaying recent files:', 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 // Determine icon and type
let iconClass = 'fas fa-file'; let iconClass = 'fas fa-file';
let iconSpecialClass = ''; let iconSpecialClass = '';
let typeLabel = 'Documento'; let typeLabel = 'Document';
if (file.mime_type) { if (file.mime_type) {
if (file.mime_type.startsWith('image/')) { if (file.mime_type.startsWith('image/')) {
iconClass = 'fas fa-file-image'; iconClass = 'fas fa-file-image';
iconSpecialClass = 'image-icon'; 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/')) { } else if (file.mime_type.startsWith('text/')) {
iconClass = 'fas fa-file-alt'; iconClass = 'fas fa-file-alt';
iconSpecialClass = 'text-icon'; 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/')) { } else if (file.mime_type.startsWith('video/')) {
iconClass = 'fas fa-file-video'; iconClass = 'fas fa-file-video';
iconSpecialClass = 'video-icon'; iconSpecialClass = 'video-icon';
@@ -204,7 +204,7 @@ const recent = {
<i class="${iconClass}"></i> <i class="${iconClass}"></i>
</div> </div>
<div class="file-name">${file.name}</div> <div class="file-name">${file.name}</div>
<div class="file-info">Accedido ${formattedDate.split(' ')[0]}</div> <div class="file-info">Accessed ${formattedDate.split(' ')[0]}</div>
`; `;
// Download on click // Download on click
+12 -12
View File
@@ -52,7 +52,7 @@ const search = {
} }
} catch (error) { } catch (error) {
console.error('Error performing search:', 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 }; return { files: [], folders: [], total_count: 0 };
} }
}, },
@@ -91,7 +91,7 @@ const search = {
} }
} catch (error) { } catch (error) {
console.error('Error performing advanced search:', 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 }; return { files: [], folders: [], total_count: 0 };
} }
}, },
@@ -109,10 +109,10 @@ const search = {
filesGrid.innerHTML = ''; filesGrid.innerHTML = '';
filesListView.innerHTML = ` filesListView.innerHTML = `
<div class="list-header"> <div class="list-header">
<div data-i18n="files.name">Nombre</div> <div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Tipo</div> <div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Tamaño</div> <div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modificado</div> <div data-i18n="files.modified">Modified</div>
</div> </div>
`; `;
@@ -120,9 +120,9 @@ const search = {
const searchHeader = document.createElement('div'); const searchHeader = document.createElement('div');
searchHeader.className = 'search-results-header'; searchHeader.className = 'search-results-header';
searchHeader.innerHTML = ` searchHeader.innerHTML = `
<h3>Resultados de búsqueda (${results.total_count || (results.files.length + results.folders.length)})</h3> <h3>Search results (${results.total_count || (results.files.length + results.folders.length)})</h3>
<button class="btn btn-secondary" id="clear-search-btn"> <button class="btn btn-secondary" id="clear-search-btn">
<i class="fas fa-times"></i> Limpiar búsqueda <i class="fas fa-times"></i> Clear search
</button> </button>
`; `;
filesGrid.appendChild(searchHeader); filesGrid.appendChild(searchHeader);
@@ -147,7 +147,7 @@ const search = {
emptyState.className = 'empty-state'; emptyState.className = 'empty-state';
emptyState.innerHTML = ` emptyState.innerHTML = `
<i class="fas fa-search" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i> <i class="fas fa-search" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<p>No se encontraron resultados para esta búsqueda</p> <p>No results found for this search</p>
`; `;
filesGrid.appendChild(emptyState); filesGrid.appendChild(emptyState);
return; return;
@@ -178,15 +178,15 @@ const search = {
}); });
if (response.ok) { 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; return true;
} else { } else {
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda'); window.ui.showNotification('Error', 'Error clearing search cache');
return false; return false;
} }
} catch (error) { } catch (error) {
console.error('Error clearing search cache:', 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; return false;
} }
} }
+66 -66
View File
@@ -16,24 +16,24 @@ const ui = {
folderMenu.id = 'folder-context-menu'; folderMenu.id = 'folder-context-menu';
folderMenu.innerHTML = ` folderMenu.innerHTML = `
<div class="context-menu-item" id="download-folder-option"> <div class="context-menu-item" id="download-folder-option">
<i class="fas fa-download"></i> <span data-i18n="actions.download">Descargar</span> <i class="fas fa-download"></i> <span data-i18n="actions.download">Download</span>
</div> </div>
<div class="context-menu-item" id="favorite-folder-option"> <div class="context-menu-item" id="favorite-folder-option">
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Añadir a favoritos</span> <i class="fas fa-star"></i> <span data-i18n="actions.favorite">Add to favorites</span>
</div> </div>
<div class="context-menu-item" id="share-folder-option"> <div class="context-menu-item" id="share-folder-option">
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Compartir</span> <i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Share</span>
</div> </div>
<div class="context-menu-separator"></div> <div class="context-menu-separator"></div>
<div class="context-menu-item" id="rename-folder-option"> <div class="context-menu-item" id="rename-folder-option">
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Renombrar</span> <i class="fas fa-pen"></i> <span data-i18n="actions.rename">Rename</span>
</div> </div>
<div class="context-menu-item" id="move-folder-option"> <div class="context-menu-item" id="move-folder-option">
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Mover a...</span> <i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Move to...</span>
</div> </div>
<div class="context-menu-separator"></div> <div class="context-menu-separator"></div>
<div class="context-menu-item context-menu-item-danger" id="delete-folder-option"> <div class="context-menu-item context-menu-item-danger" id="delete-folder-option">
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Eliminar</span> <i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Delete</span>
</div> </div>
`; `;
document.body.appendChild(folderMenu); document.body.appendChild(folderMenu);
@@ -46,28 +46,28 @@ const ui = {
fileMenu.id = 'file-context-menu'; fileMenu.id = 'file-context-menu';
fileMenu.innerHTML = ` fileMenu.innerHTML = `
<div class="context-menu-item" id="view-file-option"> <div class="context-menu-item" id="view-file-option">
<i class="fas fa-eye"></i> <span data-i18n="actions.view">Ver</span> <i class="fas fa-eye"></i> <span data-i18n="actions.view">View</span>
</div> </div>
<div class="context-menu-item" id="download-file-option"> <div class="context-menu-item" id="download-file-option">
<i class="fas fa-download"></i> <span data-i18n="actions.download">Descargar</span> <i class="fas fa-download"></i> <span data-i18n="actions.download">Download</span>
</div> </div>
<div class="context-menu-separator"></div> <div class="context-menu-separator"></div>
<div class="context-menu-item" id="favorite-file-option"> <div class="context-menu-item" id="favorite-file-option">
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Añadir a favoritos</span> <i class="fas fa-star"></i> <span data-i18n="actions.favorite">Add to favorites</span>
</div> </div>
<div class="context-menu-item" id="share-file-option"> <div class="context-menu-item" id="share-file-option">
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Compartir</span> <i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Share</span>
</div> </div>
<div class="context-menu-separator"></div> <div class="context-menu-separator"></div>
<div class="context-menu-item" id="rename-file-option"> <div class="context-menu-item" id="rename-file-option">
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Renombrar</span> <i class="fas fa-pen"></i> <span data-i18n="actions.rename">Rename</span>
</div> </div>
<div class="context-menu-item" id="move-file-option"> <div class="context-menu-item" id="move-file-option">
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Mover a...</span> <i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Move to...</span>
</div> </div>
<div class="context-menu-separator"></div> <div class="context-menu-separator"></div>
<div class="context-menu-item context-menu-item-danger" id="delete-file-option"> <div class="context-menu-item context-menu-item-danger" id="delete-file-option">
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Eliminar</span> <i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Delete</span>
</div> </div>
`; `;
document.body.appendChild(fileMenu); document.body.appendChild(fileMenu);
@@ -82,14 +82,14 @@ const ui = {
<div class="rename-dialog-content"> <div class="rename-dialog-content">
<div class="rename-dialog-header"> <div class="rename-dialog-header">
<i class="fas fa-pen" style="color:#ff5e3a"></i> <i class="fas fa-pen" style="color:#ff5e3a"></i>
<span data-i18n="dialogs.rename_folder">Renombrar</span> <span data-i18n="dialogs.rename_folder">Rename</span>
</div> </div>
<div class="rename-dialog-body"> <div class="rename-dialog-body">
<input type="text" id="rename-input" data-i18n-placeholder="dialogs.new_name" placeholder="Nuevo nombre"> <input type="text" id="rename-input" data-i18n-placeholder="dialogs.new_name" placeholder="New name">
</div> </div>
<div class="rename-dialog-buttons"> <div class="rename-dialog-buttons">
<button class="btn btn-secondary" id="rename-cancel-btn" data-i18n="actions.cancel">Cancelar</button> <button class="btn btn-secondary" id="rename-cancel-btn" data-i18n="actions.cancel">Cancel</button>
<button class="btn btn-primary" id="rename-confirm-btn" data-i18n="actions.rename">Renombrar</button> <button class="btn btn-primary" id="rename-confirm-btn" data-i18n="actions.rename">Rename</button>
</div> </div>
</div> </div>
`; `;
@@ -105,19 +105,19 @@ const ui = {
<div class="rename-dialog-content"> <div class="rename-dialog-content">
<div class="rename-dialog-header"> <div class="rename-dialog-header">
<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i> <i class="fas fa-arrows-alt" style="color:#ff5e3a"></i>
<span data-i18n="dialogs.move_file">Mover</span> <span data-i18n="dialogs.move_file">Move</span>
</div> </div>
<div class="rename-dialog-body"> <div class="rename-dialog-body">
<p style="margin:0 0 12px;color:#718096;font-size:14px" data-i18n="dialogs.select_destination">Selecciona la carpeta destino:</p> <p style="margin:0 0 12px;color:#718096;font-size:14px" data-i18n="dialogs.select_destination">Select destination folder:</p>
<div id="folder-select-container" style="max-height:220px;overflow-y:auto;"> <div id="folder-select-container" style="max-height:220px;overflow-y:auto;">
<div class="folder-select-item selected" data-folder-id=""> <div class="folder-select-item selected" data-folder-id="">
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Raíz</span> <i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Root</span>
</div> </div>
</div> </div>
</div> </div>
<div class="rename-dialog-buttons"> <div class="rename-dialog-buttons">
<button class="btn btn-secondary" id="move-cancel-btn" data-i18n="actions.cancel">Cancelar</button> <button class="btn btn-secondary" id="move-cancel-btn" data-i18n="actions.cancel">Cancel</button>
<button class="btn btn-primary" id="move-confirm-btn" data-i18n="actions.move_to">Mover</button> <button class="btn btn-primary" id="move-confirm-btn" data-i18n="actions.move_to">Move</button>
</div> </div>
</div> </div>
`; `;
@@ -133,67 +133,67 @@ const ui = {
<div class="share-dialog-content"> <div class="share-dialog-content">
<div class="share-dialog-header"> <div class="share-dialog-header">
<i class="fas fa-share-alt" style="color:#ff5e3a"></i> <i class="fas fa-share-alt" style="color:#ff5e3a"></i>
<span data-i18n="dialogs.share_file">Compartir archivo</span> <span data-i18n="dialogs.share_file">Share file</span>
</div> </div>
<div class="shared-item-info"> <div class="shared-item-info">
<strong>Elemento:</strong> <span id="shared-item-name"></span> <strong>Item:</strong> <span id="shared-item-name"></span>
</div> </div>
<div id="existing-shares-section" style="display:none; margin: 15px 0;"> <div id="existing-shares-section" style="display:none; margin: 15px 0;">
<h3 data-i18n="dialogs.existing_shares">Enlaces compartidos existentes</h3> <h3 data-i18n="dialogs.existing_shares">Existing shared links</h3>
<div id="existing-shares-container"></div> <div id="existing-shares-container"></div>
</div> </div>
<div class="share-options"> <div class="share-options">
<h3 data-i18n="dialogs.share_options">Opciones de compartición</h3> <h3 data-i18n="dialogs.share_options">Share options</h3>
<div class="form-group"> <div class="form-group">
<label for="share-password" data-i18n="dialogs.password">Contraseña (opcional):</label> <label for="share-password" data-i18n="dialogs.password">Password (optional):</label>
<input type="password" id="share-password" placeholder="Proteger con contraseña"> <input type="password" id="share-password" placeholder="Protect with password">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="share-expiration" data-i18n="dialogs.expiration">Fecha de vencimiento (opcional):</label> <label for="share-expiration" data-i18n="dialogs.expiration">Expiration date (optional):</label>
<input type="date" id="share-expiration"> <input type="date" id="share-expiration">
</div> </div>
<div class="form-group"> <div class="form-group">
<label data-i18n="dialogs.permissions">Permisos:</label> <label data-i18n="dialogs.permissions">Permissions:</label>
<div class="permission-options"> <div class="permission-options">
<div class="permission-option"> <div class="permission-option">
<input type="checkbox" id="share-permission-read" checked> <input type="checkbox" id="share-permission-read" checked>
<label for="share-permission-read" data-i18n="permissions.read">Lectura</label> <label for="share-permission-read" data-i18n="permissions.read">Read</label>
</div> </div>
<div class="permission-option"> <div class="permission-option">
<input type="checkbox" id="share-permission-write"> <input type="checkbox" id="share-permission-write">
<label for="share-permission-write" data-i18n="permissions.write">Escritura</label> <label for="share-permission-write" data-i18n="permissions.write">Write</label>
</div> </div>
<div class="permission-option"> <div class="permission-option">
<input type="checkbox" id="share-permission-reshare"> <input type="checkbox" id="share-permission-reshare">
<label for="share-permission-reshare" data-i18n="permissions.reshare">Permitir compartir</label> <label for="share-permission-reshare" data-i18n="permissions.reshare">Allow sharing</label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="new-share-section" style="display:none; margin: 15px 0;"> <div id="new-share-section" style="display:none; margin: 15px 0;">
<h3 data-i18n="dialogs.generated_link">Enlace generado</h3> <h3 data-i18n="dialogs.generated_link">Generated link</h3>
<div class="form-group"> <div class="form-group">
<input type="text" id="generated-share-url" readonly> <input type="text" id="generated-share-url" readonly>
<div class="share-link-actions"> <div class="share-link-actions">
<button class="btn btn-small" id="copy-share-btn"> <button class="btn btn-small" id="copy-share-btn">
<i class="fas fa-copy"></i> <span data-i18n="actions.copy">Copiar</span> <i class="fas fa-copy"></i> <span data-i18n="actions.copy">Copy</span>
</button> </button>
<button class="btn btn-small" id="notify-share-btn"> <button class="btn btn-small" id="notify-share-btn">
<i class="fas fa-envelope"></i> <span data-i18n="actions.notify">Notificar</span> <i class="fas fa-envelope"></i> <span data-i18n="actions.notify">Notify</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="share-dialog-buttons"> <div class="share-dialog-buttons">
<button class="btn btn-secondary" id="share-cancel-btn" data-i18n="actions.cancel">Cancelar</button> <button class="btn btn-secondary" id="share-cancel-btn" data-i18n="actions.cancel">Cancel</button>
<button class="btn btn-primary" id="share-confirm-btn" data-i18n="actions.share">Compartir</button> <button class="btn btn-primary" id="share-confirm-btn" data-i18n="actions.share">Share</button>
</div> </div>
</div> </div>
`; `;
@@ -228,24 +228,24 @@ const ui = {
<div class="share-dialog-content"> <div class="share-dialog-content">
<div class="share-dialog-header"> <div class="share-dialog-header">
<i class="fas fa-envelope" style="color:#ff5e3a"></i> <i class="fas fa-envelope" style="color:#ff5e3a"></i>
<span data-i18n="dialogs.notify">Notificar enlace compartido</span> <span data-i18n="dialogs.notify">Notify shared link</span>
</div> </div>
<p><strong>URL:</strong> <span id="notification-share-url"></span></p> <p><strong>URL:</strong> <span id="notification-share-url"></span></p>
<div class="form-group"> <div class="form-group">
<label for="notification-email" data-i18n="dialogs.recipient">Destinatario:</label> <label for="notification-email" data-i18n="dialogs.recipient">Recipient:</label>
<input type="email" id="notification-email" placeholder="Correo electrónico"> <input type="email" id="notification-email" placeholder="Email address">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notification-message" data-i18n="dialogs.message">Mensaje (opcional):</label> <label for="notification-message" data-i18n="dialogs.message">Message (optional):</label>
<textarea id="notification-message" rows="3"></textarea> <textarea id="notification-message" rows="3"></textarea>
</div> </div>
<div class="share-dialog-buttons"> <div class="share-dialog-buttons">
<button class="btn btn-secondary" id="notification-cancel-btn" data-i18n="actions.cancel">Cancelar</button> <button class="btn btn-secondary" id="notification-cancel-btn" data-i18n="actions.cancel">Cancel</button>
<button class="btn btn-primary" id="notification-send-btn" data-i18n="actions.send">Enviar</button> <button class="btn btn-primary" id="notification-send-btn" data-i18n="actions.send">Send</button>
</div> </div>
</div> </div>
`; `;
@@ -402,7 +402,7 @@ const ui = {
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home'); homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
// For searching, we might have a custom breadcrumb text // 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 // We're in search mode - don't add click handler
breadcrumb.appendChild(homeItem); breadcrumb.appendChild(homeItem);
return; return;
@@ -421,7 +421,7 @@ const ui = {
breadcrumb.appendChild(homeItem); breadcrumb.appendChild(homeItem);
// If we have a subfolder, add it to the breadcrumb // 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'); const separator = document.createElement('span');
separator.className = 'breadcrumb-separator'; separator.className = 'breadcrumb-separator';
separator.textContent = '>'; separator.textContent = '>';
@@ -633,14 +633,14 @@ const ui = {
* @param {Object} folder - Folder object * @param {Object} folder - Folder object
*/ */
addFolderToView(folder) { 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}"]`) || if (document.querySelector(`.file-card[data-folder-id="${folder.id}"]`) ||
document.querySelector(`.file-item[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; 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 // Grid view element
const folderGridElement = document.createElement('div'); const folderGridElement = document.createElement('div');
@@ -655,7 +655,7 @@ const ui = {
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
</div> </div>
<div class="file-name">${folder.name}</div> <div class="file-name">${folder.name}</div>
<div class="file-info">Carpeta</div> <div class="file-info">Folder</div>
`; `;
// Drag and drop setup for folders // Drag and drop setup for folders
@@ -746,7 +746,7 @@ const ui = {
if (id) { if (id) {
if (isFolder) { if (isFolder) {
if (id === folder.id) { if (id === folder.id) {
alert("No puedes mover una carpeta a sí misma"); alert("You cannot move a folder to itself");
return; return;
} }
await fileOps.moveFolder(id, folder.id); await fileOps.moveFolder(id, folder.id);
@@ -758,7 +758,7 @@ const ui = {
document.getElementById('files-grid').appendChild(folderGridElement); document.getElementById('files-grid').appendChild(folderGridElement);
// List view element - Mejorado // List view element - Improved
const folderListElement = document.createElement('div'); const folderListElement = document.createElement('div');
folderListElement.className = 'file-item'; folderListElement.className = 'file-item';
folderListElement.dataset.folderId = folder.id; 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 = ` folderListElement.innerHTML = `
<div class="name-cell"> <div class="name-cell">
<div class="file-icon folder-icon"> <div class="file-icon folder-icon">
@@ -796,7 +796,7 @@ const ui = {
</div> </div>
<span>${folder.name}</span> <span>${folder.name}</span>
</div> </div>
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'}</div> <div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
<div class="size-cell">--</div> <div class="size-cell">--</div>
<div class="date-cell">${formattedDate}</div> <div class="date-cell">${formattedDate}</div>
`; `;
@@ -844,7 +844,7 @@ const ui = {
if (id) { if (id) {
if (isFolder) { if (isFolder) {
if (id === folder.id) { if (id === folder.id) {
alert("No puedes mover una carpeta a sí misma"); alert("You cannot move a folder to itself");
return; return;
} }
await fileOps.moveFolder(id, folder.id); await fileOps.moveFolder(id, folder.id);
@@ -862,29 +862,29 @@ const ui = {
* @param {Object} file - File object * @param {Object} file - File object
*/ */
addFileToView(file) { 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}"]`) || if (document.querySelector(`.file-card[data-file-id="${file.id}"]`) ||
document.querySelector(`.file-item[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; 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 // Determine icon and type
let iconClass = 'fas fa-file'; let iconClass = 'fas fa-file';
let iconSpecialClass = ''; let iconSpecialClass = '';
let typeLabel = 'Documento'; let typeLabel = 'Document';
if (file.mime_type) { if (file.mime_type) {
if (file.mime_type.startsWith('image/')) { if (file.mime_type.startsWith('image/')) {
iconClass = 'fas fa-file-image'; iconClass = 'fas fa-file-image';
iconSpecialClass = 'image-icon'; 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/')) { } else if (file.mime_type.startsWith('text/')) {
iconClass = 'fas fa-file-alt'; iconClass = 'fas fa-file-alt';
iconSpecialClass = 'text-icon'; 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/')) { } else if (file.mime_type.startsWith('video/')) {
iconClass = 'fas fa-file-video'; iconClass = 'fas fa-file-video';
iconSpecialClass = 'video-icon'; iconSpecialClass = 'video-icon';
@@ -916,7 +916,7 @@ const ui = {
<i class="${iconClass}"></i> <i class="${iconClass}"></i>
</div> </div>
<div class="file-name">${file.name}</div> <div class="file-name">${file.name}</div>
<div class="file-info">Modificado ${formattedDate.split(' ')[0]}</div> <div class="file-info">Modified ${formattedDate.split(' ')[0]}</div>
`; `;
fileGridElement.dataset.fileId = file.id; fileGridElement.dataset.fileId = file.id;
@@ -1007,7 +1007,7 @@ const ui = {
document.getElementById('files-grid').appendChild(fileGridElement); 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'); const fileListElement = document.createElement('div');
fileListElement.className = 'file-item'; fileListElement.className = 'file-item';
fileListElement.dataset.fileId = file.id; fileListElement.dataset.fileId = file.id;
@@ -1238,9 +1238,9 @@ window.initRubberBandSelection = initRubberBandSelection;
* @returns {Promise<boolean>} true if confirmed, false if cancelled * @returns {Promise<boolean>} true if confirmed, false if cancelled
*/ */
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) { function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Eliminar'); const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Delete');
const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancelar'); const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancel');
const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirmar acción'); const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirm action');
return new Promise((resolve) => { return new Promise((resolve) => {
// Remove any previous confirm dialog // Remove any previous confirm dialog
+34 -34
View File
@@ -62,46 +62,46 @@
<div class="auth-logo-text">OxiCloud</div> <div class="auth-logo-text">OxiCloud</div>
</div> </div>
<h2 class="auth-title" data-i18n="auth.login_title">Iniciar sesión</h2> <h2 class="auth-title" data-i18n="auth.login_title">Log in</h2>
<div class="auth-error" id="login-error"></div> <div class="auth-error" id="login-error"></div>
<form class="auth-form" id="login-form"> <form class="auth-form" id="login-form">
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="login-username" data-i18n="auth.username">Usuario</label> <label class="auth-label" for="login-username" data-i18n="auth.username">Username</label>
<input <input
type="text" type="text"
id="login-username" id="login-username"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.username_placeholder" data-i18n-placeholder="auth.username_placeholder"
placeholder="Ingresa tu nombre de usuario" placeholder="Enter your username"
required required
> >
</div> </div>
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="login-password" data-i18n="auth.password">Contraseña</label> <label class="auth-label" for="login-password" data-i18n="auth.password">Password</label>
<input <input
type="password" type="password"
id="login-password" id="login-password"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.password_placeholder" data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa tu contraseña" placeholder="Enter your password"
required required
> >
</div> </div>
<button type="submit" class="auth-button" data-i18n="auth.login_button">Iniciar sesión</button> <button type="submit" class="auth-button" data-i18n="auth.login_button">Log in</button>
</form> </form>
<div class="auth-toggle"> <div class="auth-toggle">
<span data-i18n="auth.no_account">¿No tienes cuenta?</span> <span data-i18n="auth.no_account">Don't have an account?</span>
<span class="auth-toggle-link" id="show-register" data-i18n="auth.register">Regístrate</span> <span class="auth-toggle-link" id="show-register" data-i18n="auth.register">Sign up</span>
</div> </div>
<div class="auth-toggle"> <div class="auth-toggle">
<span data-i18n="auth.admin_setup">¿Primera vez?</span> <span data-i18n="auth.admin_setup">First time?</span>
<span class="auth-toggle-link" id="show-admin-setup" data-i18n="auth.setup">Configurar administrador</span> <span class="auth-toggle-link" id="show-admin-setup" data-i18n="auth.setup">Set up administrator</span>
</div> </div>
</div> </div>
@@ -115,20 +115,20 @@
<div class="auth-logo-text">OxiCloud</div> <div class="auth-logo-text">OxiCloud</div>
</div> </div>
<h2 class="auth-title" data-i18n="auth.register_title">Crear cuenta</h2> <h2 class="auth-title" data-i18n="auth.register_title">Create account</h2>
<div class="auth-error" id="register-error"></div> <div class="auth-error" id="register-error"></div>
<div class="auth-success" id="register-success"></div> <div class="auth-success" id="register-success"></div>
<form class="auth-form" id="register-form"> <form class="auth-form" id="register-form">
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="register-username" data-i18n="auth.username">Usuario</label> <label class="auth-label" for="register-username" data-i18n="auth.username">Username</label>
<input <input
type="text" type="text"
id="register-username" id="register-username"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.username_placeholder" data-i18n-placeholder="auth.username_placeholder"
placeholder="Ingresa un nombre de usuario" placeholder="Enter a username"
required required
minlength="3" minlength="3"
maxlength="32" maxlength="32"
@@ -142,42 +142,42 @@
id="register-email" id="register-email"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.email_placeholder" data-i18n-placeholder="auth.email_placeholder"
placeholder="Ingresa tu email" placeholder="Enter your email"
required required
> >
</div> </div>
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="register-password" data-i18n="auth.password">Contraseña</label> <label class="auth-label" for="register-password" data-i18n="auth.password">Password</label>
<input <input
type="password" type="password"
id="register-password" id="register-password"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.password_placeholder" data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa una contraseña segura" placeholder="Enter a secure password"
required required
minlength="8" minlength="8"
> >
</div> </div>
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="register-password-confirm" data-i18n="auth.confirm_password">Confirmar contraseña</label> <label class="auth-label" for="register-password-confirm" data-i18n="auth.confirm_password">Confirm password</label>
<input <input
type="password" type="password"
id="register-password-confirm" id="register-password-confirm"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.confirm_password_placeholder" data-i18n-placeholder="auth.confirm_password_placeholder"
placeholder="Confirma tu contraseña" placeholder="Confirm your password"
required required
> >
</div> </div>
<button type="submit" class="auth-button" data-i18n="auth.register_button">Crear cuenta</button> <button type="submit" class="auth-button" data-i18n="auth.register_button">Create account</button>
</form> </form>
<div class="auth-toggle"> <div class="auth-toggle">
<span data-i18n="auth.have_account">¿Ya tienes cuenta?</span> <span data-i18n="auth.have_account">Already have an account?</span>
<span class="auth-toggle-link" id="show-login" data-i18n="auth.login">Iniciar sesión</span> <span class="auth-toggle-link" id="show-login" data-i18n="auth.login">Log in</span>
</div> </div>
</div> </div>
@@ -191,7 +191,7 @@
<div class="auth-logo-text">OxiCloud</div> <div class="auth-logo-text">OxiCloud</div>
</div> </div>
<h2 class="auth-title" data-i18n="auth.setup_title">Configuración inicial</h2> <h2 class="auth-title" data-i18n="auth.setup_title">Initial setup</h2>
<div class="setup-steps"> <div class="setup-steps">
<div class="setup-step"> <div class="setup-step">
@@ -200,11 +200,11 @@
</div> </div>
<div class="setup-step"> <div class="setup-step">
<div class="step-number">2</div> <div class="step-number">2</div>
<div class="step-title" data-i18n="auth.setup_step2">Sistema</div> <div class="step-title" data-i18n="auth.setup_step2">System</div>
</div> </div>
<div class="setup-step"> <div class="setup-step">
<div class="step-number">3</div> <div class="step-number">3</div>
<div class="step-title" data-i18n="auth.setup_step3">Completado</div> <div class="step-title" data-i18n="auth.setup_step3">Completed</div>
</div> </div>
</div> </div>
@@ -213,7 +213,7 @@
<form class="auth-form" id="admin-setup-form"> <form class="auth-form" id="admin-setup-form">
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="admin-username" data-i18n="auth.admin_username">Usuario administrador</label> <label class="auth-label" for="admin-username" data-i18n="auth.admin_username">Administrator username</label>
<input <input
type="text" type="text"
id="admin-username" id="admin-username"
@@ -224,48 +224,48 @@
</div> </div>
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="admin-email" data-i18n="auth.admin_email">Email administrador</label> <label class="auth-label" for="admin-email" data-i18n="auth.admin_email">Administrator email</label>
<input <input
type="email" type="email"
id="admin-email" id="admin-email"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.email_placeholder" data-i18n-placeholder="auth.email_placeholder"
placeholder="Ingresa el email del administrador" placeholder="Enter the administrator email"
required required
> >
</div> </div>
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="admin-password" data-i18n="auth.admin_password">Contraseña administrador</label> <label class="auth-label" for="admin-password" data-i18n="auth.admin_password">Administrator password</label>
<input <input
type="password" type="password"
id="admin-password" id="admin-password"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.password_placeholder" data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa una contraseña segura" placeholder="Enter a secure password"
required required
minlength="8" minlength="8"
> >
</div> </div>
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="admin-password-confirm" data-i18n="auth.confirm_password">Confirmar contraseña</label> <label class="auth-label" for="admin-password-confirm" data-i18n="auth.confirm_password">Confirm password</label>
<input <input
type="password" type="password"
id="admin-password-confirm" id="admin-password-confirm"
class="auth-input" class="auth-input"
data-i18n-placeholder="auth.confirm_password_placeholder" data-i18n-placeholder="auth.confirm_password_placeholder"
placeholder="Confirma la contraseña" placeholder="Confirm the password"
required required
> >
</div> </div>
<button type="submit" class="auth-button" data-i18n="auth.create_admin">Crear administrador</button> <button type="submit" class="auth-button" data-i18n="auth.create_admin">Create administrator</button>
</form> </form>
<div class="auth-toggle"> <div class="auth-toggle">
<span data-i18n="auth.back_to_login">¿Ya está configurado?</span> <span data-i18n="auth.back_to_login">Already configured?</span>
<span class="auth-toggle-link" id="back-to-login" data-i18n="auth.login">Iniciar sesión</span> <span class="auth-toggle-link" id="back-to-login" data-i18n="auth.login">Log in</span>
</div> </div>
</div> </div>
</div> </div>
+10 -10
View File
@@ -8,17 +8,17 @@
<body> <body>
<h1>OxiCloud Test Page</h1> <h1>OxiCloud Test Page</h1>
<div id="result"></div> <div id="result"></div>
<button id="createBtn">Crear carpeta nueva</button> <button id="createBtn">Create new folder</button>
<button id="listBtn">Listar carpetas</button> <button id="listBtn">List folders</button>
<script> <script>
// Función para mostrar resultados // Function to show results
function showResult(data) { function showResult(data) {
document.getElementById('result').innerHTML = document.getElementById('result').innerHTML =
'<pre>' + JSON.stringify(data, null, 2) + '</pre>'; '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
} }
// Función para crear carpeta // Function to create folder
async function createFolder() { async function createFolder() {
try { try {
const response = await fetch('/api/folders', { const response = await fetch('/api/folders', {
@@ -33,34 +33,34 @@
}); });
const data = await response.json(); const data = await response.json();
console.log('Respuesta de creación de carpeta:', data); console.log('Folder creation response:', data);
showResult(data); showResult(data);
return data; return data;
} catch (error) { } catch (error) {
console.error('Error al crear carpeta:', error); console.error('Error creating folder:', error);
showResult({error: error.message}); showResult({error: error.message});
return null; return null;
} }
} }
// Función para listar carpetas // Function to list folders
async function listFolders() { async function listFolders() {
try { try {
const response = await fetch('/api/folders'); const response = await fetch('/api/folders');
const data = await response.json(); const data = await response.json();
console.log('Listado de carpetas:', data); console.log('Folder listing:', data);
showResult(data); showResult(data);
return data; return data;
} catch (error) { } catch (error) {
console.error('Error al listar carpetas:', error); console.error('Error listing folders:', error);
showResult({error: error.message}); showResult({error: error.message});
return []; return [];
} }
} }
// Asignar eventos a botones // Assign events to buttons
document.getElementById('createBtn').addEventListener('click', createFolder); document.getElementById('createBtn').addEventListener('click', createFolder);
document.getElementById('listBtn').addEventListener('click', listFolders); document.getElementById('listBtn').addEventListener('click', listFolders);
</script> </script>
+10 -2
View File
@@ -1,11 +1,19 @@
{ {
"path_to_id": { "path_to_id": {
"/Mi Carpeta - testuser999": "7c1afffd-867f-4649-a0f6-4cc8a9979af4",
"/Mi Carpeta - test": "edfc23e0-d9f9-4cc7-b1dc-6369568b19e6", "/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": { "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", "304384d6-9a7e-4b22-a659-287957e8b667": "/Mi Carpeta - testuser",
"b775c641-a698-458b-a561-42ae93073c74": "/Mi Carpeta - user1",
"edfc23e0-d9f9-4cc7-b1dc-6369568b19e6": "/Mi Carpeta - test" "edfc23e0-d9f9-4cc7-b1dc-6369568b19e6": "/Mi Carpeta - test"
}, },
"version": 2 "version": 6
} }