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 {
fn from(dto: FileDto) -> Self {
// Usar constructor para crear una entidad desde DTO
// Nota: esto debe simplificarse si File tiene un constructor adecuado
// Si no, deberías hacer la conversión de la mejor manera posible
// Use constructor to create an entity from DTO
// Note: this should be simplified if File has a proper constructor
// If not, make the conversion as best as possible
File::from_dto(
dto.id,
dto.name,
+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 {
fn from(dto: FolderDto) -> Self {
// Usar constructor para crear una entidad desde DTO
// Nota: esto debe simplificarse si Folder tiene un constructor adecuado
// Use constructor to create an entity from DTO
// Note: this should be simplified if Folder has a proper constructor
Folder::from_dto(
dto.id,
dto.name,
+21 -21
View File
@@ -1,39 +1,39 @@
use serde::{Serialize, Deserialize};
/// Un DTO para representar información de paginación
/// A DTO to represent pagination information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationDto {
/// Página actual (comienza en 0)
/// Current page (starts at 0)
pub page: usize,
/// Tamaño de página
/// Page size
pub page_size: usize,
/// Número total de elementos
/// Total number of items
pub total_items: usize,
/// Número total de páginas
/// Total number of pages
pub total_pages: usize,
/// Indica si hay una página siguiente
/// Indicates if there is a next page
pub has_next: bool,
/// Indica si hay una página anterior
/// Indicates if there is a previous page
pub has_prev: bool,
}
/// Un DTO para representar una solicitud de paginación
/// A DTO to represent a pagination request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationRequestDto {
/// Página solicitada (comienza en 0)
/// Requested page (starts at 0)
#[serde(default)]
pub page: usize,
/// Tamaño de página solicitado
/// Requested page size
#[serde(default = "default_page_size")]
pub page_size: usize,
}
/// Un DTO para representar una respuesta paginada
/// A DTO to represent a paginated response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResponseDto<T> {
/// Datos en la página actual
/// Data on the current page
pub items: Vec<T>,
/// Información de paginación
/// Pagination information
pub pagination: PaginationDto,
}
@@ -46,33 +46,33 @@ impl Default for PaginationRequestDto {
}
}
/// Función para establecer el tamaño de página por defecto
/// Function to set the default page size
fn default_page_size() -> usize {
100 // Por defecto, 100 items por página
100 // By default, 100 items per page
}
impl PaginationRequestDto {
/// Calcula el offset para consultas paginadas
/// Calculates the offset for paginated queries
pub fn offset(&self) -> usize {
self.page * self.page_size
}
/// Calcula el límite para consultas paginadas
/// Calculates the limit for paginated queries
pub fn limit(&self) -> usize {
self.page_size
}
/// Valida y ajusta los parámetros de paginación
/// Validates and adjusts the pagination parameters
pub fn validate_and_adjust(&self) -> Self {
let mut page = self.page;
let mut page_size = self.page_size;
// Asegurar que la página sea al menos 0
// Ensure the page is at least 0
if page < 1 {
page = 0;
}
// Asegurar que el tamaño de página esté entre 10 y 500
// Ensure the page size is between 10 and 500
if page_size < 10 {
page_size = 10;
} else if page_size > 500 {
@@ -87,7 +87,7 @@ impl PaginationRequestDto {
}
impl<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(
items: Vec<T>,
page: usize,
+6 -6
View File
@@ -1,21 +1,21 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// DTO para elementos recientes
/// DTO for recent items
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentItemDto {
/// Identificador único para el elemento reciente
/// Unique identifier for the recent item
pub id: String,
/// ID del usuario propietario
/// Owner user ID
pub user_id: String,
/// ID del elemento (archivo o carpeta)
/// Item ID (file or folder)
pub item_id: String,
/// Tipo del elemento ('file' o 'folder')
/// Item type ('file' or 'folder')
pub item_type: String,
/// Cuándo se accedió al elemento
/// When the item was accessed
pub accessed_at: DateTime<Utc>,
}
+1 -1
View File
@@ -67,7 +67,7 @@ pub struct RefreshTokenDto {
pub refresh_token: String,
}
/// Datos del usuario autenticado actual (para uso en servicios de application)
/// Authenticated current user data (for use in application services)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentUser {
pub id: String,
+18 -18
View File
@@ -65,49 +65,49 @@ pub trait TokenServicePort: Send + Sync + 'static {
#[async_trait]
pub trait UserStoragePort: Send + Sync + 'static {
/// Crea un nuevo usuario
/// Creates a new user
async fn create_user(&self, user: User) -> Result<User, DomainError>;
/// Obtiene un usuario por ID
/// Gets a user by ID
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>;
/// Obtiene un usuario por correo electrónico
/// Gets a user by email
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>;
/// Actualiza solo el uso de almacenamiento de un usuario
/// Updates only the storage usage of a user
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError>;
/// Lista usuarios con paginación
/// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> Result<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>;
/// Elimina un usuario por su ID
/// Deletes a user by their ID
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>;
/// Cambia la contraseña de un usuario
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<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>;
/// Cambia el rol de un usuario
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>;
/// Actualiza la cuota de almacenamiento de un usuario
/// Updates a user's storage quota
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError>;
/// Cuenta el número total de usuarios
/// Counts the total number of users
async fn count_users(&self) -> Result<i64, DomainError>;
}
@@ -156,15 +156,15 @@ pub trait OidcServicePort: Send + Sync + 'static {
#[async_trait]
pub trait SessionStoragePort: Send + Sync + 'static {
/// Crea una nueva sesión
/// Creates a new session
async fn create_session(&self, session: Session) -> Result<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>;
/// Revoca una sesión específica
/// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>;
/// Revoca todas las sesiones de un usuario
/// Revokes all sessions of a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
}
+8 -8
View File
@@ -22,22 +22,22 @@ pub trait FavoritesUseCase: Send + Sync {
// Outbound port — persistence abstraction
// ─────────────────────────────────────────────────────
/// Puerto secundario (outbound) para persistencia de favoritos.
/// Secondary (outbound) port for favorites persistence.
///
/// Los servicios de aplicación dependen de este trait en lugar de
/// acceder directamente a `PgPool`. La implementación concreta
/// vive en `infrastructure::repositories::pg`.
/// Application services depend on this trait instead of
/// accessing `PgPool` directly. The concrete implementation
/// lives in `infrastructure::repositories::pg`.
#[async_trait]
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
/// Obtiene todos los favoritos de un usuario.
/// Gets all favorites for a user.
async fn get_favorites(&self, user_id: &str) -> Result<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<()>;
/// 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>;
/// 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>;
}
+15 -15
View File
@@ -22,10 +22,10 @@ pub enum UploadStrategy {
Streaming,
}
/// Puerto primario para operaciones de subida de archivos
/// Primary port for file upload operations
#[async_trait]
pub trait FileUploadUseCase: Send + Sync + 'static {
/// Sube un nuevo archivo desde bytes
/// Uploads a new file from bytes
async fn upload_file(
&self,
name: String,
@@ -47,10 +47,10 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
total_size: usize,
) -> Result<(FileDto, UploadStrategy), DomainError>;
/// Crea un nuevo archivo en la ruta especificada (para WebDAV)
/// Creates a new file at the specified path (for WebDAV)
async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<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>;
}
@@ -76,22 +76,22 @@ pub enum OptimizedFileContent {
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]
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>;
/// 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>;
/// Lista archivos en una carpeta
/// Lists files in a folder
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>;
/// 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>;
/// Optimized multi-tier download.
@@ -119,16 +119,16 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
// Management port (delete, move)
// ─────────────────────────────────────────────────────
/// Puerto primario para operaciones de gestión de archivos
/// Primary port for file management operations
#[async_trait]
pub trait FileManagementUseCase: Send + Sync + 'static {
/// Mueve un archivo a otra carpeta
/// Moves a file to another folder
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
/// Renombra un archivo
/// Renames a file
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>;
/// Smart delete: trash-first with dedup reference cleanup.
@@ -145,7 +145,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
) -> 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 {
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>;
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::common::errors::DomainError;
/// Puerto primario para operaciones de carpetas
/// Primary port for folder operations
#[async_trait]
pub trait FolderUseCase: Send + Sync + 'static {
/// Crea una nueva carpeta
/// Creates a new folder
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>;
/// Obtiene una carpeta por su ID
/// Gets a folder by its ID
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>;
/// 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>;
/// Lista carpetas con paginación
/// Lists folders with pagination
async fn list_folders_paginated(
&self,
parent_id: Option<&str>,
pagination: &crate::application::dtos::pagination::PaginationRequestDto
) -> Result<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>;
/// Mueve una carpeta a otro padre
/// Moves a folder to another parent
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>;
}
/**
* Puerto primario para búsqueda de archivos y carpetas
* Primary port for file and folder search
*
* Define las operaciones relacionadas con la búsqueda avanzada de
* archivos y carpetas basándose en diversos criterios.
* Defines the operations related to advanced search of
* files and folders based on various criteria.
*/
#[async_trait]
pub trait SearchUseCase: Send + Sync + 'static {
/**
* Realiza una búsqueda basada en los criterios especificados
* Performs a search based on the specified criteria
*
* @param criteria Criterios de búsqueda que incluyen texto, fechas, tamaños, etc.
* @return Resultados de la búsqueda que contienen archivos y carpetas coincidentes
* @param criteria Search criteria including text, dates, sizes, etc.
* @return Search results containing matching files and folders
*/
async fn search(&self, criteria: SearchCriteriaDto) -> Result<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>;
}
+27 -27
View File
@@ -9,71 +9,71 @@ pub use crate::domain::repositories::folder_repository::FolderRepository;
use super::storage_ports::{FileReadPort, FileWritePort};
/// Puerto secundario para operaciones de almacenamiento
/// Secondary port for storage operations
#[async_trait]
pub trait StoragePort: Send + Sync + 'static {
/// Resuelve una ruta de dominio a una ruta física
/// Resolves a domain path to a physical path
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
/// Crea directorios si no existen
/// Creates directories if they don't exist
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
/// Verifica si existe un archivo en la ruta dada
/// Checks if a file exists at the given path
async fn file_exists(&self, storage_path: &StoragePath) -> Result<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>;
}
/// Puerto unificado para persistencia de archivos (backward-compatible).
/// Unified port for file persistence (backward-compatible).
///
/// Ahora es un **supertrait** de `FileReadPort + FileWritePort`.
/// Cualquier tipo que implemente ambos ports obtiene `FileStoragePort`
/// automáticamente via blanket impl. Esto permite migrar consumidores
/// gradualmente a los ports granulares mientras los existentes siguen
/// funcionando sin cambios.
/// Now it is a **supertrait** of `FileReadPort + FileWritePort`.
/// Any type that implements both ports gets `FileStoragePort`
/// automatically via blanket impl. This allows consumers to be migrated
/// gradually to granular ports while existing ones continue
/// working without changes.
pub trait FileStoragePort: FileReadPort + FileWritePort {}
/// Blanket implementation: cualquier tipo que implemente ambos ports
/// es automáticamente un FileStoragePort.
/// Blanket implementation: any type that implements both ports
/// is automatically a FileStoragePort.
impl<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.
/// Las implementaciones concretas deben implementar `FolderRepository`,
/// obteniendo `FolderStoragePort` automáticamente vía blanket impl.
/// Has the same signature as the domain's `FolderRepository`.
/// Concrete implementations must implement `FolderRepository`,
/// getting `FolderStoragePort` automatically via blanket impl.
pub trait FolderStoragePort: FolderRepository {}
/// Blanket implementation: cualquier tipo que implemente FolderRepository
/// es automáticamente un FolderStoragePort.
/// Blanket implementation: any type that implements FolderRepository
/// is automatically a FolderStoragePort.
impl<T: FolderRepository> FolderStoragePort for T {}
/// Puerto secundario para mapeo de IDs
/// Secondary port for ID mapping
#[async_trait]
pub trait IdMappingPort: Send + Sync + 'static {
/// Obtiene o crea un ID para una ruta
/// Gets or creates an ID for a path
async fn get_or_create_id(&self, path: &StoragePath) -> Result<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>;
/// Actualiza la ruta para un ID existente
/// Updates the path for an existing ID
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError>;
/// Elimina un ID del mapeo
/// Removes an ID from the mapping
async fn remove_id(&self, id: &str) -> Result<(), DomainError>;
/// Guarda cambios pendientes
/// Saves pending changes
async fn save_changes(&self) -> Result<(), DomainError>;
/// Obtiene la ruta de archivo como PathBuf
/// Gets the file path as a PathBuf
async fn get_file_path(&self, file_id: &str) -> Result<PathBuf, DomainError> {
let storage_path = self.get_path_by_id(file_id).await?;
Ok(PathBuf::from(storage_path.to_string()))
}
/// Actualiza la ruta de un archivo
/// Updates a file's path
async fn update_file_path(&self, file_id: &str, new_path: &PathBuf) -> Result<(), DomainError> {
let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string());
self.update_path(file_id, &storage_path).await
+13 -13
View File
@@ -2,19 +2,19 @@ use async_trait::async_trait;
use crate::common::errors::Result;
use crate::application::dtos::recent_dto::RecentItemDto;
/// Define operaciones para gestionar elementos recientes del usuario
/// Defines operations for managing user recent items
#[async_trait]
pub trait RecentItemsUseCase: Send + Sync {
/// Obtener todos los elementos recientes de un usuario
/// Get all recent items for a user
async fn get_recent_items(&self, user_id: &str, limit: Option<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<()>;
/// 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>;
/// Limpiar toda la lista de elementos recientes
/// Clear the entire recent items list
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
}
@@ -22,24 +22,24 @@ pub trait RecentItemsUseCase: Send + Sync {
// Outbound port — persistence abstraction
// ─────────────────────────────────────────────────────
/// Puerto secundario (outbound) para persistencia de elementos recientes.
/// Secondary (outbound) port for recent items persistence.
///
/// Abstrae el acceso a la tabla `auth.user_recent_files` para que
/// `RecentService` no dependa directamente de `PgPool`.
/// Abstracts access to the `auth.user_recent_files` table so that
/// `RecentService` does not depend directly on `PgPool`.
#[async_trait]
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
/// Obtiene los últimos elementos recientes de un usuario (ordenados por fecha desc).
/// Gets the latest recent items for a user (ordered by date desc).
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<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<()>;
/// 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>;
/// Elimina todos los elementos recientes de un usuario.
/// Removes all recent items for a user.
async fn clear_all(&self, user_id: &str) -> Result<()>;
/// Elimina elementos que excedan `max_items` (los más antiguos).
/// Removes items exceeding `max_items` (the oldest ones).
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
}
+37 -37
View File
@@ -17,28 +17,28 @@ pub use crate::domain::repositories::folder_repository::FolderRepository;
// FileReadPort — application-layer alias for FileReadRepository
// ─────────────────────────────────────────────────────
/// Puerto secundario para **lectura** de archivos.
/// Secondary port for file **reading**.
///
/// Encapsula toda operación que consulta estado sin modificarlo:
/// get, list, content, stream, mmap, range, resolución de rutas.
/// Encapsulates every operation that queries state without modifying it:
/// get, list, content, stream, mmap, range, path resolution.
#[async_trait]
pub trait FileReadPort: Send + Sync + 'static {
/// Obtiene un archivo por su ID.
/// Gets a file by its ID.
async fn get_file(&self, id: &str) -> Result<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>;
/// 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>;
/// Obtiene contenido como stream (ideal para archivos grandes).
/// Gets content as a stream (ideal for large files).
async fn get_file_stream(
&self,
id: &str,
) -> Result<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(
&self,
id: &str,
@@ -46,13 +46,13 @@ pub trait FileReadPort: Send + Sync + 'static {
end: Option<u64>,
) -> 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>;
/// 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>;
/// 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>;
}
@@ -60,13 +60,13 @@ pub trait FileReadPort: Send + Sync + 'static {
// FileWritePort — all write / mutate operations
// ─────────────────────────────────────────────────────
/// Puerto secundario para **escritura** de archivos.
/// Secondary port for file **writing**.
///
/// Cubre: upload (buffered + streaming), move, delete, update,
/// y el registro diferido para write-behind cache.
/// Covers: upload (buffered + streaming), move, delete, update,
/// and deferred registration for the write-behind cache.
#[async_trait]
pub trait FileWritePort: Send + Sync + 'static {
/// Guarda un nuevo archivo desde bytes.
/// Saves a new file from bytes.
async fn save_file(
&self,
name: String,
@@ -75,7 +75,7 @@ pub trait FileWritePort: Send + Sync + 'static {
content: Vec<u8>,
) -> 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(
&self,
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>>,
) -> Result<File, DomainError>;
/// Mueve un archivo a otra carpeta.
/// Moves a file to another folder.
async fn move_file(
&self,
file_id: &str,
target_folder_id: Option<String>,
) -> Result<File, DomainError>;
/// Renombra un archivo (same folder, different name).
/// Renames a file (same folder, different name).
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError>;
/// Elimina un archivo.
/// Deletes a file.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Actualiza el contenido de un archivo existente.
/// Updates the content of an existing file.
async fn update_file_content(&self, file_id: &str, content: Vec<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
/// escritura diferida que realizará el `WriteBehindCache`.
/// Returns `(File, PathBuf)` where `PathBuf` is the destination path for the
/// deferred write that the `WriteBehindCache` will perform.
async fn register_file_deferred(
&self,
name: String,
@@ -118,13 +118,13 @@ pub trait FileWritePort: Send + Sync + 'static {
// ── Trash operations ──
/// Mueve un archivo a la papelera
/// Moves a file to the trash
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
/// Restaura un archivo desde la papelera a su ubicación original
/// Restores a file from the trash to its original location
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
/// Elimina un archivo permanentemente (usado por la papelera)
/// Permanently deletes a file (used by the trash)
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
}
@@ -132,40 +132,40 @@ pub trait FileWritePort: Send + Sync + 'static {
// Auxiliary ports (unchanged)
// ─────────────────────────────────────────────────────
/// Puerto secundario para resolución de rutas de archivos
/// Secondary port for file path resolution
#[async_trait]
pub trait FilePathResolutionPort: Send + Sync + 'static {
/// Obtiene la ruta de almacenamiento de un archivo
/// Gets the storage path of a file
async fn get_file_path(&self, id: &str) -> Result<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;
}
/// Puerto secundario para verificación de existencia de archivos/directorios
/// Secondary port for file/directory existence verification
#[async_trait]
pub trait StorageVerificationPort: Send + Sync + 'static {
/// Verifica si existe un archivo en la ruta dada
/// Checks whether a file exists at the given path
async fn file_exists(&self, storage_path: &StoragePath) -> Result<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>;
}
/// Puerto secundario para gestión de directorios
/// Secondary port for directory management
#[async_trait]
pub trait DirectoryManagementPort: Send + Sync + 'static {
/// Crea directorios si no existen
/// Creates directories if they do not exist
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
}
/// Puerto secundario para gestión de uso de almacenamiento
/// Secondary port for storage usage management
#[async_trait]
pub trait StorageUsagePort: Send + Sync + 'static {
/// Actualiza estadísticas de uso de almacenamiento para un usuario
/// Updates storage usage statistics for a user
async fn update_user_storage_usage(&self, user_id: &str) -> Result<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>;
}
@@ -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 {
self.folder_service = Some(folder_service);
self
}
/// Configura el servicio OIDC
/// Configures the OIDC service
pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self {
{
let mut state = self.oidc.write().unwrap();
@@ -123,12 +123,12 @@ impl AuthApplicationService {
}
pub async fn register(&self, dto: RegisterDto) -> Result<UserDto, DomainError> {
// Verificar usuario duplicado
// Check for duplicate user
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
return Err(DomainError::new(
ErrorKind::AlreadyExists,
"User",
format!("El usuario '{}' ya existe", dto.username)
format!("User '{}' already exists", dto.username)
));
}
@@ -136,62 +136,62 @@ impl AuthApplicationService {
return Err(DomainError::new(
ErrorKind::AlreadyExists,
"User",
format!("El email '{}' ya está registrado", dto.email)
format!("Email '{}' is already registered", dto.email)
));
}
// Verificar si el usuario quiere crear un admin
// Check if the user wants to create an admin
let is_admin_request = dto.username.to_lowercase() == "admin" ||
(dto.role.is_some() && dto.role.as_ref().unwrap().to_lowercase() == "admin");
// Si está intentando crear un admin, verificar si ya existen admins en el sistema
// If trying to create an admin, check if admins already exist in the system
if is_admin_request {
match self.count_admin_users().await {
Ok(admin_count) => {
// Si ya hay admins en el sistema y no estamos en instalación limpia,
// no permitimos crear otro admin desde el registro
// If there are already admins in the system and this is not a clean install,
// we do not allow creating another admin from registration
if admin_count > 0 {
// Verificar si es una instalación limpia (solo el admin predeterminado)
// Check if this is a clean install (only the default admin)
match self.count_all_users().await {
Ok(user_count) => {
// Si hay más de 2 usuarios (admin + test), no es instalación limpia
// If there are more than 2 users (admin + test), it is not a clean install
if user_count > 2 {
tracing::warn!("Intento de crear admin adicional rechazado: ya existe al menos un admin");
tracing::warn!("Attempt to create additional admin rejected: at least one admin already exists");
return Err(DomainError::new(
ErrorKind::AccessDenied,
"User",
"No se permite crear usuarios admin adicionales desde la página de registro"
"Creating additional admin users from the registration page is not allowed"
));
}
// En caso contrario, es instalación limpia y se permite el primer admin
tracing::info!("Permitiendo creación de admin en instalación limpia");
// Otherwise, it is a clean install and the first admin is allowed
tracing::info!("Allowing admin creation on clean install");
},
Err(e) => {
tracing::error!("Error al contar usuarios: {}", e);
// Por seguridad, si no podemos verificar, rechazamos la creación de admin
tracing::error!("Error counting users: {}", e);
// For security, if we cannot verify, we reject admin creation
return Err(DomainError::new(
ErrorKind::AccessDenied,
"User",
"No se permite crear usuarios admin adicionales"
"Creating additional admin users is not allowed"
));
}
}
}
},
Err(e) => {
tracing::error!("Error al contar usuarios admin: {}", e);
// Por seguridad, si no podemos verificar, rechazamos la creación de admin
tracing::error!("Error counting admin users: {}", e);
// For security, if we cannot verify, we reject admin creation
return Err(DomainError::new(
ErrorKind::AccessDenied,
"User",
"No se permite crear usuarios admin adicionales"
"Creating additional admin users is not allowed"
));
}
}
}
// Determinar rol y cuota según el tipo de usuario
// Si se proporciona un rol explícito de "admin", usar rol de administrador
// Determine role and quota based on user type
// If an explicit "admin" role is provided, use the administrator role
let role = if let Some(role_str) = &dto.role {
if role_str.to_lowercase() == "admin" {
UserRole::Admin
@@ -199,7 +199,7 @@ impl AuthApplicationService {
UserRole::User
}
} else {
// Caso especial: si el nombre es "admin", asignar rol de admin aunque no se especifique
// Special case: if the username is "admin", assign admin role even if not specified
if dto.username.to_lowercase() == "admin" {
UserRole::Admin
} else {
@@ -207,26 +207,26 @@ impl AuthApplicationService {
}
};
// Cuota según el rol: 100GB para admin, 1GB para usuarios normales
// Quota based on role: 100GB for admin, 1GB for regular users
let quota = if role == UserRole::Admin {
107374182400 // 100GB para admin
107374182400 // 100GB for admin
} else {
1024 * 1024 * 1024 // 1GB para usuarios normales
1024 * 1024 * 1024 // 1GB for regular users
};
// Validar longitud de password antes de hashear
// Validate password length before hashing
if dto.password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Password debe tener al menos 8 caracteres"
"Password must be at least 8 characters long"
));
}
// Hashear el password usando el servicio de infraestructura
// Hash the password using the infrastructure service
let password_hash = self.password_hasher.hash_password(&dto.password)?;
// Crear usuario con el hash pre-generado
// Create user with the pre-generated hash
let user = User::new(
dto.username.clone(),
dto.email,
@@ -236,13 +236,13 @@ impl AuthApplicationService {
).map_err(|e| DomainError::new(
ErrorKind::InvalidInput,
"User",
format!("Error al crear usuario: {}", e)
format!("Error creating user: {}", e)
))?;
// Guardar usuario
// Save user
let created_user = self.user_storage.create_user(user).await?;
// Crear carpeta personal para el usuario
// Create personal folder for the user
if let Some(folder_service) = &self.folder_service {
let folder_name = format!("Mi Carpeta - {}", dto.username);
@@ -252,20 +252,20 @@ impl AuthApplicationService {
}).await {
Ok(folder) => {
tracing::info!(
"Carpeta personal creada para el usuario {}: {} (ID: {})",
"Personal folder created for user {}: {} (ID: {})",
created_user.id(),
folder.name,
folder.id
);
// Aquí se podría guardar la asociación de la carpeta al usuario
// por ejemplo, en una tabla de relación carpeta-usuario
// Here we could save the folder-to-user association,
// for example, in a folder-user relationship table
},
Err(e) => {
// No fallamos el registro por un error en la creación de la carpeta
// pero lo registramos para investigación
// We don't fail registration due to a folder creation error,
// but we log it for investigation
tracing::error!(
"No se pudo crear la carpeta personal para el usuario {}: {}",
"Could not create personal folder for user {}: {}",
created_user.id(),
e
);
@@ -273,67 +273,67 @@ impl AuthApplicationService {
}
} else {
tracing::warn!(
"No se configuró el servicio de carpetas, no se puede crear carpeta personal para el usuario: {}",
"Folder service not configured, cannot create personal folder for user: {}",
created_user.id()
);
}
tracing::info!("Usuario registrado: {}", created_user.id());
tracing::info!("User registered: {}", created_user.id());
Ok(UserDto::from(created_user))
}
pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> {
// Buscar usuario
// Find user
let mut user = self.user_storage
.get_user_by_username(&dto.username)
.await
.map_err(|_| DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Credenciales inválidas"
"Invalid credentials"
))?;
// Verificar si usuario está activo
// Check if user is active
if !user.is_active() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Cuenta desactivada"
"Account deactivated"
));
}
// Verificar contraseña usando el hasher inyectado
// Verify password using the injected hasher
let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Credenciales inválidas"
"Invalid credentials"
));
}
// Actualizar último login
// Update last login
user.register_login();
self.user_storage.update_user(user.clone()).await?;
// Generar tokens usando el servicio de tokens inyectado
// Generate tokens using the injected token service
let access_token = self.token_service.generate_access_token(&user)?;
let refresh_token = self.token_service.generate_refresh_token();
// Guardar sesión
// Save session
let session = Session::new(
user.id().to_string(),
refresh_token.clone(),
None, // IP (se puede añadir desde la capa HTTP)
None, // User-Agent (se puede añadir desde la capa HTTP)
None, // IP (can be added from the HTTP layer)
None, // User-Agent (can be added from the HTTP layer)
self.token_service.refresh_token_expiry_days(),
);
self.session_storage.create_session(session).await?;
// Respuesta de autenticación
// Authentication response
Ok(AuthResponseDto {
user: UserDto::from(user),
access_token,
@@ -344,43 +344,43 @@ impl AuthApplicationService {
}
pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result<AuthResponseDto, DomainError> {
// Obtener sesión válida
// Get valid session
let session = self.session_storage
.get_session_by_refresh_token(&dto.refresh_token)
.await?;
// Verificar si la sesión está expirada o revocada
// Check if the session is expired or revoked
if session.is_expired() || session.is_revoked() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Sesión expirada o inválida"
"Session expired or invalid"
));
}
// Obtener usuario
// Get user
let user = self.user_storage
.get_user_by_id(session.user_id())
.await?;
// Verificar si usuario está activo
// Check if user is active
if !user.is_active() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Cuenta desactivada"
"Account deactivated"
));
}
// Revocar sesión actual
// Revoke current session
self.session_storage.revoke_session(session.id()).await?;
// Generar nuevos tokens
// Generate new tokens
let access_token = self.token_service.generate_access_token(&user)?;
let new_refresh_token = self.token_service.generate_refresh_token();
// Crear nueva sesión
// Create new session
let new_session = Session::new(
user.id().to_string(),
new_refresh_token.clone(),
@@ -401,67 +401,67 @@ impl AuthApplicationService {
}
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> {
// Obtener sesión
// Get session
let session = match self.session_storage.get_session_by_refresh_token(refresh_token).await {
Ok(s) => s,
// Si la sesión no existe, consideramos el logout como exitoso
// If the session doesn't exist, we consider the logout successful
Err(_) => return Ok(()),
};
// Verificar que la sesión pertenece al usuario
// Verify that the session belongs to the user
if session.user_id() != user_id {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"La sesión no pertenece al usuario"
"The session does not belong to the user"
));
}
// Revocar sesión
// Revoke session
self.session_storage.revoke_session(session.id()).await?;
Ok(())
}
pub async fn logout_all(&self, user_id: &str) -> Result<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?;
Ok(revoked_count)
}
pub async fn change_password(&self, user_id: &str, dto: ChangePasswordDto) -> Result<(), DomainError> {
// Obtener usuario
// Get user
let mut user = self.user_storage.get_user_by_id(user_id).await?;
// Verificar contraseña actual usando el hasher inyectado
// Verify current password using the injected hasher
let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Contraseña actual incorrecta"
"Current password is incorrect"
));
}
// Validar nueva contraseña
// Validate new password
if dto.new_password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Password debe tener al menos 8 caracteres"
"Password must be at least 8 characters long"
));
}
// Hashear nueva contraseña y actualizar usuario
// Hash new password and update user
let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
user.update_password_hash(new_hash);
// Guardar usuario actualizado
// Save updated user
self.user_storage.update_user(user).await?;
// Opcional: revocar todas las sesiones para forzar re-login con nueva contraseña
// Optional: revoke all sessions to force re-login with new password
self.session_storage.revoke_all_user_sessions(user_id).await?;
Ok(())
@@ -492,7 +492,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al contar usuarios administradores: {}", e)
format!("Error counting admin users: {}", e)
))?;
Ok(admin_users.len() as i64)
@@ -506,7 +506,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al contar usuarios: {}", e)
format!("Error counting users: {}", e)
))?;
Ok(all_users.len() as i64)
@@ -523,7 +523,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al eliminar usuario admin predeterminado: {}", e)
format!("Error deleting default admin user: {}", e)
))
},
Err(_) => {
@@ -545,7 +545,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al eliminar usuario admin predeterminado: {}", e)
format!("Error deleting default admin user: {}", e)
))?;
// 3. Create new admin user with the provided credentials but admin role
@@ -564,7 +564,7 @@ impl AuthApplicationService {
).map_err(|e| DomainError::new(
ErrorKind::InvalidInput,
"User",
format!("Error al crear usuario admin: {}", e)
format!("Error creating admin user: {}", e)
))?;
// 4. Save the new admin user
@@ -580,7 +580,7 @@ impl AuthApplicationService {
}).await {
Ok(folder) => {
tracing::info!(
"Carpeta personal creada para el admin {}: {} (ID: {})",
"Personal folder created for admin {}: {} (ID: {})",
created_user.id(),
folder.name,
folder.id
@@ -588,7 +588,7 @@ impl AuthApplicationService {
},
Err(e) => {
tracing::error!(
"No se pudo crear la carpeta personal para el admin {}: {}",
"Could not create personal folder for admin {}: {}",
created_user.id(),
e
);
@@ -596,7 +596,7 @@ impl AuthApplicationService {
}
}
tracing::info!("Admin personalizado creado: {}", created_user.id());
tracing::info!("Custom admin created: {}", created_user.id());
Ok(UserDto::from(created_user))
}
+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::folder_dto::FolderDto;
/// Errores específicos para operaciones por lotes
/// Specific errors for batch operations
#[derive(Debug, Error)]
pub enum BatchOperationError {
#[error("Error de dominio: {0}")]
#[error("Domain error: {0}")]
Domain(#[from] DomainError),
#[error("Operación cancelada: {0}")]
#[error("Operation cancelled: {0}")]
Cancelled(String),
#[error("Límite de concurrencia excedido: {0}")]
#[error("Concurrency limit exceeded: {0}")]
ConcurrencyLimit(String),
#[error("Error en operación del lote: {0} ({1} de {2} completadas)")]
#[error("Batch operation error: {0} ({1} of {2} completed)")]
PartialFailure(String, usize, usize),
#[error("Error interno: {0}")]
#[error("Internal error: {0}")]
Internal(String),
}
/// Resultado de una operación por lotes con estadísticas
/// Result of a batch operation with statistics
#[derive(Debug, Clone)]
pub struct BatchResult<T> {
/// Resultados exitosos
/// Successful results
pub successful: Vec<T>,
/// Operaciones fallidas con sus errores
/// Failed operations with their errors
pub failed: Vec<(String, String)>,
/// Estadísticas de la operación
/// Operation statistics
pub stats: BatchStats,
}
/// Estadísticas de una operación por lotes
/// Statistics of a batch operation
#[derive(Debug, Clone, Default)]
pub struct BatchStats {
/// Número total de operaciones
/// Total number of operations
pub total: usize,
/// Número de operaciones exitosas
/// Number of successful operations
pub successful: usize,
/// Número de operaciones fallidas
/// Number of failed operations
pub failed: usize,
/// Tiempo total de ejecución en milisegundos
/// Total execution time in milliseconds
pub execution_time_ms: u128,
/// Concurrencia máxima alcanzada
/// Maximum concurrency reached
pub max_concurrency: usize,
}
/// Servicio de operaciones por lotes
/// Batch operations service
pub struct BatchOperationService {
file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>,
@@ -67,14 +67,14 @@ pub struct BatchOperationService {
}
impl BatchOperationService {
/// Crea una nueva instancia del servicio de operaciones por lotes
/// Creates a new instance of the batch operations service
pub fn new(
file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>,
folder_service: Arc<FolderService>,
config: AppConfig
) -> Self {
// Limitar la concurrencia basada en la configuración
// Limit concurrency based on configuration
let max_concurrency = config.concurrency.max_concurrent_files;
Self {
@@ -86,7 +86,7 @@ impl BatchOperationService {
}
}
/// Crea una nueva instancia con la configuración por defecto
/// Creates a new instance with default configuration
pub fn default(
file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>,
@@ -95,16 +95,16 @@ impl BatchOperationService {
Self::new(file_retrieval, file_management, folder_service, AppConfig::default())
}
/// Copia múltiples archivos en paralelo
/// Copies multiple files in parallel
pub async fn copy_files(
&self,
file_ids: Vec<String>,
target_folder_id: Option<String>,
) -> 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();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -114,30 +114,30 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await;
// Liberar el permiso explícitamente (también se libera al hacer drop)
// Release the permit explicitly (also released on drop)
drop(permit);
// Devolver el resultado junto con el ID para identificar éxitos/fallos
// Return the result along with the ID to identify successes/failures
(file_id, copy_result)
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(file) => {
@@ -151,13 +151,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Copia en lote completada: {}/{} exitosas en {}ms",
"Batch copy completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -166,16 +166,16 @@ impl BatchOperationService {
Ok(result)
}
/// Mueve múltiples archivos en paralelo
/// Moves multiple files in parallel
pub async fn move_files(
&self,
file_ids: Vec<String>,
target_folder_id: Option<String>,
) -> 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();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -185,30 +185,30 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let move_result = mgmt.move_file(&file_id, target_folder.clone()).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID para identificar éxitos/fallos
// Return the result along with the ID to identify successes/failures
(file_id, move_result)
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(file) => {
@@ -222,13 +222,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Movimiento en lote completado: {}/{} exitosas en {}ms",
"Batch move completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -237,15 +237,15 @@ impl BatchOperationService {
Ok(result)
}
/// Elimina múltiples archivos en paralelo
/// Deletes multiple files in parallel
pub async fn delete_files(
&self,
file_ids: Vec<String>,
) -> 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();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -255,30 +255,30 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let semaphore = self.semaphore.clone();
let id_clone = file_id.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let delete_result = mgmt.delete_file(&file_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID
// Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone))
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(id) => {
@@ -292,13 +292,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Eliminación en lote completada: {}/{} exitosas en {}ms",
"Batch deletion completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -307,15 +307,15 @@ impl BatchOperationService {
Ok(result)
}
/// Carga múltiples archivos en paralelo (datos en memoria)
/// Loads multiple files in parallel (data in memory)
pub async fn get_multiple_files(
&self,
file_ids: Vec<String>,
) -> 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();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -325,29 +325,29 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let retrieval = self.file_retrieval.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let get_result = retrieval.get_file(&file_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID
// Return the result along with the ID
(file_id, get_result)
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(file) => {
@@ -361,13 +361,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Carga en lote completada: {}/{} exitosas en {}ms",
"Batch load completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -376,16 +376,16 @@ impl BatchOperationService {
Ok(result)
}
/// Elimina múltiples carpetas en paralelo
/// Deletes multiple folders in parallel
pub async fn delete_folders(
&self,
folder_ids: Vec<String>,
_recursive: bool,
) -> 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();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -395,32 +395,32 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada carpeta
// Define the operation to perform for each folder
let operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone();
let id_clone = folder_id.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
// For both recursive and non-recursive, use the standard delete_folder method
// since FolderUseCase only has a single delete_folder method
let delete_result = folder_service.delete_folder(&folder_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID
// Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone))
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (folder_id, operation_result) in operation_results {
match operation_result {
Ok(id) => {
@@ -434,13 +434,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Eliminación en lote de carpetas completada: {}/{} exitosas en {}ms",
"Batch folder deletion completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -449,7 +449,7 @@ impl BatchOperationService {
Ok(result)
}
/// Operación genérica de lote para cualquier tipo de función asíncrona
/// Generic batch operation for any type of async function
pub async fn generic_batch_operation<T, F, Fut>(
&self,
items: Vec<T>,
@@ -460,10 +460,10 @@ impl BatchOperationService {
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<T, DomainError>> + Send + 'static,
{
info!("Iniciando operación genérica en lote con {} items", items.len());
info!("Starting generic batch operation with {} items", items.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -473,25 +473,25 @@ impl BatchOperationService {
},
};
// Convertir cada item a una tarea
// Convert each item to a task
let tasks = items.iter().map(|item| {
let item_clone = item.clone();
let op = operation.clone();
let semaphore = self.semaphore.clone();
async move {
// La función proporcionada debe manejar la adquisición del semáforo
// The provided function must handle semaphore acquisition
let op_result = op(item_clone.clone(), semaphore).await;
// Devolver el resultado junto con el item original para identificación
// Return the result along with the original item for identification
(item_clone, op_result)
}
});
// Ejecutar todas las tareas en paralelo
// Execute all tasks in parallel
let operation_results = join_all(tasks).await;
// Procesar resultados
// Process results
for (item, operation_result) in operation_results {
match operation_result {
Ok(result_item) => {
@@ -499,20 +499,20 @@ impl BatchOperationService {
result.stats.successful += 1;
}
Err(e) => {
// Convertir el item a string para el reporte de error
// Convert item to string for error reporting
result.failed.push((format!("{:?}", item), e.to_string()));
result.stats.failed += 1;
}
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Operación genérica en lote completada: {}/{} exitosas en {}ms",
"Generic batch operation completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -521,15 +521,15 @@ impl BatchOperationService {
Ok(result)
}
/// Crear múltiples carpetas en paralelo
/// Create multiple folders in parallel
pub async fn create_folders(
&self,
folders: Vec<(String, Option<String>)>, // (nombre, padre_id)
folders: Vec<(String, Option<String>)>, // (name, parent_id)
) -> 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();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -539,13 +539,13 @@ impl BatchOperationService {
},
};
// Definir la operación para cada carpeta
// Define the operation for each folder
let operations = folders.into_iter().map(|(name, parent_id)| {
let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let dto = crate::application::dtos::folder_dto::CreateFolderDto {
@@ -554,19 +554,19 @@ impl BatchOperationService {
};
let create_result = folder_service.create_folder(dto).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado con un identificador para los errores
// Return the result with an identifier for errors
let id = format!("{}:{}", name, parent_id.unwrap_or_default());
(id, create_result)
}
});
// Ejecutar todas las operaciones en paralelo
// Execute all operations in parallel
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (id, operation_result) in operation_results {
match operation_result {
Ok(folder) => {
@@ -580,13 +580,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Creación en lote de carpetas completada: {}/{} exitosas en {}ms",
"Batch folder creation completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -595,15 +595,15 @@ impl BatchOperationService {
Ok(result)
}
/// Obtener metadatos de múltiples carpetas en paralelo
/// Get metadata of multiple folders in parallel
pub async fn get_multiple_folders(
&self,
folder_ids: Vec<String>,
) -> 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();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -613,29 +613,29 @@ impl BatchOperationService {
},
};
// Definir la operación para cada carpeta
// Define the operation for each folder
let operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let get_result = folder_service.get_folder(&folder_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado con su ID
// Return the result with its ID
(folder_id, get_result)
}
});
// Ejecutar todas las operaciones en paralelo
// Execute all operations in parallel
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (folder_id, operation_result) in operation_results {
match operation_result {
Ok(folder) => {
@@ -649,13 +649,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Carga en lote de carpetas completada: {}/{} exitosas en {}ms",
"Batch folder load completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -673,7 +673,7 @@ mod tests {
#[tokio::test]
async fn test_generic_batch_operation() {
// Crear el servicio de batch with stubs
// Create the batch service with stubs
let batch_service = BatchOperationService::new(
Arc::new(StubFileRetrievalUseCase),
Arc::new(StubFileManagementUseCase),
@@ -683,35 +683,35 @@ mod tests {
AppConfig::default()
);
// Definir una operación genérica de prueba
// Define a generic test operation
let operation = |item: i32, semaphore: Arc<Semaphore>| async move {
// Adquirir y liberar el semáforo
// Acquire and release the semaphore
let _permit = semaphore.acquire().await.unwrap();
if item % 2 == 0 {
// Simular éxito para números pares
// Simulate success for even numbers
Ok(item * 2)
} else {
// Simular error para números impares
// Simulate error for odd numbers
Err(DomainError::validation_error("Odd number not allowed"))
}
};
// Ejecutar la operación de batch
// Execute the batch operation
let items = vec![1, 2, 3, 4, 5];
let result = batch_service.generic_batch_operation(items, operation).await.unwrap();
// Verificar los resultados
// Verify the results
assert_eq!(result.stats.total, 5);
assert_eq!(result.stats.successful, 2);
assert_eq!(result.stats.failed, 3);
// Los números pares deberían estar en los éxitos, duplicados
// Even numbers should be in successes, doubled
assert!(result.successful.contains(&4)); // 2*2
assert!(result.successful.contains(&8)); // 4*2
// Los impares deberían estar en los fallos
// Odd numbers should be in failures
assert_eq!(result.failed.len(), 3);
}
}
@@ -16,7 +16,7 @@ const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
/// Threshold above which mmap is used instead of streaming (100 MB).
const MMAP_THRESHOLD: u64 = 100 * 1024 * 1024;
/// Servicio para operaciones de recuperación de archivos
/// Service for file retrieval operations
///
/// Implements a multi-tier download strategy:
/// - Tier 0: Write-behind cache (just-uploaded files still in RAM)
@@ -29,7 +29,7 @@ fn extract_username_from_path(path: &str) -> Option<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:
/// 1. **Write-Behind** (<256 KB): store in RAM, respond instantly, flush async.
@@ -78,7 +78,7 @@ impl FileUploadService {
}
}
/// Configura el servicio de uso de almacenamiento
/// Configures the storage usage service
pub fn with_storage_usage_service(
mut self,
storage_usage_service: Arc<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::ports::storage_ports::{FileReadPort, FileWritePort};
/// Factory para crear implementaciones de casos de uso de archivos
/// Factory for creating file use case implementations
pub struct AppFileUseCaseFactory {
file_read_repository: Arc<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort>,
}
impl AppFileUseCaseFactory {
/// Crea una nueva factory para casos de uso de archivos
/// Creates a new factory for file use cases
pub fn new(
file_read_repository: Arc<dyn FileReadPort>,
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::common::errors::{DomainError, ErrorKind};
/// Implementación del caso de uso para operaciones de carpetas
/// Implementation of the use case for folder operations
pub struct FolderService {
folder_storage: Arc<dyn FolderStoragePort>,
}
impl FolderService {
/// Crea un nuevo servicio de carpetas
/// Creates a new folder service
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
Self { folder_storage }
}
@@ -72,9 +72,9 @@ impl FolderService {
#[async_trait]
impl FolderUseCase for FolderService {
/// Crea una nueva carpeta
/// Creates a new folder
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
// Validación de entrada
// Input validation
if dto.name.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -83,7 +83,7 @@ impl FolderUseCase for FolderService {
));
}
// Si se proporciona un parent_id, verificar que existe
// If a parent_id is provided, verify it exists
if let Some(parent_id) = &dto.parent_id {
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists {
@@ -91,16 +91,16 @@ impl FolderUseCase for FolderService {
}
}
// Crear la carpeta
// Create the folder
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?;
// Convertir a DTO
// Convert to DTO
Ok(FolderDto::from(folder))
}
/// Obtiene una carpeta por su ID
/// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
let folder = self.folder_storage.get_folder(id)
.await
@@ -109,9 +109,9 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Obtiene una carpeta por su ruta
/// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
// Convertir la ruta de string a StoragePath
// Convert the string path to StoragePath
let storage_path = StoragePath::from_string(path);
let folder = self.folder_storage.get_folder_by_path(&storage_path)
@@ -121,39 +121,39 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Lista carpetas dentro de una carpeta padre
/// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
let folders = self.folder_storage.list_folders(parent_id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?;
// Convertir a DTOs
// Convert to DTOs
Ok(folders.into_iter().map(FolderDto::from).collect())
}
/// Lista carpetas con paginación
/// Lists folders with pagination
async fn list_folders_paginated(
&self,
parent_id: Option<&str>,
pagination: &crate::application::dtos::pagination::PaginationRequestDto
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
// Validar y ajustar la paginación
// Validate and adjust pagination
let pagination = pagination.validate_and_adjust();
// Obtener carpetas paginadas y conteo total
// Get paginated folders and total count
let (folders, total_items) = self.folder_storage.list_folders_paginated(
parent_id,
pagination.offset(),
pagination.limit(),
true // Siempre incluir total para mejor UX
true // Always include total for better UX
)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?;
// El total es necesario para calcular la paginación
// The total is needed to calculate pagination
let total = total_items.unwrap_or(folders.len());
// Convertir a PaginatedResponseDto
// Convert to PaginatedResponseDto
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
folders.into_iter().map(FolderDto::from).collect(),
pagination.page,
@@ -164,9 +164,9 @@ impl FolderUseCase for FolderService {
Ok(response)
}
/// Renombra una carpeta
/// Renames a folder
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> {
// Validación de entrada
// Input validation
if dto.name.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -175,15 +175,15 @@ impl FolderUseCase for FolderService {
));
}
// Verificar que la carpeta existe
// Verify the folder exists
let existing_folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?;
// Crear transacción para renombrar
// Create transaction for renaming
let mut transaction = StorageTransaction::new("rename_folder");
// Operación principal: renombrar carpeta
// Main operation: rename folder
// Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string();
@@ -200,7 +200,7 @@ impl FolderUseCase for FolderService {
let id_clone = id.to_string();
async move {
// En caso de fallo, restaurar el nombre original
// In case of failure, restore the original name
storage.rename_folder(&id_clone, original_name).await
.map(|_| ())
.map_err(|e| DomainError::new(
@@ -211,13 +211,13 @@ impl FolderUseCase for FolderService {
}
};
// Añadir a la transacción
// Add to the transaction
transaction.add_operation(rename_op, rollback_op);
// Ejecutar transacción
// Execute transaction
transaction.commit().await?;
// Obtener la carpeta renombrada
// Get the renamed folder
let folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?;
@@ -225,16 +225,16 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Mueve una carpeta a un nuevo padre
/// Moves a folder to a new parent
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
// Verificar que la carpeta origen existe
// Verify the source folder exists
let source_folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?;
// Si se especifica un parent_id, verificar que existe
// If a parent_id is specified, verify it exists
if let Some(parent_id) = &dto.parent_id {
// Verificar que no estamos intentando mover la carpeta a sí misma o a uno de sus descendientes
// Verify we are not trying to move the folder into itself or one of its descendants
if parent_id == id {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -243,19 +243,19 @@ impl FolderUseCase for FolderService {
));
}
// Verificar que el destino existe
// Verify the destination exists
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists {
return Err(DomainError::not_found("Folder", parent_id));
}
// TODO: Idealmente deberíamos verificar toda la jerarquía para evitar ciclos
// TODO: Ideally we should verify the entire hierarchy to prevent cycles
}
// Crear transacción para mover
// Create transaction for moving
let mut transaction = StorageTransaction::new("move_folder");
// Operación principal: mover carpeta
// Main operation: move folder
// Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string();
@@ -275,7 +275,7 @@ impl FolderUseCase for FolderService {
let id_clone = id.to_string();
async move {
// En caso de fallo, restaurar la ubicación original
// In case of failure, restore the original location
storage.move_folder(&id_clone, original_parent_id.as_deref()).await
.map(|_| ())
.map_err(|e| DomainError::new(
@@ -286,13 +286,13 @@ impl FolderUseCase for FolderService {
}
};
// Añadir a la transacción
// Add to the transaction
transaction.add_operation(move_op, rollback_op);
// Ejecutar transacción
// Execute transaction
transaction.commit().await?;
// Obtener la carpeta movida
// Get the moved folder
let folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?;
@@ -300,16 +300,16 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Elimina una carpeta
/// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
// Verificar que la carpeta existe
// Verify the folder exists
let _folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?;
// En una implementación real, podríamos verificar permisos, dependencias, etc.
// In a real implementation, we could verify permissions, dependencies, etc.
// Eliminar la carpeta
// Delete the folder
self.folder_storage.delete_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
+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::dtos::recent_dto::RecentItemDto;
/// Implementación del caso de uso para gestionar elementos recientes.
/// Implementation of the use case for managing recent items.
///
/// Depende de `RecentItemsRepositoryPort` (outbound port) en lugar
/// de acceder directamente a `PgPool`, siguiendo la arquitectura hexagonal.
/// Depends on `RecentItemsRepositoryPort` (outbound port) instead
/// of accessing `PgPool` directly, following the hexagonal architecture.
pub struct RecentService {
repo: Arc<dyn RecentItemsRepositoryPort>,
max_recent_items: i32,
}
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 {
Self {
repo,
@@ -26,51 +26,51 @@ impl RecentService {
#[async_trait]
impl RecentItemsUseCase for RecentService {
/// Obtener elementos recientes de un usuario
/// Get recent items for a user
async fn get_recent_items(&self, user_id: &str, limit: Option<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 items = self.repo.get_recent_items(user_id, limit_value).await?;
info!("Recuperados {} elementos recientes para usuario {}", items.len(), user_id);
info!("Retrieved {} recent items for user {}", items.len(), user_id);
Ok(items)
}
/// Registrar acceso a un elemento
/// Record access to an item
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
info!("Registrando acceso a {} '{}' para usuario {}", item_type, item_id, user_id);
info!("Recording access to {} '{}' for user {}", item_type, item_id, user_id);
if item_type != "file" && item_type != "folder" {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"RecentItems",
"El tipo de elemento debe ser 'file' o 'folder'",
"Item type must be 'file' or 'folder'",
));
}
self.repo.upsert_access(user_id, item_id, item_type).await?;
self.repo.prune(user_id, self.max_recent_items).await?;
info!("Registrado correctamente acceso a {} '{}' para usuario {}", item_type, item_id, user_id);
info!("Successfully recorded access to {} '{}' for user {}", item_type, item_id, user_id);
Ok(())
}
/// Eliminar un elemento de recientes
/// Remove an item from recent
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<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?;
info!(
"{} {} '{}' de recientes para usuario {}",
if removed { "Eliminado correctamente" } else { "No se encontró" },
if removed { "Successfully removed" } else { "Not found" },
item_type, item_id, user_id
);
Ok(removed)
}
/// Limpiar todos los elementos recientes
/// Clear all recent items
async fn clear_recent_items(&self, user_id: &str) -> Result<()> {
info!("Limpiando todos los elementos recientes para usuario {}", user_id);
info!("Clearing all recent items for user {}", user_id);
self.repo.clear_all(user_id).await?;
info!("Limpiados todos los elementos recientes para usuario {}", user_id);
info!("Cleared all recent items for user {}", user_id);
Ok(())
}
}
+93 -93
View File
@@ -14,57 +14,57 @@ use crate::application::ports::outbound::FolderStoragePort;
use crate::application::ports::storage_ports::FileReadPort;
/**
* Implementación del servicio de búsqueda para archivos y carpetas.
* Search service implementation for files and folders.
*
* Este servicio implementa la funcionalidad de búsqueda avanzada que permite
* a los usuarios encontrar archivos y carpetas basados en diversos criterios
* como nombre, tipo, fecha y tamaño. También incluye una caché para mejorar
* el rendimiento de búsquedas repetidas.
* This service implements the advanced search functionality that allows
* users to find files and folders based on various criteria
* such as name, type, date and size. It also includes a cache to improve
* the performance of repeated searches.
*/
pub struct SearchService {
/// Repositorio para operaciones con archivos
/// Repository for file operations
file_repository: Arc<dyn FileReadPort>,
/// Repositorio para operaciones con carpetas
/// Repository for folder operations
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>>>,
/// Duración de validez de la caché en segundos
/// Cache validity duration in seconds
cache_ttl: u64,
/// Tamaño máximo de la caché (número de resultados almacenados)
/// Maximum cache size (number of stored results)
max_cache_size: usize,
}
/// Clave para la caché de búsqueda
/// Key for the search cache
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct SearchCacheKey {
/// Representación serializada de los criterios de búsqueda
/// Serialized representation of the search criteria
criteria_hash: String,
/// ID del usuario (para aislar búsquedas entre usuarios)
/// User ID (to isolate searches between users)
user_id: String,
}
/// Resultado de búsqueda en caché con tiempo de expiración
/// Cached search result with expiration time
struct CachedSearchResult {
/// Resultados de la búsqueda
/// Search results
results: SearchResultsDto,
/// Momento en que se creó la entrada de caché
/// Time when the cache entry was created
timestamp: Instant,
}
impl SearchService {
/**
* Crea una nueva instancia del servicio de búsqueda.
* Creates a new instance of the search service.
*
* @param file_repository Repositorio para operaciones con archivos
* @param folder_repository Repositorio para operaciones con carpetas
* @param cache_ttl Tiempo de vida de la caché en segundos (0 para desactivar)
* @param max_cache_size Tamaño máximo de la caché
* @param file_repository Repository for file operations
* @param folder_repository Repository for folder operations
* @param cache_ttl Cache time-to-live in seconds (0 to disable)
* @param max_cache_size Maximum cache size
*/
pub fn new(
file_repository: Arc<dyn FileReadPort>,
@@ -80,7 +80,7 @@ impl SearchService {
max_cache_size,
};
// Iniciar tarea de limpieza de caché si TTL > 0
// Start cache cleanup task if TTL > 0
if cache_ttl > 0 {
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl);
}
@@ -89,10 +89,10 @@ impl SearchService {
}
/**
* Inicia una tarea asíncrona para limpiar entradas expiradas de la caché.
* Starts an asynchronous task to clean up expired cache entries.
*
* @param cache_ref Referencia a la caché compartida
* @param ttl_seconds TTL en segundos
* @param cache_ref Reference to the shared cache
* @param ttl_seconds TTL in seconds
*/
fn start_cache_cleanup_task(
cache_ref: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
@@ -105,18 +105,18 @@ impl SearchService {
loop {
time::sleep(cleanup_interval).await;
// Obtener lock y limpiar entradas expiradas
// Acquire lock and clean up expired entries
if let Ok(mut cache) = cache_ref.lock() {
let now = Instant::now();
// Identificar entradas expiradas
// Identify expired entries
let expired_keys: Vec<SearchCacheKey> = cache
.iter()
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
.map(|(key, _)| key.clone())
.collect();
// Eliminar entradas expiradas
// Remove expired entries
for key in expired_keys {
cache.remove(&key);
}
@@ -126,14 +126,14 @@ impl SearchService {
}
/**
* Crea una clave de caché a partir de los criterios de búsqueda.
* Creates a cache key from the search criteria.
*
* @param criteria Criterios de búsqueda
* @param user_id ID del usuario (para aislar caché entre usuarios)
* @return Clave para la caché
* @param criteria Search criteria
* @param user_id User ID (to isolate cache between users)
* @return Cache key
*/
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey {
// Serializar criterios para generar un hash
// Serialize criteria to generate a hash
let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
SearchCacheKey {
@@ -143,13 +143,13 @@ impl SearchService {
}
/**
* Intenta obtener resultados de la caché.
* Attempts to retrieve results from the cache.
*
* @param key Clave de caché
* @return Opcionalmente, los resultados si existen y no han expirado
* @param key Cache key
* @return Optionally, the results if they exist and have not expired
*/
fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> {
// Si TTL es 0, la caché está desactivada
// If TTL is 0, the cache is disabled
if self.cache_ttl == 0 {
return None;
}
@@ -159,7 +159,7 @@ impl SearchService {
let now = Instant::now();
let ttl = Duration::from_secs(self.cache_ttl);
// Comprobar si la entrada ha expirado
// Check if the entry has expired
if now.duration_since(cached_result.timestamp) < ttl {
return Some(cached_result.results.clone());
}
@@ -170,19 +170,19 @@ impl SearchService {
}
/**
* Almacena resultados en la caché.
* Stores results in the cache.
*
* @param key Clave de caché
* @param results Resultados a almacenar
* @param key Cache key
* @param results Results to store
*/
fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) {
// Si TTL es 0, la caché está desactivada
// If TTL is 0, the cache is disabled
if self.cache_ttl == 0 {
return;
}
if let Ok(mut cache) = self.search_cache.lock() {
// Si la caché está llena, eliminar la entrada más antigua
// If the cache is full, remove the oldest entry
if cache.len() >= self.max_cache_size {
if let Some((oldest_key, _)) = cache
.iter()
@@ -192,7 +192,7 @@ impl SearchService {
}
}
// Almacenar el nuevo resultado
// Store the new result
cache.insert(key, CachedSearchResult {
results,
timestamp: Instant::now(),
@@ -201,35 +201,35 @@ impl SearchService {
}
/**
* Filtra archivos según los criterios de búsqueda.
* Filters files according to the search criteria.
*
* @param files Lista de archivos a filtrar
* @param criteria Criterios de búsqueda
* @return Archivos que cumplen con los criterios
* @param files List of files to filter
* @param criteria Search criteria
* @return Files that match the criteria
*/
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
files.into_iter()
.filter(|file| {
// Filtrar por nombre
// Filter by name
if let Some(name_query) = &criteria.name_contains {
if !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false;
}
}
// Filtrar por tipo de archivo (extensión)
// Filter by file type (extension)
if let Some(file_types) = &criteria.file_types {
if let Some(extension) = file.name.split('.').last() {
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) {
return false;
}
} else {
// No tiene extensión
// Has no extension
return false;
}
}
// Filtrar por fecha de creación
// Filter by creation date
if let Some(created_after) = criteria.created_after {
if file.created_at < created_after {
return false;
@@ -242,7 +242,7 @@ impl SearchService {
}
}
// Filtrar por fecha de modificación
// Filter by modification date
if let Some(modified_after) = criteria.modified_after {
if file.modified_at < modified_after {
return false;
@@ -255,7 +255,7 @@ impl SearchService {
}
}
// Filtrar por tamaño
// Filter by size
if let Some(min_size) = criteria.min_size {
if file.size < min_size {
return false;
@@ -274,23 +274,23 @@ impl SearchService {
}
/**
* Filtra carpetas según los criterios de búsqueda.
* Filters folders according to the search criteria.
*
* @param folders Lista de carpetas a filtrar
* @param criteria Criterios de búsqueda
* @return Carpetas que cumplen con los criterios
* @param folders List of folders to filter
* @param criteria Search criteria
* @return Folders that match the criteria
*/
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
folders.into_iter()
.filter(|folder| {
// Filtrar por nombre
// Filter by name
if let Some(name_query) = &criteria.name_contains {
if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false;
}
}
// Filtrar por fecha de creación
// Filter by creation date
if let Some(created_after) = criteria.created_after {
if folder.created_at < created_after {
return false;
@@ -303,7 +303,7 @@ impl SearchService {
}
}
// Filtrar por fecha de modificación
// Filter by modification date
if let Some(modified_after) = criteria.modified_after {
if folder.modified_at < modified_after {
return false;
@@ -322,12 +322,12 @@ impl SearchService {
}
/**
* Implementación de la búsqueda recursiva a través de carpetas.
* Implementation of recursive search through folders.
*
* @param current_folder_id ID de la carpeta actual
* @param criteria Criterios de búsqueda
* @param found_files Archivos encontrados hasta ahora
* @param found_folders Carpetas encontradas hasta ahora
* @param current_folder_id ID of the current folder
* @param criteria Search criteria
* @param found_files Files found so far
* @param found_folders Folders found so far
*/
async fn search_recursive(
&self,
@@ -337,31 +337,31 @@ impl SearchService {
found_folders: &mut Vec<FolderDto>,
) -> Result<()> {
Box::pin(async move {
// Listar archivos en la carpeta actual
// List files in the current folder
let files = self.file_repository.list_files(current_folder_id).await?;
// Filtrar archivos según criterios y agregarlos a los resultados
// Filter files according to criteria and add them to the results
let filtered_files = self.filter_files(
files.into_iter().map(FileDto::from).collect(),
criteria
);
found_files.extend(filtered_files);
// Si la búsqueda es recursiva, procesar subcarpetas
// If the search is recursive, process subfolders
if criteria.recursive {
// Listar subcarpetas
// List subfolders
let folders = self.folder_repository.list_folders(current_folder_id).await?;
// Filtrar carpetas según criterios y agregarlas a los resultados
// Filter folders according to criteria and add them to the results
let filtered_folders: Vec<FolderDto> = self.filter_folders(
folders.into_iter().map(FolderDto::from).collect(),
criteria
);
// Añadir las carpetas filtradas a los resultados
// Add filtered folders to the results
found_folders.extend(filtered_folders.iter().cloned());
// Buscar recursivamente en cada subcarpeta
// Search recursively in each subfolder
for folder in filtered_folders {
self.search_recursive(
Some(&folder.id),
@@ -380,26 +380,26 @@ impl SearchService {
#[async_trait]
impl SearchUseCase for SearchService {
/**
* Realiza una búsqueda basada en los criterios especificados.
* Performs a search based on the specified criteria.
*
* @param criteria Criterios de búsqueda
* @return Resultados de la búsqueda
* @param criteria Search criteria
* @return Search results
*/
async fn search(&self, criteria: SearchCriteriaDto) -> Result<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 cache_key = self.create_cache_key(&criteria, user_id);
// Intentar obtener resultados de la caché
// Try to get results from the cache
if let Some(cached_results) = self.get_from_cache(&cache_key) {
return Ok(cached_results);
}
// Inicializar colecciones para resultados
// Initialize collections for results
let mut found_files: Vec<FileDto> = 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(
criteria.folder_id.as_deref(),
&criteria,
@@ -407,29 +407,29 @@ impl SearchUseCase for SearchService {
&mut found_folders,
).await?;
// Aplicar paginación
// Apply pagination
let total_count = found_files.len() + found_folders.len();
// Ordenar por relevancia o fecha según criterios
// Por defecto, ordenamos por fecha de modificación (más reciente primero)
// Sort by relevance or date according to criteria
// By default, sort by modification date (most recent first)
found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
found_folders.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
// Aplicar límite y offset para paginación
// Apply limit and offset for pagination
let start_idx = criteria.offset.min(total_count);
let end_idx = (criteria.offset + criteria.limit).min(total_count);
let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx)
.map(|i| {
if i < found_folders.len() {
(true, i) // Es una carpeta
(true, i) // It's a folder
} else {
(false, i - found_folders.len()) // Es un archivo
(false, i - found_folders.len()) // It's a file
}
})
.collect();
// Extraer elementos paginados
// Extract paginated items
let mut paginated_folders = Vec::new();
let mut paginated_files = Vec::new();
@@ -445,7 +445,7 @@ impl SearchUseCase for SearchService {
}
}
// Crear objeto de resultados
// Create results object
let search_results = SearchResultsDto::new(
paginated_files,
paginated_folders,
@@ -454,16 +454,16 @@ impl SearchUseCase for SearchService {
Some(total_count),
);
// Almacenar en caché
// Store in cache
self.store_in_cache(cache_key, search_results.clone());
Ok(search_results)
}
/**
* Limpia la caché de resultados de búsqueda.
* Clears the search results cache.
*
* @return Resultado indicando éxito
* @return Result indicating success
*/
async fn clear_search_cache(&self) -> Result<()> {
if let Ok(mut cache) = self.search_cache.lock() {
@@ -473,9 +473,9 @@ impl SearchUseCase for SearchService {
}
}
// Implementar el caso de uso de prueba (stub)
// Implement the test use case (stub)
impl SearchService {
/// Crea una versión stub del servicio para pruebas
/// Creates a stub version of the service for testing
pub fn new_stub() -> impl SearchUseCase {
struct SearchServiceStub;
+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(
&self,
item_id: &str,
@@ -89,13 +89,13 @@ impl ShareService {
match item_type {
ShareItemType::File => {
self.file_repository
.get_file(item_id) // Usando el método correcto del trait FileStoragePort
.get_file(item_id) // Using the correct method from the FileStoragePort trait
.await
.map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?;
}
ShareItemType::Folder => {
self.folder_repository
.get_folder(item_id) // Usando el método correcto del trait FolderStoragePort
.get_folder(item_id) // Using the correct method from the FolderStoragePort trait
.await
.map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?;
}
@@ -103,7 +103,7 @@ impl ShareService {
Ok(())
}
/// Hash de contraseña usando Argon2id (resistente a timing attacks y GPU attacks)
/// Password hash using Argon2id (resistant to timing attacks and GPU attacks)
fn hash_password(&self, password: &str) -> String {
use argon2::{Argon2, PasswordHasher};
use argon2::password_hash::SaltString;
@@ -126,20 +126,20 @@ impl ShareUseCase for ShareService {
user_id: &str,
dto: CreateShareDto,
) -> Result<ShareDto, DomainError> {
// Convertir el tipo de elemento
// Convert the item type
let item_type = ShareItemType::try_from(dto.item_type.as_str())
.map_err(|e| ShareServiceError::InvalidItemType(e.to_string()))?;
// Verificar que el elemento existe
// Verify that the item exists
self.verify_item_exists(&dto.item_id, &item_type).await?;
// Convertir el DTO de permisos si existe
// Convert the permissions DTO if it exists
let permissions = dto.permissions.map(|p| p.to_entity());
// Hash de contraseña si existe
// Hash the password if provided
let password_hash = dto.password.map(|p| self.hash_password(&p));
// Crear la entidad Share
// Create the Share entity
let share = Share::new(
dto.item_id.clone(),
item_type,
@@ -150,48 +150,48 @@ impl ShareUseCase for ShareService {
)
.map_err(|e| ShareServiceError::Validation(e.to_string()))?;
// Guardar en el repositorio
// Save to the repository
let saved_share = self
.share_repository
.save_share(&share)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&saved_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
async fn get_shared_link(&self, id: &str) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido por su ID
// Find the shared link by its ID
let share = self
.share_repository
.find_share_by_id(id)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido por su token
// Find the shared link by its token
let share = self
.share_repository
.find_share_by_token(token)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
@@ -200,17 +200,17 @@ impl ShareUseCase for ShareService {
item_id: &str,
item_type: &ShareItemType,
) -> Result<Vec<ShareDto>, DomainError> {
// Buscar todos los enlaces compartidos para el elemento
// Find all shared links for the item
let shares = self
.share_repository
.find_shares_by_item(item_id, item_type)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Filtrar los enlaces expirados
// Filter out expired links
let active_shares: Vec<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
.iter()
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
@@ -224,14 +224,14 @@ impl ShareUseCase for ShareService {
id: &str,
dto: UpdateShareDto,
) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido existente
// Find the existing shared link
let mut share = self
.share_repository
.find_share_by_id(id)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
// Actualizar permisos si se proporcionan
// Update permissions if provided
if let Some(permissions_dto) = dto.permissions {
let permissions = SharePermissions::new(
permissions_dto.read,
@@ -241,7 +241,7 @@ impl ShareUseCase for ShareService {
share = share.with_permissions(permissions);
}
// Actualizar contraseña si se proporciona
// Update password if provided
if let Some(password) = dto.password {
let password_hash = if password.is_empty() {
None
@@ -251,24 +251,24 @@ impl ShareUseCase for ShareService {
share = share.with_password(password_hash);
}
// Actualizar fecha de expiración si se proporciona
// Update expiration date if provided
if dto.expires_at.is_some() {
share = share.with_expiration(dto.expires_at);
}
// Guardar los cambios
// Save the changes
let updated_share = self
.share_repository
.update_share(&share)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&updated_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> {
// Eliminar el enlace compartido
// Delete the shared link
self.share_repository
.delete_share(id)
.await
@@ -283,23 +283,23 @@ impl ShareUseCase for ShareService {
page: usize,
per_page: usize,
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
// Calcular offset para paginación
// Calculate offset for pagination
let offset = (page - 1) * per_page;
// Buscar los enlaces compartidos del usuario
// Find the user's shared links
let (shares, total) = self
.share_repository
.find_shares_by_user(user_id, offset, per_page)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convertir las entidades a DTOs
// Convert the entities to DTOs
let share_dtos: Vec<ShareDto> = shares
.iter()
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
.collect();
// Crear el resultado paginado
// Create the paginated result
let paginated = PaginatedResponseDto::new(
share_dtos,
page,
@@ -315,19 +315,19 @@ impl ShareUseCase for ShareService {
token: &str,
password: &str,
) -> Result<bool, DomainError> {
// Buscar el enlace compartido por su token
// Find the shared link by its token
let share = self
.share_repository
.find_share_by_token(token)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Verificar la contraseña usando el port de infraestructura
// Verify the password using the infrastructure port
match share.password_hash() {
Some(hash) => {
self.password_hasher.verify_password(password, hash)
@@ -337,22 +337,22 @@ impl ShareUseCase for ShareService {
}
async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError> {
// Buscar el enlace compartido por su token
// Find the shared link by its token
let share = self
.share_repository
.find_share_by_token(token)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Incrementar el contador de accesos
// Increment the access counter
let updated_share = share.increment_access_count();
// Guardar los cambios
// Save the changes
self.share_repository
.update_share(&updated_share)
.await
+31 -31
View File
@@ -7,69 +7,69 @@ use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath;
use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort};
/// Errores específicos del mediador de almacenamiento
/// Storage mediator specific errors
#[derive(Debug, Error)]
pub enum StorageMediatorError {
#[error("Entidad no encontrada: {0}")]
#[error("Entity not found: {0}")]
NotFound(String),
#[error("Entidad ya existe: {0}")]
#[error("Entity already exists: {0}")]
AlreadyExists(String),
#[error("Ruta inválida: {0}")]
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Error de acceso: {0}")]
#[error("Access error: {0}")]
AccessError(String),
#[error("Error interno: {0}")]
#[error("Internal error: {0}")]
InternalError(String),
#[error("Error de dominio: {0}")]
#[error("Domain error: {0}")]
DomainError(#[from] crate::common::errors::DomainError),
}
/// Tipo de resultado para las operaciones del mediador
/// Result type for mediator operations
pub type StorageMediatorResult<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]
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>;
/// 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>;
/// 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>;
/// 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>;
/// 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>;
/// 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>;
/// 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>;
/// Resuelve una ruta relativa a absoluta (legacy)
/// Resolves a relative path to absolute (legacy)
fn resolve_path(&self, relative_path: &Path) -> PathBuf;
/// Resuelve una ruta de dominio a una ruta física absoluta
/// Resolves a domain path to an absolute physical path
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf;
/// Crea un directorio si no existe (legacy)
/// Creates a directory if it does not exist (legacy)
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
/// Crea un directorio si no existe
/// Creates a directory if it does not exist
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>;
}
/// Implementación concreta del mediador de almacenamiento
/// Concrete implementation of the storage mediator
pub struct FileSystemStorageMediator {
pub folder_storage_port: Arc<dyn FolderStoragePort>,
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> {
let abs_path = self.resolve_path(path);
// Verificar si existe como archivo (no como directorio)
// Check if it exists as a file (not as a directory)
let exists = abs_path.exists() && abs_path.is_file();
Ok(exists)
@@ -198,7 +198,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path);
// Verificar si existe como archivo (no como directorio)
// Check if it exists as a file (not as a directory)
let exists = abs_path.exists() && abs_path.is_file();
Ok(exists)
@@ -207,7 +207,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_path(path);
// Verificar si existe como directorio
// Check if it exists as a directory
let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists)
@@ -216,7 +216,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path);
// Verificar si existe como directorio
// Check if it exists as a directory
let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists)
@@ -236,13 +236,13 @@ impl StorageMediator for FileSystemStorageMediator {
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
let abs_path = self.resolve_path(path);
// Crear directorios si no existen
// Create directories if they don't exist
if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
} else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath(
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
format!("Path exists but is not a directory: {}", abs_path.display())
));
}
@@ -252,13 +252,13 @@ impl StorageMediator for FileSystemStorageMediator {
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
let abs_path = self.resolve_storage_path(storage_path);
// Crear directorios si no existen
// Create directories if they don't exist
if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
} else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath(
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
format!("Path exists but is not a directory: {}", abs_path.display())
));
}
+4 -14
View File
@@ -303,17 +303,7 @@ impl TrashUseCase for TrashService {
}
};
// Obtener el elemento de la papelera
info!("Retrieving trash item from repository: ID={}", trash_id);
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await;
match item_result {
Ok(Some(item)) => {
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}",
trash_id, item.item_type(), item.original_id());
// Restore based on type
match item.item_type() {
// Get the trash item
TrashedItemType::File => {
// Restore the file to its original location
let file_id = item.original_id().to_string();
@@ -428,7 +418,7 @@ impl TrashUseCase for TrashService {
}
};
// Obtener el elemento de la papelera
// Get the trash item
info!("Retrieving trash item from repository: ID={}", trash_id);
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await;
@@ -440,7 +430,7 @@ impl TrashUseCase for TrashService {
// Permanently delete based on type
match item.item_type() {
TrashedItemType::File => {
// Eliminar el archivo permanentemente
// Permanently delete the file
let file_id = item.original_id().to_string();
info!("Permanently deleting file: {}", file_id);
@@ -466,7 +456,7 @@ impl TrashUseCase for TrashService {
}
},
TrashedItemType::Folder => {
// Eliminar la carpeta permanentemente
// Permanently delete the folder
let folder_id = item.original_id().to_string();
info!("Permanently deleting folder: {}", folder_id);
@@ -2,22 +2,22 @@ use std::future::Future;
use std::pin::Pin;
use crate::common::errors::{DomainError, ErrorKind};
/// Tipo para operaciones y rollbacks asíncronos
/// Type for async operations and rollbacks
type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>;
/// Transacción para operaciones de almacenamiento
/// Permite definir un conjunto de operaciones y sus rollbacks correspondientes
/// Transaction for storage operations
/// Allows defining a set of operations and their corresponding rollbacks
pub struct StorageTransaction {
/// Operaciones a ejecutar
/// Operations to execute
operations: Vec<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>>,
/// Nombre de la transacción para logging
/// Transaction name for logging
name: String,
}
impl StorageTransaction {
/// Crea una nueva transacción
/// Creates a new transaction
pub fn new(name: &str) -> Self {
Self {
operations: Vec::new(),
@@ -26,7 +26,7 @@ impl StorageTransaction {
}
}
/// Añade una operación a la transacción con su correspondiente rollback
/// Adds an operation to the transaction with its corresponding rollback
pub fn add_operation<F, R>(&mut self, operation: F, rollback: R)
where
F: Future<Output = Result<(), DomainError>> + Send + 'static,
@@ -36,74 +36,74 @@ impl StorageTransaction {
self.rollbacks.push(Box::new(move || Box::pin(rollback)));
}
/// Añade una operación sin rollback (para limpieza o logging)
/// Adds an operation without rollback (for cleanup or logging)
pub fn add_finalizer<F>(&mut self, finalizer: F)
where
F: Future<Output = Result<(), DomainError>> + Send + 'static,
{
// El rollback es una operación nula
// The rollback is a no-op
let noop = async { Ok(()) };
self.operations.push(Box::new(move || Box::pin(finalizer)));
self.rollbacks.push(Box::new(move || Box::pin(noop)));
}
/// Ejecuta la transacción aplicando todas las operaciones en orden
/// Si alguna falla, ejecuta los rollbacks en orden inverso
/// Executes the transaction by applying all operations in order
/// If any fails, executes rollbacks in reverse order
pub async fn commit(mut self) -> Result<(), DomainError> {
tracing::debug!("Iniciando transacción: {}", self.name);
tracing::debug!("Starting transaction: {}", self.name);
let mut completed_ops = Vec::new();
// Extraer operaciones para evitar problemas de propiedad
// Extract operations to avoid ownership issues
let operations = std::mem::take(&mut self.operations);
let transaction_name = self.name.clone();
// Ejecutar operaciones
// Execute operations
for (i, op) in operations.into_iter().enumerate() {
match op().await {
Ok(()) => {
completed_ops.push(i);
tracing::trace!("Operación {} completada en transacción: {}", i, transaction_name);
tracing::trace!("Operation {} completed in transaction: {}", i, transaction_name);
}
Err(e) => {
tracing::error!("Error en operación {} de transacción {}: {}", i, transaction_name, e);
tracing::error!("Error in operation {} of transaction {}: {}", i, transaction_name, e);
// Ejecutar rollbacks para las operaciones completadas en orden inverso
// Execute rollbacks for completed operations in reverse order
self.rollback(completed_ops).await?;
return Err(DomainError::new(
ErrorKind::InternalError,
"Transaction",
format!("Falló la transacción '{}': {}", transaction_name, e)
format!("Transaction '{}' failed: {}", transaction_name, e)
).with_source(e));
}
}
}
tracing::debug!("Transacción completada exitosamente: {}", transaction_name);
tracing::debug!("Transaction completed successfully: {}", transaction_name);
Ok(())
}
/// Ejecuta rollbacks para las operaciones completadas
/// Executes rollbacks for completed operations
async fn rollback(mut self, completed_ops: Vec<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();
// Extraer rollbacks para evitar problemas de propiedad
// Extract rollbacks to avoid ownership issues
let mut rollbacks = Vec::new();
std::mem::swap(&mut rollbacks, &mut self.rollbacks);
// Ejecutar rollbacks en orden inverso
// Execute rollbacks in reverse order
for i in completed_ops.into_iter().rev() {
if i < rollbacks.len() {
// Tomar propiedad del rollback (obtener una referencia mutable)
// Take ownership of the rollback (get a mutable reference)
if let Some(rb) = rollbacks.get_mut(i) {
// Intercambiar con una función vacía
// Swap with an empty function
let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) })));
if let Err(e) = rollback().await {
tracing::error!("Error en rollback de operación {} en transacción {}: {}",
tracing::error!("Error in rollback of operation {} in transaction {}: {}",
i, self.name, e);
rollback_errors.push(e);
}
@@ -111,20 +111,20 @@ impl StorageTransaction {
}
}
// Si hubo errores en el rollback, reportarlos
// If there were errors during rollback, report them
if !rollback_errors.is_empty() {
tracing::error!("Errores durante rollback de transacción {}: {} errores",
tracing::error!("Errors during transaction rollback {}: {} errors",
self.name, rollback_errors.len());
return Err(DomainError::new(
ErrorKind::InternalError,
"Transaction",
format!("Errores durante rollback de transacción '{}': {} errores",
format!("Errors during transaction '{}' rollback: {} errors",
self.name, rollback_errors.len())
));
}
tracing::info!("Rollback de transacción completado: {}", self.name);
tracing::info!("Transaction rollback completed: {}", self.name);
Ok(())
}
}
+14 -14
View File
@@ -5,10 +5,10 @@ use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configurar logging
// Configure logging
tracing_subscriber::fmt::init();
// Cargar variables de entorno (primero .env.local, luego .env)
// Load environment variables (.env.local first, then .env)
if let Ok(path) = env::var("DOTENV_PATH") {
dotenv::from_path(Path::new(&path)).ok();
} else {
@@ -16,34 +16,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenv::dotenv().ok();
}
// Obtener DATABASE_URL desde variables de entorno
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
// Get DATABASE_URL from environment variables
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be configured");
println!("Conectando a la base de datos...");
println!("Connecting to the database...");
// Crear pool de conexiones
// Create connection pool
let pool = PgPoolOptions::new()
.max_connections(5)
.acquire_timeout(Duration::from_secs(10))
.connect(&database_url)
.await?;
// Ejecutar migraciones
println!("Ejecutando migraciones...");
// Run migrations
println!("Running migrations...");
// Obtenemos el directorio desde una variable de entorno o usamos un valor por defecto
// Get the directory from an environment variable or use a default value
let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string());
println!("Directorio de migraciones: {}", migrations_dir);
println!("Migrations directory: {}", migrations_dir);
// Crear un migrator
// Create a migrator
let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir))
.await
.expect("No se pudo crear el migrator");
.expect("Could not create the migrator");
// Ejecutar todas las migraciones pendientes
// Run all pending migrations
migrator.run(&pool).await?;
println!("Migraciones aplicadas correctamente");
println!("Migrations applied successfully");
Ok(())
}
+77 -77
View File
@@ -2,98 +2,98 @@ use std::time::Duration;
use std::path::PathBuf;
use std::env;
/// Configuración de caché
/// Cache configuration
#[derive(Debug, Clone)]
pub struct CacheConfig {
/// TTL para entradas de archivos en caché (ms)
/// TTL for file cache entries (ms)
pub file_ttl_ms: u64,
/// TTL para entradas de directorios en caché (ms)
/// TTL for directory cache entries (ms)
pub directory_ttl_ms: u64,
/// Máximo número de entradas en caché
/// Maximum number of cache entries
pub max_entries: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
file_ttl_ms: 60_000, // 1 minuto
directory_ttl_ms: 120_000, // 2 minutos
max_entries: 10_000, // 10,000 entradas
file_ttl_ms: 60_000, // 1 minute
directory_ttl_ms: 120_000, // 2 minutes
max_entries: 10_000, // 10,000 entries
}
}
}
/// Configuración de timeouts para diferentes operaciones
/// Timeout configuration for different operations
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
/// Timeout para operaciones de archivo (ms)
/// Timeout for file operations (ms)
pub file_operation_ms: u64,
/// Timeout para operaciones de directorio (ms)
/// Timeout for directory operations (ms)
pub dir_operation_ms: u64,
/// Timeout para adquisición de locks (ms)
/// Timeout for lock acquisition (ms)
pub lock_acquisition_ms: u64,
/// Timeout para operaciones de red (ms)
/// Timeout for network operations (ms)
pub network_operation_ms: u64,
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
file_operation_ms: 10000, // 10 segundos
dir_operation_ms: 30000, // 30 segundos
lock_acquisition_ms: 5000, // 5 segundos
network_operation_ms: 15000, // 15 segundos
file_operation_ms: 10000, // 10 seconds
dir_operation_ms: 30000, // 30 seconds
lock_acquisition_ms: 5000, // 5 seconds
network_operation_ms: 15000, // 15 seconds
}
}
}
impl TimeoutConfig {
/// Obtiene un Duration para operaciones de archivo
/// Gets a Duration for file operations
pub fn file_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms)
}
/// Obtiene un Duration para operaciones de escritura de archivo
/// Gets a Duration for file write operations
pub fn file_write_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms)
}
/// Obtiene un Duration para operaciones de lectura de archivo
/// Gets a Duration for file read operations
pub fn file_read_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms)
}
/// Obtiene un Duration para operaciones de eliminación de archivo
/// Gets a Duration for file delete operations
pub fn file_delete_timeout(&self) -> Duration {
Duration::from_millis(self.file_operation_ms)
}
/// Obtiene un Duration para operaciones de directorio
/// Gets a Duration for directory operations
pub fn dir_timeout(&self) -> Duration {
Duration::from_millis(self.dir_operation_ms)
}
/// Obtiene un Duration para adquisición de locks
/// Gets a Duration for lock acquisition
pub fn lock_timeout(&self) -> Duration {
Duration::from_millis(self.lock_acquisition_ms)
}
/// Obtiene un Duration para operaciones de red
/// Gets a Duration for network operations
pub fn network_timeout(&self) -> Duration {
Duration::from_millis(self.network_operation_ms)
}
}
/// Configuración para manejo de recursos grandes
/// Configuration for large resource handling
#[derive(Debug, Clone)]
pub struct ResourceConfig {
/// Umbral en MB para considerar un archivo como grande
/// Threshold in MB to consider a file as large
pub large_file_threshold_mb: u64,
/// Umbral de entradas para considerar un directorio como grande
/// Entry threshold to consider a directory as large
pub large_dir_threshold_entries: usize,
/// Tamaño de chunk para procesamiento de archivos grandes (bytes)
/// Chunk size for large file processing (bytes)
pub chunk_size_bytes: usize,
/// Límite de tamaño de archivo para cargar en memoria (MB)
/// File size limit for loading into memory (MB)
pub max_in_memory_file_size_mb: u64,
}
@@ -101,7 +101,7 @@ impl Default for ResourceConfig {
fn default() -> Self {
Self {
large_file_threshold_mb: 100, // 100 MB
large_dir_threshold_entries: 1000, // 1000 entradas
large_dir_threshold_entries: 1000, // 1000 entries
chunk_size_bytes: 1024 * 1024, // 1 MB
max_in_memory_file_size_mb: 50, // 50 MB
}
@@ -109,71 +109,71 @@ impl Default for ResourceConfig {
}
impl ResourceConfig {
/// Convierte un tamaño en bytes a MB
/// Converts a size in bytes to MB
pub fn bytes_to_mb(&self, bytes: u64) -> u64 {
bytes / (1024 * 1024)
}
/// Determina si un archivo es considerado grande
/// Determines if a file is considered large
pub fn is_large_file(&self, size_bytes: u64) -> bool {
self.bytes_to_mb(size_bytes) >= self.large_file_threshold_mb
}
/// Determina si un archivo es suficientemente grande para procesamiento paralelo
/// Determines if a file is large enough for parallel processing
pub fn needs_parallel_processing(&self, size_bytes: u64, config: &ConcurrencyConfig) -> bool {
self.bytes_to_mb(size_bytes) >= config.min_size_for_parallel_chunks_mb
}
/// Determina si un archivo puede cargarse completo en memoria
/// Determines if a file can be fully loaded into memory
pub fn can_load_in_memory(&self, size_bytes: u64) -> bool {
self.bytes_to_mb(size_bytes) <= self.max_in_memory_file_size_mb
}
/// Determina si un directorio es considerado grande
/// Determines if a directory is considered large
pub fn is_large_directory(&self, entry_count: usize) -> bool {
entry_count >= self.large_dir_threshold_entries
}
/// Calcula el número de chunks para procesamiento paralelo
/// Calculates the number of chunks for parallel processing
pub fn calculate_optimal_chunks(&self, size_bytes: u64, config: &ConcurrencyConfig) -> usize {
// Si el archivo no es suficientemente grande, retornar 1
// If the file is not large enough, return 1
if !self.needs_parallel_processing(size_bytes, config) {
return 1;
}
// Calcular el número de chunks basado en el tamaño
// Calculate the number of chunks based on size
let chunk_count = (size_bytes as usize + config.parallel_chunk_size_bytes - 1)
/ config.parallel_chunk_size_bytes;
// Limitar al máximo de chunks en paralelo
// Limit to the maximum number of parallel chunks
chunk_count.min(config.max_parallel_chunks)
}
/// Calcula el tamaño óptimo de cada chunk para procesamiento paralelo
/// Calculates the optimal size of each chunk for parallel processing
pub fn calculate_chunk_size(&self, file_size: u64, chunk_count: usize) -> usize {
if chunk_count <= 1 {
return file_size as usize;
}
// Distribuir equitativamente el tamaño entre los chunks
// Distribute the size evenly among the chunks
((file_size as usize) + chunk_count - 1) / chunk_count
}
}
/// Configuración para operaciones concurrentes
/// Configuration for concurrent operations
#[derive(Debug, Clone)]
pub struct ConcurrencyConfig {
/// Máximo de tareas de archivo concurrentes
/// Maximum concurrent file tasks
pub max_concurrent_files: usize,
/// Máximo de tareas de directorio concurrentes
/// Maximum concurrent directory tasks
pub max_concurrent_dirs: usize,
/// Máximo de operaciones de IO concurrentes
/// Maximum concurrent IO operations
pub max_concurrent_io: usize,
/// Máximo de chunks para procesar en paralelo por archivo
/// Maximum chunks to process in parallel per file
pub max_parallel_chunks: usize,
/// Tamaño mínimo de archivo (MB) para aplicar procesamiento paralelo de chunks
/// Minimum file size (MB) to apply parallel chunk processing
pub min_size_for_parallel_chunks_mb: u64,
/// Tamaño de chunk para procesamiento paralelo (bytes)
/// Chunk size for parallel processing (bytes)
pub parallel_chunk_size_bytes: usize,
}
@@ -190,16 +190,16 @@ impl Default for ConcurrencyConfig {
}
}
/// Configuración de almacenamiento
/// Storage configuration
#[derive(Debug, Clone)]
pub struct StorageConfig {
/// Directorio raíz para el almacenamiento
/// Root directory for storage
pub root_dir: String,
/// Tamaño de chunk para procesamiento de archivos
/// Chunk size for file processing
pub chunk_size: usize,
/// Umbral para procesamiento paralelo
/// Threshold for parallel processing
pub parallel_threshold: usize,
/// Días de retención para archivos en la papelera
/// Retention days for files in the trash
pub trash_retention_days: u32,
}
@@ -209,12 +209,12 @@ impl Default for StorageConfig {
root_dir: "storage".to_string(),
chunk_size: 1024 * 1024, // 1 MB
parallel_threshold: 100 * 1024 * 1024, // 100 MB
trash_retention_days: 30, // 30 días
trash_retention_days: 30, // 30 days
}
}
}
/// Configuración de base de datos
/// Database configuration
#[derive(Debug, Clone)]
pub struct DatabaseConfig {
pub connection_string: String,
@@ -239,7 +239,7 @@ impl Default for DatabaseConfig {
}
}
/// Configuración de autenticación
/// Authentication configuration
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub jwt_secret: String,
@@ -256,15 +256,15 @@ impl Default for AuthConfig {
// to set OXICLOUD_JWT_SECRET in production. The from_env() method
// will validate this and warn/panic if not configured.
jwt_secret: String::new(),
access_token_expiry_secs: 3600, // 1 hora
refresh_token_expiry_secs: 2592000, // 30 días
access_token_expiry_secs: 3600, // 1 hour
refresh_token_expiry_secs: 2592000, // 30 days
hash_memory_cost: 65536, // 64MB
hash_time_cost: 3,
}
}
}
/// Configuración de OpenID Connect (OIDC)
/// OpenID Connect (OIDC) configuration
#[derive(Debug, Clone)]
pub struct OidcConfig {
/// Whether OIDC authentication is enabled
@@ -335,7 +335,7 @@ impl OidcConfig {
}
}
/// Configuración de funcionalidades (feature flags)
/// Feature configuration (feature flags)
#[derive(Debug, Clone)]
pub struct FeaturesConfig {
pub enable_auth: bool,
@@ -357,34 +357,34 @@ impl Default for FeaturesConfig {
}
}
/// Configuración global de la aplicación
/// Global application configuration
#[derive(Debug, Clone)]
pub struct AppConfig {
/// Ruta del directorio de almacenamiento
/// Storage directory path
pub storage_path: PathBuf,
/// Ruta del directorio de archivos estáticos
/// Static files directory path
pub static_path: PathBuf,
/// Puerto del servidor
/// Server port
pub server_port: u16,
/// Host del servidor
/// Server host
pub server_host: String,
/// Configuración de caché
/// Cache configuration
pub cache: CacheConfig,
/// Configuración de timeouts
/// Timeout configuration
pub timeouts: TimeoutConfig,
/// Configuración de recursos
/// Resource configuration
pub resources: ResourceConfig,
/// Configuración de concurrencia
/// Concurrency configuration
pub concurrency: ConcurrencyConfig,
/// Configuración de almacenamiento
/// Storage configuration
pub storage: StorageConfig,
/// Configuración de base de datos
/// Database configuration
pub database: DatabaseConfig,
/// Configuración de autenticación
/// Authentication configuration
pub auth: AuthConfig,
/// Configuración de funcionalidades
/// Feature configuration
pub features: FeaturesConfig,
/// Configuración OIDC
/// OIDC configuration
pub oidc: OidcConfig,
}
@@ -412,7 +412,7 @@ impl AppConfig {
pub fn from_env() -> Self {
let mut config = Self::default();
// Usar variables de entorno para sobrescribir valores por defecto
// Use environment variables to override default values
if let Ok(storage_path) = env::var("OXICLOUD_STORAGE_PATH") {
config.storage_path = PathBuf::from(storage_path);
}
@@ -431,7 +431,7 @@ impl AppConfig {
config.server_host = server_host;
}
// Configuración de Database
// Database configuration
if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") {
config.database.connection_string = connection_string;
}
@@ -450,7 +450,7 @@ impl AppConfig {
}
}
// Configuración Auth
// Auth configuration
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
config.auth.jwt_secret = jwt_secret;
}
@@ -582,7 +582,7 @@ impl AppConfig {
}
}
/// Obtenemos una configuración global por defecto
/// Gets a default global configuration
pub fn default_config() -> AppConfig {
AppConfig::default()
}
+53 -53
View File
@@ -53,10 +53,10 @@ use crate::common::stubs::{
StubSearchUseCase,
};
/// Fábrica para los diferentes componentes de la aplicación
/// Factory for the different application components
///
/// Esta fábrica centraliza la creación de todos los servicios de la aplicación,
/// garantizando el orden correcto de inicialización y resolviendo dependencias circulares.
/// This factory centralizes the creation of all application services,
/// ensuring the correct initialization order and resolving circular dependencies.
pub struct AppServiceFactory {
storage_path: PathBuf,
locales_path: PathBuf,
@@ -64,7 +64,7 @@ pub struct AppServiceFactory {
}
impl AppServiceFactory {
/// Crea una nueva fábrica de servicios
/// Creates a new service factory
pub fn new(storage_path: PathBuf, locales_path: PathBuf) -> Self {
Self {
storage_path,
@@ -73,7 +73,7 @@ impl AppServiceFactory {
}
}
/// Crea una nueva fábrica de servicios con configuración personalizada
/// Creates a new service factory with custom configuration
pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self {
Self {
storage_path,
@@ -82,17 +82,17 @@ impl AppServiceFactory {
}
}
/// Obtiene la configuración
/// Gets the configuration
pub fn config(&self) -> &AppConfig {
&self.config
}
/// Obtiene la ruta de almacenamiento
/// Gets the storage path
pub fn storage_path(&self) -> &PathBuf {
&self.storage_path
}
/// Inicializa los servicios base del sistema
/// Initializes the core system services
pub async fn create_core_services(&self) -> Result<CoreServices, DomainError> {
// Path service
let path_service = Arc::new(PathService::new(self.storage_path.clone()));
@@ -105,57 +105,57 @@ impl AppServiceFactory {
}));
tracing::info!("FileContentCache initialized: max 10MB/file, 512MB total, 10k entries");
// ID mapping service para carpetas
// ID mapping service for folders
let folder_id_mapping_path = self.storage_path.join("folder_ids.json");
let folder_id_mapping_service = Arc::new(
IdMappingService::new(folder_id_mapping_path).await?
);
// ID mapping service para archivos
// ID mapping service for files
let file_id_mapping_path = self.storage_path.join("file_ids.json");
let file_id_mapping_service = Arc::new(
IdMappingService::new(file_id_mapping_path).await?
);
// Optimizer con batch processing y caching
// Optimizer with batch processing and caching
let id_mapping_optimizer = Arc::new(
IdMappingOptimizer::new(folder_id_mapping_service.clone())
);
// Iniciar tarea de limpieza del optimizer
// Start optimizer cleanup task
IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone());
// Thumbnail service para generación de miniaturas
// Thumbnail service for thumbnail generation
let thumbnail_service = Arc::new(
crate::infrastructure::services::thumbnail_service::ThumbnailService::new(
&self.storage_path,
5000, // max 5000 thumbnails en cache
100 * 1024 * 1024, // max 100MB de cache
5000, // max 5000 thumbnails in cache
100 * 1024 * 1024, // max 100MB cache
)
);
// Inicializar directorios de thumbnails
// Initialize thumbnail directories
thumbnail_service.initialize().await?;
// Write-behind cache para uploads instantáneos de archivos pequeños
// Write-behind cache for instant uploads of small files
let write_behind_cache = crate::infrastructure::services::write_behind_cache::WriteBehindCache::new();
// Chunked upload service para archivos grandes (>10MB)
// Chunked upload service for large files (>10MB)
let chunked_temp_dir = std::path::PathBuf::from(&self.storage_path).join(".uploads");
let chunked_upload_service = Arc::new(
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(chunked_temp_dir)
);
// Image transcoding service para conversión automática a WebP
// Image transcoding service for automatic WebP conversion
let image_transcode_service = Arc::new(
crate::infrastructure::services::image_transcode_service::ImageTranscodeService::new(
&self.storage_path,
2000, // max 2000 imágenes transcodificadas en cache
50 * 1024 * 1024, // max 50MB de cache en memoria
2000, // max 2000 transcoded images in cache
50 * 1024 * 1024, // max 50MB in-memory cache
)
);
image_transcode_service.initialize().await?;
// Deduplication service para eliminar archivos duplicados
// Deduplication service for removing duplicate files
let dedup_service = Arc::new(
crate::infrastructure::services::dedup_service::DedupService::new(&self.storage_path)
);
@@ -189,7 +189,7 @@ impl AppServiceFactory {
})
}
/// Inicializa los servicios de repositorio
/// Initializes the repository services
pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices {
// Storage mediator - uses stub initially, will be replaced after folder repo is ready
let storage_mediator_stub: Arc<dyn StorageMediator> = Arc::new(
@@ -216,13 +216,13 @@ impl AppServiceFactory {
FileMetadataCache::default_with_config(core.config.clone())
);
// Iniciar tarea de limpieza de metadata cache
// Start metadata cache cleanup task
let cache_clone = metadata_cache.clone();
tokio::spawn(async move {
FileMetadataCache::start_cleanup_task(cache_clone).await;
});
// Buffer pool para optimización de memoria
// Buffer pool for memory optimization
let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL
BufferPool::start_cleaner(buffer_pool.clone());
@@ -232,7 +232,7 @@ impl AppServiceFactory {
buffer_pool.clone()
));
// File repositories separados para lectura y escritura
// Separate file repositories for reading and writing
let file_read_repository = Arc::new(FileFsReadRepository::new(
self.storage_path.clone(),
storage_mediator.clone(),
@@ -281,19 +281,19 @@ impl AppServiceFactory {
}
}
/// Inicializa los servicios de aplicación
/// Initializes the application services
pub fn create_application_services(
&self,
core: &CoreServices,
repos: &RepositoryServices,
trash_service: Option<Arc<dyn TrashUseCase>>,
) -> ApplicationServices {
// Servicios principales
// Main services
let folder_service = Arc::new(FolderService::new(
repos.folder_repository.clone()
));
// Servicios refactorizados con todos los puertos de infraestructura
// Refactored services with all infrastructure ports
let file_upload_service = Arc::new(FileUploadService::new_full(
repos.file_write_repository.clone(),
repos.file_read_repository.clone(),
@@ -308,7 +308,7 @@ impl AppServiceFactory {
core.image_transcode_service.clone(),
));
// FileManagementService con dedup y trash
// FileManagementService with dedup and trash
let file_management_service = Arc::new(FileManagementService::new_full(
repos.file_write_repository.clone(),
repos.file_read_repository.clone(),
@@ -325,7 +325,7 @@ impl AppServiceFactory {
repos.i18n_repository.clone()
));
// Search service con caché
// Search service with cache
let search_service: Option<Arc<dyn SearchUseCase>> = Some(Arc::new(SearchService::new(
repos.file_read_repository.clone(),
repos.folder_repository.clone(),
@@ -336,9 +336,9 @@ impl AppServiceFactory {
tracing::info!("Application services initialized");
ApplicationServices {
// Tipos concretos para handlers que los necesitan
// Concrete types for handlers that need them
folder_service_concrete: folder_service.clone(),
// Traits para abstracción
// Traits for abstraction
folder_service,
file_upload_service,
file_retrieval_service,
@@ -347,13 +347,13 @@ impl AppServiceFactory {
i18n_service,
trash_service, // Already set via parameter
search_service,
share_service: None, // Se configura después con create_share_service
favorites_service: None, // Se configura después con create_favorites_service
recent_service: None, // Se configura después con create_recent_service
share_service: None, // Configured later with create_share_service
favorites_service: None, // Configured later with create_favorites_service
recent_service: None, // Configured later with create_recent_service
}
}
/// Crea el servicio de papelera
/// Creates the trash service
pub async fn create_trash_service(
&self,
repos: &RepositoryServices,
@@ -374,7 +374,7 @@ impl AppServiceFactory {
self.config.storage.trash_retention_days,
));
// Inicializar servicio de limpieza
// Initialize cleanup service
let cleanup_service = TrashCleanupService::new(
service.clone(),
trash_repo.clone(),
@@ -387,7 +387,7 @@ impl AppServiceFactory {
Some(service as Arc<dyn TrashUseCase>)
}
/// Crea el servicio de compartición
/// Creates the sharing service
pub fn create_share_service(
&self,
repos: &RepositoryServices,
@@ -417,7 +417,7 @@ impl AppServiceFactory {
Some(service)
}
/// Crea el servicio de favoritos (requiere base de datos)
/// Creates the favorites service (requires database)
pub fn create_favorites_service(
&self,
db_pool: &Arc<PgPool>,
@@ -430,7 +430,7 @@ impl AppServiceFactory {
service
}
/// Crea el servicio de elementos recientes (requiere base de datos)
/// Creates the recent items service (requires database)
pub fn create_recent_service(
&self,
db_pool: &Arc<PgPool>,
@@ -446,7 +446,7 @@ impl AppServiceFactory {
service
}
/// Precarga traducciones
/// Preloads translations
pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) {
use crate::domain::services::i18n_service::Locale;
@@ -468,7 +468,7 @@ impl AppServiceFactory {
tracing::info!("Translations preloaded");
}
/// Precarga directorios en caché
/// Preloads directories into cache
pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) {
tracing::info!("Preloading common directories to warm up cache...");
if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await {
@@ -476,7 +476,7 @@ impl AppServiceFactory {
}
}
/// Crea el servicio de uso de almacenamiento (requiere base de datos)
/// Creates the storage usage service (requires database)
pub fn create_storage_usage_service(
&self,
repos: &RepositoryServices,
@@ -495,9 +495,9 @@ impl AppServiceFactory {
service
}
/// Construye el AppState completo usando todos los servicios de la fábrica.
/// Builds the complete AppState using all factory services.
///
/// Este es el punto de entrada principal que reemplaza toda la lógica manual de `main.rs`.
/// This is the main entry point that replaces all manual logic in `main.rs`.
pub async fn build_app_state(
&self,
db_pool: Option<Arc<PgPool>>,
@@ -677,7 +677,7 @@ impl AppServiceFactory {
}
}
/// Contenedor para servicios base
/// Container for core services
#[derive(Clone)]
pub struct CoreServices {
pub path_service: Arc<PathService>,
@@ -695,7 +695,7 @@ pub struct CoreServices {
pub config: AppConfig,
}
/// Contenedor para servicios de repositorio
/// Container for repository services
#[derive(Clone)]
pub struct RepositoryServices {
pub folder_repository: Arc<dyn FolderStoragePort>,
@@ -707,12 +707,12 @@ pub struct RepositoryServices {
pub trash_repository: Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>,
}
/// Contenedor para servicios de aplicación
/// Container for application services
#[derive(Clone)]
pub struct ApplicationServices {
// Tipos concretos para compatibilidad con handlers existentes
// Concrete types for compatibility with existing handlers
pub folder_service_concrete: Arc<FolderService>,
// Traits para abstracción
// Traits for abstraction
pub folder_service: Arc<dyn FolderUseCase>,
pub file_upload_service: Arc<dyn FileUploadUseCase>,
pub file_retrieval_service: Arc<dyn FileRetrievalUseCase>,
@@ -726,14 +726,14 @@ pub struct ApplicationServices {
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
}
/// Contenedor para servicios de autenticación
/// Container for authentication services
#[derive(Clone)]
pub struct AuthServices {
pub token_service: Arc<dyn crate::application::ports::auth_ports::TokenServicePort>,
pub auth_application_service: Arc<AuthApplicationService>,
}
/// Estado global de la aplicación para dependency injection
/// Global application state for dependency injection
#[derive(Clone)]
pub struct AppState {
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.
//! Las conversiones de errores de infraestructura (sqlx, serde_json, etc.)
//! se encuentran en infrastructure/adapters/error_adapters.rs, siguiendo
//! los principios de Clean Architecture donde el dominio no debe conocer
//! detalles de infraestructura.
//! This module re-exports domain errors for compatibility.
//! Infrastructure error conversions (sqlx, serde_json, etc.)
//! are located in infrastructure/adapters/error_adapters.rs, following
//! Clean Architecture principles where the domain should not know about
//! infrastructure details.
// Re-exportar errores del dominio para compatibilidad
// Re-export domain errors for compatibility
pub use crate::domain::errors::{DomainError, ErrorKind, Result};
// Re-exportar AppError desde interfaces para compatibilidad hacia atrás
// NOTA: El lugar canónico de AppError es ahora crate::interfaces::errors
// Re-export AppError from interfaces for backward compatibility
// NOTE: The canonical location of AppError is now crate::interfaces::errors
// Las conversiones de errores de infraestructura se han movido a:
// Infrastructure error conversions have been moved to:
// crate::infrastructure::adapters::error_adapters
//
// Para convertir errores de infraestructura a DomainError, use:
// - El trait IntoDomainError para conversiones explícitas con contexto
// - O maneje los errores en los repositorios/servicios de infraestructura
// usando map_err() con DomainError::internal_error() o métodos similares
// To convert infrastructure errors to DomainError, use:
// - The IntoDomainError trait for explicit conversions with context
// - Or handle errors in infrastructure repositories/services
// using map_err() with DomainError::internal_error() or similar methods
+1 -1
View File
@@ -14,7 +14,7 @@ use chrono::{DateTime, Utc};
use crate::common::errors::{Result, DomainError, ErrorKind};
// Re-exportar errores de entidad desde el módulo centralizado
// Re-export entity errors from the centralized module
pub use super::entity_errors::CalendarError;
/**
+1 -1
View File
@@ -14,7 +14,7 @@ use chrono::{DateTime, Utc, Duration, TimeZone};
use crate::common::errors::{Result, DomainError, ErrorKind};
// Re-exportar errores de entidad desde el módulo centralizado
// Re-export entity errors from the centralized module
pub use super::entity_errors::CalendarEventError;
/**
+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
//! sin dependencias de frameworks externos, siguiendo los principios de
//! Clean Architecture.
//! This module defines domain entity-specific errors
//! without external framework dependencies, following
//! Clean Architecture principles.
//!
//! Los errores implementan manualmente `std::error::Error` y `std::fmt::Display`
//! para mantener el dominio libre de dependencias externas.
//! Errors manually implement `std::error::Error` and `std::fmt::Display`
//! to keep the domain free of external dependencies.
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};
@@ -14,12 +14,12 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
// FILE ERRORS
// ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades File
/// Errors that can occur during File entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileError {
/// Ocurre cuando el nombre de archivo contiene caracteres inválidos o está vacío
/// Occurs when the file name contains invalid characters or is empty
InvalidFileName(String),
/// Ocurre cuando falla la validación de cualquier atributo de la entidad
/// Occurs when validation of any entity attribute fails
ValidationError(String),
}
@@ -34,19 +34,19 @@ impl Display for FileError {
impl Error for FileError {}
/// Alias de tipo para resultados de operaciones con entidades File
/// Type alias for File entity operation results
pub type FileResult<T> = Result<T, FileError>;
// ============================================================================
// FOLDER ERRORS
// ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades Folder
/// Errors that can occur during Folder entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FolderError {
/// Ocurre cuando el nombre de carpeta contiene caracteres inválidos o está vacío
/// Occurs when the folder name contains invalid characters or is empty
InvalidFolderName(String),
/// Ocurre cuando falla la validación de cualquier atributo de la entidad
/// Occurs when validation of any entity attribute fails
ValidationError(String),
}
@@ -61,54 +61,54 @@ impl Display for FolderError {
impl Error for FolderError {}
/// Alias de tipo para resultados de operaciones con entidades Folder
/// Type alias for Folder entity operation results
pub type FolderResult<T> = Result<T, FolderError>;
// ============================================================================
// USER ERRORS
// ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades User
/// Errors that can occur during User entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserError {
/// Nombre de usuario inválido
/// Invalid username
InvalidUsername(String),
/// Contraseña inválida
/// Invalid password
InvalidPassword(String),
/// Error de validación general
/// General validation error
ValidationError(String),
/// Error de autenticación
/// Authentication error
AuthenticationError(String),
}
impl Display for UserError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
UserError::InvalidUsername(msg) => write!(f, "Username inválido: {}", msg),
UserError::InvalidPassword(msg) => write!(f, "Password inválido: {}", msg),
UserError::ValidationError(msg) => write!(f, "Error en la validación: {}", msg),
UserError::AuthenticationError(msg) => write!(f, "Error en la autenticación: {}", msg),
UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg),
UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg),
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
}
}
}
impl Error for UserError {}
/// Alias de tipo para resultados de operaciones con entidades User
/// Type alias for User entity operation results
pub type UserResult<T> = Result<T, UserError>;
// ============================================================================
// SHARE ERRORS
// ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades Share
/// Errors that can occur during Share entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShareError {
/// Token de compartición inválido
/// Invalid share token
InvalidToken(String),
/// Fecha de expiración inválida
/// Invalid expiration date
InvalidExpiration(String),
/// Error de validación general
/// General validation error
ValidationError(String),
}
@@ -124,21 +124,21 @@ impl Display for ShareError {
impl Error for ShareError {}
/// Alias de tipo para resultados de operaciones con entidades Share
/// Type alias for Share entity operation results
pub type ShareResult<T> = Result<T, ShareError>;
// ============================================================================
// CALENDAR ERRORS
// ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades Calendar
/// Errors that can occur during Calendar entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarError {
/// Nombre de calendario inválido
/// Invalid calendar name
InvalidName(String),
/// Código de color inválido
/// Invalid color code
InvalidColor(String),
/// ID de propietario inválido
/// Invalid owner ID
InvalidOwnerId(String),
}
@@ -154,23 +154,23 @@ impl Display for CalendarError {
impl Error for CalendarError {}
/// Alias de tipo para resultados de operaciones con entidades Calendar
/// Type alias for Calendar entity operation results
pub type CalendarResult<T> = Result<T, CalendarError>;
// ============================================================================
// CALENDAR EVENT ERRORS
// ============================================================================
/// Errores que pueden ocurrir durante operaciones con entidades CalendarEvent
/// Errors that can occur during CalendarEvent entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarEventError {
/// Resumen/título de evento inválido
/// Invalid event summary/title
InvalidSummary(String),
/// Fechas de evento inválidas
/// Invalid event dates
InvalidDates(String),
/// Regla de recurrencia inválida
/// Invalid recurrence rule
InvalidRecurrence(String),
/// Datos iCalendar inválidos
/// Invalid iCalendar data
InvalidICalData(String),
}
@@ -187,7 +187,7 @@ impl Display for CalendarEventError {
impl Error for CalendarEventError {}
/// Alias de tipo para resultados de operaciones con entidades CalendarEvent
/// Type alias for CalendarEvent entity operation results
pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
// ============================================================================
@@ -216,10 +216,10 @@ mod tests {
#[test]
fn test_user_error_display() {
let err = UserError::InvalidUsername("".to_string());
assert_eq!(err.to_string(), "Username inválido: ");
assert_eq!(err.to_string(), "Invalid username: ");
let err = UserError::AuthenticationError("invalid credentials".to_string());
assert_eq!(err.to_string(), "Error en la autenticación: invalid credentials");
assert_eq!(err.to_string(), "Authentication error: invalid credentials");
}
#[test]
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::domain::services::path_service::StoragePath;
// Re-exportar errores de entidad desde el módulo centralizado
// Re-export entity errors from the centralized module
pub use super::entity_errors::{FileError, FileResult};
/**
@@ -366,6 +366,6 @@ mod tests {
assert!(renamed.is_ok());
let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "newname.txt");
assert_eq!(renamed.id(), "123"); // El ID no cambia
assert_eq!(renamed.id(), "123"); // The ID does not change
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::domain::services::path_service::StoragePath;
// Re-exportar errores de entidad desde el módulo centralizado
// Re-export entity errors from the centralized module
pub use super::entity_errors::{FolderError, FolderResult};
/// Represents a folder entity in the domain
+1 -1
View File
@@ -1,7 +1,7 @@
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
// Re-exportar errores de entidad desde el módulo centralizado
// Re-export entity errors from the centralized module
pub use super::entity_errors::ShareError;
#[derive(Debug, Clone, PartialEq)]
+12 -12
View File
@@ -1,7 +1,7 @@
use uuid::Uuid;
use chrono::{DateTime, Utc};
// Re-exportar errores de entidad desde el módulo centralizado
// Re-export entity errors from the centralized module
pub use super::entity_errors::{UserError, UserResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -57,22 +57,22 @@ impl User {
role: UserRole,
storage_quota_bytes: i64,
) -> UserResult<Self> {
// Validaciones
// Validations
if username.is_empty() || username.len() < 3 || username.len() > 32 {
return Err(UserError::InvalidUsername(format!(
"Username debe tener entre 3 y 32 caracteres"
"Username must be between 3 and 32 characters"
)));
}
if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError(format!(
"Email inválido"
"Invalid email"
)));
}
if password_hash.is_empty() {
return Err(UserError::InvalidPassword(format!(
"Password hash no puede estar vacío"
"Password hash cannot be empty"
)));
}
@@ -106,12 +106,12 @@ impl User {
) -> UserResult<Self> {
if username.is_empty() || username.len() < 3 || username.len() > 32 {
return Err(UserError::InvalidUsername(
"Username debe tener entre 3 y 32 caracteres".to_string(),
"Username must be between 3 and 32 characters".to_string(),
));
}
if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError(
"Email inválido".to_string(),
"Invalid email".to_string(),
));
}
let now = Utc::now();
@@ -132,7 +132,7 @@ impl User {
})
}
// Crear desde valores existentes (para reconstrucción desde BD)
// Create from existing values (for reconstruction from DB)
pub fn from_data(
id: String,
username: String,
@@ -263,26 +263,26 @@ impl User {
self.updated_at = Utc::now();
}
// Actualizar uso de almacenamiento
// Update storage usage
pub fn update_storage_used(&mut self, storage_used_bytes: i64) {
self.storage_used_bytes = storage_used_bytes;
self.updated_at = Utc::now();
}
// Registrar login
// Register login
pub fn register_login(&mut self) {
let now = Utc::now();
self.last_login_at = Some(now);
self.updated_at = now;
}
// Desactivar usuario
// Deactivate user
pub fn deactivate(&mut self) {
self.active = false;
self.updated_at = Utc::now();
}
// Activar usuario
// Activate user
pub fn activate(&mut self) {
self.active = true;
self.updated_at = Utc::now();
+34 -34
View File
@@ -1,35 +1,35 @@
//! Errores del dominio
//! Domain errors
//!
//! Este módulo contiene los tipos de error propios del dominio.
//! DomainError es el error base que se usa en toda la capa de dominio.
//! This module contains domain-specific error types.
//! DomainError is the base error used throughout the domain layer.
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::error::Error as StdError;
use thiserror::Error;
/// Tipo Result común para el dominio con DomainError como error estándar
/// Common Result type for the domain with DomainError as the standard error
pub type Result<T> = std::result::Result<T, DomainError>;
/// Tipos de errores del dominio
/// Domain error types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
/// Entidad no encontrada
/// Entity not found
NotFound,
/// Entidad ya existe
/// Entity already exists
AlreadyExists,
/// Entrada inválida o validación fallida
/// Invalid input or failed validation
InvalidInput,
/// Error de acceso o permisos
/// Access or permissions error
AccessDenied,
/// Tiempo de espera agotado
/// Timeout expired
Timeout,
/// Error interno del sistema
/// Internal system error
InternalError,
/// Funcionalidad no implementada
/// Functionality not implemented
NotImplemented,
/// Operación no soportada
/// Unsupported operation
UnsupportedOperation,
/// Error de base de datos
/// Database error
DatabaseError,
}
@@ -49,25 +49,25 @@ impl Display for ErrorKind {
}
}
/// Error base de dominio que proporciona contexto detallado
/// Base domain error that provides detailed context
#[derive(Error, Debug)]
#[error("{kind}: {message}")]
pub struct DomainError {
/// Tipo de error
/// Error type
pub kind: ErrorKind,
/// Tipo de entidad afectada (ej: "File", "Folder")
/// Affected entity type (e.g.: "File", "Folder")
pub entity_type: &'static str,
/// Identificador de la entidad si está disponible
/// Entity identifier if available
pub entity_id: Option<String>,
/// Mensaje descriptivo del error
/// Descriptive error message
pub message: String,
/// Error fuente (opcional)
/// Source error (optional)
#[source]
pub source: Option<Box<dyn StdError + Send + Sync>>,
}
impl DomainError {
/// Crea un nuevo error de dominio
/// Creates a new domain error
pub fn new<S: Into<String>>(
kind: ErrorKind,
entity_type: &'static str,
@@ -82,7 +82,7 @@ impl DomainError {
}
}
/// Crea un error de entidad no encontrada
/// Creates an entity not found error
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into();
Self {
@@ -94,7 +94,7 @@ impl DomainError {
}
}
/// Crea un error de entidad ya existente
/// Creates an entity already exists error
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into();
Self {
@@ -106,7 +106,7 @@ impl DomainError {
}
}
/// Crea un error para operaciones no soportadas
/// Creates an error for unsupported operations
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self::new(
ErrorKind::UnsupportedOperation,
@@ -115,7 +115,7 @@ impl DomainError {
)
}
/// Crea un error de tiempo agotado
/// Creates a timeout error
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::Timeout,
@@ -126,7 +126,7 @@ impl DomainError {
}
}
/// Crea un error interno
/// Creates an internal error
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::InternalError,
@@ -137,7 +137,7 @@ impl DomainError {
}
}
/// Crea un error de acceso denegado
/// Creates an access denied error
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::AccessDenied,
@@ -159,7 +159,7 @@ impl DomainError {
}
}
/// Crea un error de base de datos
/// Creates a database error
pub fn database_error<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::DatabaseError,
@@ -170,7 +170,7 @@ impl DomainError {
}
}
/// Crea un error de validación
/// Creates a validation error
pub fn validation_error<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::InvalidInput,
@@ -181,7 +181,7 @@ impl DomainError {
}
}
/// Crea un error de funcionalidad no implementada
/// Creates a not implemented error
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::NotImplemented,
@@ -192,20 +192,20 @@ impl DomainError {
}
}
/// Establece el ID de la entidad
/// Sets the entity ID
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
self.entity_id = Some(entity_id.into());
self
}
/// Establece el error fuente
/// Sets the source error
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
self.source = Some(Box::new(source));
self
}
}
/// Trait para añadir contexto a los errores
/// Trait for adding context to errors
pub trait ErrorContext<T, E> {
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
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 {
fn from(err: std::io::Error) -> Self {
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
//! debe cumplir. Este trait vive en el dominio porque File es una entidad core
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio,
//! siguiendo los principios de Clean/Hexagonal Architecture.
//! Defines the contract that any file storage implementation must fulfill.
//! This trait lives in the domain because File is a core entity of the system
//! and its persistence contracts belong to the domain layer, following
//! Clean/Hexagonal Architecture principles.
//!
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en
//! la capa de infraestructura.
//! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in
//! the infrastructure layer.
use std::path::PathBuf;
use std::pin::Pin;
@@ -20,31 +20,31 @@ use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
// ─────────────────────────────────────────────────────
// FileReadRepository — operaciones de lectura/consulta
// FileReadRepository — read/query operations
// ─────────────────────────────────────────────────────
/// Puerto del dominio para **lectura** de archivos.
/// Domain port for file **reading**.
///
/// Encapsula toda operación que consulta estado sin modificarlo:
/// obtener, listar, contenido, stream, mmap, rango, resolución de rutas.
/// Encapsulates every operation that queries state without modifying it:
/// get, list, content, stream, mmap, range, path resolution.
#[async_trait]
pub trait FileReadRepository: Send + Sync + 'static {
/// Obtiene un archivo por su ID.
/// Gets a file by its ID.
async fn get_file(&self, id: &str) -> Result<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>;
/// 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>;
/// Obtiene contenido como stream (ideal para archivos grandes).
/// Gets content as a stream (ideal for large files).
async fn get_file_stream(
&self,
id: &str,
) -> Result<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(
&self,
id: &str,
@@ -52,27 +52,27 @@ pub trait FileReadRepository: Send + Sync + 'static {
end: Option<u64>,
) -> 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>;
/// 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>;
/// 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>;
}
// ─────────────────────────────────────────────────────
// FileWriteRepository — operaciones de escritura/mutación
// FileWriteRepository — write/mutation operations
// ─────────────────────────────────────────────────────
/// Puerto del dominio para **escritura** de archivos.
/// Domain port for file **writing**.
///
/// Cubre: upload (buffered + streaming), move, delete, update,
/// y el registro diferido para write-behind cache.
/// Covers: upload (buffered + streaming), move, delete, update,
/// and deferred registration for write-behind cache.
#[async_trait]
pub trait FileWriteRepository: Send + Sync + 'static {
/// Guarda un nuevo archivo desde bytes.
/// Saves a new file from bytes.
async fn save_file(
&self,
name: String,
@@ -81,7 +81,7 @@ pub trait FileWriteRepository: Send + Sync + 'static {
content: Vec<u8>,
) -> 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(
&self,
name: String,
@@ -90,30 +90,30 @@ pub trait FileWriteRepository: Send + Sync + 'static {
stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
) -> Result<File, DomainError>;
/// Mueve un archivo a otra carpeta.
/// Moves a file to another folder.
async fn move_file(
&self,
file_id: &str,
target_folder_id: Option<String>,
) -> Result<File, DomainError>;
/// Renombra un archivo (same folder, different name).
/// Renames a file (same folder, different name).
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError>;
/// Elimina un archivo.
/// Deletes a file.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Actualiza el contenido de un archivo existente.
/// Updates the content of an existing file.
async fn update_file_content(&self, file_id: &str, content: Vec<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
/// escritura diferida que realizará el `WriteBehindCache`.
/// Returns `(File, PathBuf)` where `PathBuf` is the destination path for
/// the deferred write that the `WriteBehindCache` will perform.
async fn register_file_deferred(
&self,
name: String,
@@ -124,27 +124,27 @@ pub trait FileWriteRepository: Send + Sync + 'static {
// ── Trash operations ──
/// Mueve un archivo a la papelera
/// Moves a file to the trash
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
/// Restaura un archivo desde la papelera a su ubicación original
/// Restores a file from the trash to its original location
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
/// Elimina un archivo permanentemente (usado por la papelera)
/// Permanently deletes a file (used by the trash)
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
}
// ─────────────────────────────────────────────────────
// FileRepository — supertrait unificado
// FileRepository — unified supertrait
// ─────────────────────────────────────────────────────
/// Puerto unificado para persistencia de archivos.
/// Unified port for file persistence.
///
/// Es un supertrait de `FileReadRepository + FileWriteRepository`.
/// Cualquier tipo que implemente ambos ports obtiene `FileRepository`
/// automáticamente vía blanket impl.
/// It is a supertrait of `FileReadRepository + FileWriteRepository`.
/// Any type that implements both ports gets `FileRepository`
/// automatically via blanket impl.
pub trait FileRepository: FileReadRepository + FileWriteRepository {}
/// Blanket implementation: cualquier tipo que implemente ambos ports
/// es automáticamente un FileRepository.
/// Blanket implementation: any type that implements both ports
/// is automatically a FileRepository.
impl<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
//! debe cumplir. Este trait vive en el dominio porque Folder es una entidad core
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio,
//! siguiendo los principios de Clean/Hexagonal Architecture.
//! Defines the contract that any folder storage implementation
//! must fulfill. This trait lives in the domain because Folder is a core entity
//! of the system and its persistence contracts belong to the domain layer,
//! following the principles of Clean/Hexagonal Architecture.
//!
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en
//! la capa de infraestructura.
//! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in
//! the infrastructure layer.
use async_trait::async_trait;
@@ -14,25 +14,25 @@ use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
/// Puerto del dominio para persistencia de carpetas.
/// Domain port for folder persistence.
///
/// Define las operaciones CRUD y de gestión necesarias para
/// la entidad Folder en el sistema de almacenamiento.
/// Defines the CRUD and management operations required for
/// the Folder entity in the storage system.
#[async_trait]
pub trait FolderRepository: Send + Sync + 'static {
/// Crea una nueva carpeta
/// Creates a new folder
async fn create_folder(&self, name: String, parent_id: Option<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>;
/// 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>;
/// 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>;
/// Lista carpetas con paginación
/// Lists folders with pagination
async fn list_folders_paginated(
&self,
parent_id: Option<&str>,
@@ -41,29 +41,29 @@ pub trait FolderRepository: Send + Sync + 'static {
include_total: bool
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
/// Renombra una carpeta
/// Renames a folder
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>;
/// Elimina una carpeta
/// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
/// Verifica si existe una carpeta en la ruta dada
/// Checks if a folder exists at the given path
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<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>;
// ── Trash operations ──
/// Mueve una carpeta a la papelera
/// Moves a folder to the trash
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
/// Restaura una carpeta desde la papelera a su ubicación original
/// Restores a folder from the trash to its original location
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError>;
/// Elimina una carpeta permanentemente (usado por la papelera)
/// Permanently deletes a folder (used by the trash)
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
}
+11 -11
View File
@@ -4,19 +4,19 @@ use crate::common::errors::DomainError;
#[derive(Debug, thiserror::Error)]
pub enum SessionRepositoryError {
#[error("Sesión no encontrada: {0}")]
#[error("Session not found: {0}")]
NotFound(String),
#[error("Error de base de datos: {0}")]
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Error de tiempo de espera: {0}")]
#[error("Timeout error: {0}")]
Timeout(String),
}
pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
// Conversión de SessionRepositoryError a DomainError
// Conversion from SessionRepositoryError to DomainError
impl From<SessionRepositoryError> for DomainError {
fn from(err: SessionRepositoryError) -> Self {
match err {
@@ -35,24 +35,24 @@ impl From<SessionRepositoryError> for DomainError {
#[async_trait]
pub trait SessionRepository: Send + Sync + 'static {
/// Crea una nueva sesión
/// Creates a new session
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
/// Obtiene una sesión por ID
/// Gets a session by ID
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>;
/// 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>>;
/// Revoca una sesión específica
/// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>;
/// Revoca todas las sesiones de un usuario
/// Revokes all sessions for a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>;
/// Elimina sesiones expiradas
/// Deletes expired sessions
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)]
pub enum UserRepositoryError {
#[error("Usuario no encontrado: {0}")]
#[error("User not found: {0}")]
NotFound(String),
#[error("Usuario ya existe: {0}")]
#[error("User already exists: {0}")]
AlreadyExists(String),
#[error("Error de base de datos: {0}")]
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Error de validación: {0}")]
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Error de tiempo de espera: {0}")]
#[error("Timeout error: {0}")]
Timeout(String),
#[error("Operación no permitida: {0}")]
#[error("Operation not allowed: {0}")]
OperationNotAllowed(String),
}
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
// Conversión de UserRepositoryError a DomainError
// Conversion from UserRepositoryError to DomainError
impl From<UserRepositoryError> for DomainError {
fn from(err: UserRepositoryError) -> Self {
match err {
@@ -53,59 +53,59 @@ impl From<UserRepositoryError> for DomainError {
#[async_trait]
pub trait UserRepository: Send + Sync + 'static {
/// Crea un nuevo usuario
/// Creates a new user
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
/// Obtiene un usuario por ID
/// Gets a user by ID
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>;
/// Obtiene un usuario por correo electrónico
/// Gets a user by email
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>;
/// Actualiza solo el uso de almacenamiento de un usuario
/// Updates only a user's storage usage
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()>;
/// Actualiza la fecha de último inicio de sesión
/// Updates the last login date
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
/// Lista usuarios con paginación
/// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<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<()>;
/// Cambia la contraseña de un usuario
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()>;
/// Cambia el rol de un usuario
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
/// Lista usuarios por rol (admin o user)
/// Lists users by role (admin or user)
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
/// Elimina un usuario
/// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<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<()>;
/// Cuenta el número total de usuarios
/// Counts the total number of users
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>;
}
/// Estadísticas de almacenamiento agregadas
/// Aggregated storage statistics
#[derive(Debug, Clone)]
pub struct StorageStats {
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.
//! PathService (que implementa StoragePort y StorageMediator) fue movido a
//! infrastructure/services/path_service.rs porque tiene dependencias de sistema de archivos.
//! This module contains only the StoragePath Value Object which is part of the pure domain.
//! PathService (which implements StoragePort and StorageMediator) was moved to
//! infrastructure/services/path_service.rs because it has file system dependencies.
use std::path::PathBuf;
/// Representa una ruta de almacenamiento en el dominio (Value Object)
/// Represents a storage path in the domain (Value Object)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StoragePath {
segments: Vec<String>,
}
impl StoragePath {
/// Crea una nueva ruta de almacenamiento
/// Creates a new storage path
pub fn new(segments: Vec<String>) -> Self {
Self { segments }
}
/// Crea una ruta vacía (raíz)
/// Creates an empty path (root)
pub fn root() -> Self {
Self { segments: Vec::new() }
}
/// Crea una ruta a partir de una cadena con segmentos separados por /
/// Creates a path from a string with segments separated by /
pub fn from_string(path: &str) -> Self {
let segments = path
.split('/')
@@ -33,7 +33,7 @@ impl StoragePath {
Self { segments }
}
/// Crea una ruta a partir de un PathBuf
/// Creates a path from a PathBuf
pub fn from(path_buf: PathBuf) -> Self {
let segments = path_buf
.components()
@@ -45,19 +45,19 @@ impl StoragePath {
Self { segments }
}
/// Añade un segmento a la ruta
/// Appends a segment to the path
pub fn join(&self, segment: &str) -> Self {
let mut new_segments = self.segments.clone();
new_segments.push(segment.to_string());
Self { segments: new_segments }
}
/// Obtiene el nombre del archivo (último segmento)
/// Gets the file name (last segment)
pub fn file_name(&self) -> Option<String> {
self.segments.last().cloned()
}
/// Obtiene la ruta del directorio padre
/// Gets the parent directory path
pub fn parent(&self) -> Option<Self> {
if self.segments.is_empty() {
None
@@ -67,12 +67,12 @@ impl StoragePath {
}
}
/// Verifica si la ruta está vacía (es la raíz)
/// Checks if the path is empty (is the root)
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
/// Convierte la ruta a una cadena con formato "/segment1/segment2/..."
/// Converts the path to a string with format "/segment1/segment2/..."
pub fn to_string(&self) -> String {
if self.segments.is_empty() {
"/".to_string()
@@ -81,15 +81,15 @@ impl StoragePath {
}
}
/// Devuelve la representación de la ruta como cadena
/// Returns the path representation as a string
pub fn as_str(&self) -> &str {
// Nota: La implementación realmente debería almacenar la cadena,
// pero aquí hacemos una implementación temporal que siempre devuelve "/"
// Esto se usa solo para la implementación de get_folder_path_str
// Note: The implementation should really store the string,
// but here we do a temporary implementation that always returns "/"
// This is only used for the get_folder_path_str implementation
"/"
}
/// Obtiene los segmentos de la ruta
/// Gets the path segments
pub fn segments(&self) -> &[String] {
&self.segments
}
+6 -6
View File
@@ -17,21 +17,21 @@ pub async fn create_auth_services(
pool: Arc<PgPool>,
folder_service: Option<Arc<FolderService>>
) -> 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(
config.auth.jwt_secret.clone(),
config.auth.access_token_expiry_secs,
config.auth.refresh_token_expiry_secs,
));
// Crear servicio de hashing de contraseñas
// Create password hashing service
let password_hasher = Arc::new(Argon2PasswordHasher::new());
// Crear repositorios PostgreSQL
// Create PostgreSQL repositories
let user_repository = Arc::new(UserPgRepository::new(pool.clone()));
let session_repository = Arc::new(SessionPgRepository::new(pool.clone()));
// Crear servicio de aplicación de autenticación
// Create authentication application service
let mut auth_app_service = AuthApplicationService::new(
user_repository,
session_repository,
@@ -39,7 +39,7 @@ pub async fn create_auth_services(
token_service.clone(),
);
// Configurar servicio de carpetas si está disponible
// Configure folder service if available
if let Some(folder_svc) = folder_service {
auth_app_service = auth_app_service.with_folder_service(folder_svc);
}
@@ -57,7 +57,7 @@ pub async fn create_auth_services(
}
}
// Empaquetar servicio en Arc
// Package service in Arc
let auth_application_service = Arc::new(auth_app_service);
Ok(AuthServices {
+13 -13
View File
@@ -4,7 +4,7 @@ use std::time::Duration;
use crate::common::config::AppConfig;
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]@"));
// 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 {
attempt += 1;
tracing::info!("Intento de conexión a PostgreSQL #{}", attempt);
tracing::info!("PostgreSQL connection attempt #{}", attempt);
// Crear el pool de conexiones con las opciones de configuración
// Create the connection pool with configuration options
match PgPoolOptions::new()
.max_connections(config.database.max_connections)
.min_connections(config.database.min_connections)
@@ -25,10 +25,10 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
.connect(&config.database.connection_string)
.await {
Ok(pool) => {
// Verificar la conexión
// Verify the connection
match sqlx::query("SELECT 1").execute(&pool).await {
Ok(_) => {
tracing::info!("Conexión a PostgreSQL establecida correctamente");
tracing::info!("PostgreSQL connection established successfully");
// Verify if migrations have been applied
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
@@ -39,34 +39,34 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
Ok(row) => {
let tables_exist: bool = row.get(0);
if !tables_exist {
tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
tracing::warn!("Database tables do not exist. Please run migrations with: cargo run --bin migrate --features migrations");
}
},
Err(_) => {
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
tracing::warn!("Could not verify migration status. Please run migrations with: cargo run --bin migrate --features migrations");
}
}
return Ok(pool);
},
Err(e) => {
tracing::error!("Error al verificar conexión: {}", e);
tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
tracing::error!("Error verifying connection: {}", e);
tracing::warn!("The database appears to not be configured. Please run migrations with: cargo run --bin migrate --features migrations");
if attempt >= MAX_ATTEMPTS {
return Err(anyhow::anyhow!("Error al verificar la conexión a PostgreSQL: {}", e));
return Err(anyhow::anyhow!("Error verifying PostgreSQL connection: {}", e));
}
}
}
},
Err(e) => {
tracing::error!("Error al conectar a PostgreSQL: {}", e);
tracing::error!("Error connecting to PostgreSQL: {}", e);
if attempt >= MAX_ATTEMPTS {
return Err(anyhow::anyhow!("Error en la conexión a PostgreSQL: {}", e));
return Err(anyhow::anyhow!("Error in PostgreSQL connection: {}", e));
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
Err(anyhow::anyhow!("No se pudo establecer la conexión a PostgreSQL después de {} intentos", MAX_ATTEMPTS))
Err(anyhow::anyhow!("Could not establish PostgreSQL connection after {} attempts", MAX_ATTEMPTS))
}
@@ -10,11 +10,11 @@ use crate::common::errors::DomainError;
use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath;
/// Composite que envuelve `Arc<dyn FileReadPort>` + `Arc<dyn FileWritePort>`
/// y delega cada método al port correspondiente.
/// Composite that wraps `Arc<dyn FileReadPort>` + `Arc<dyn FileWritePort>`
/// and delegates each method to the corresponding port.
///
/// Gracias al blanket impl `impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}`
/// este tipo obtiene `FileStoragePort` automáticamente.
/// Thanks to the blanket impl `impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}`
/// this type automatically gets `FileStoragePort`.
pub struct CompositeFileRepository {
read: Arc<dyn FileReadPort>,
write: Arc<dyn FileWritePort>,
@@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService;
use crate::domain::services::path_service::StoragePath;
use crate::common::config::AppConfig;
/// Implementación de repositorio para operaciones de **lectura** de archivos.
/// Repository implementation for file **read** operations.
///
/// Implementa `FileReadPort`:
/// Implements `FileReadPort`:
/// get_file, list_files, get_file_content, get_file_stream,
/// get_file_range_stream, get_file_mmap, get_file_path, get_parent_folder_id.
pub struct FileFsReadRepository {
@@ -37,7 +37,7 @@ pub struct FileFsReadRepository {
}
impl FileFsReadRepository {
/// Constructor completo con todas las dependencias de infraestructura.
/// Full constructor with all infrastructure dependencies.
pub fn new(
root_path: PathBuf,
storage_mediator: Arc<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 {
Self {
root_path: PathBuf::from("./storage"),
@@ -75,7 +75,7 @@ impl FileFsReadRepository {
}
}
// ─── helpers internos ────────────────────────────────────
// ─── internal helpers ─────────────────────────────────────
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
self.path_service.resolve_path(storage_path)
@@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService;
use crate::domain::services::path_service::StoragePath;
use crate::common::config::AppConfig;
/// Implementación de repositorio para operaciones de **escritura** de archivos.
/// Repository implementation for file **write** operations.
///
/// Implementa `FileWritePort`:
/// Implements `FileWritePort`:
/// save_file, save_file_from_stream, move_file, delete_file,
/// update_file_content, register_file_deferred.
pub struct FileFsWriteRepository {
@@ -37,7 +37,7 @@ pub struct FileFsWriteRepository {
}
impl FileFsWriteRepository {
/// Constructor completo con todas las dependencias.
/// Full constructor with all dependencies.
pub fn new(
root_path: PathBuf,
storage_mediator: Arc<dyn StorageMediator>,
@@ -50,7 +50,7 @@ impl FileFsWriteRepository {
Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor }
}
/// Stub para pruebas (no realiza I/O real).
/// Stub for testing (does not perform real I/O).
pub fn default_stub() -> Self {
Self {
root_path: PathBuf::from("./storage"),
@@ -5,147 +5,147 @@ use tracing::{debug, error};
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
// Este archivo contiene la implementación de los métodos relacionados con la papelera
// para el repositorio de carpetas FolderFsRepository
// This file contains the implementation of trash-related methods
// for the FolderFsRepository folder repository
// Implementación de métodos de papelera para el repositorio de carpetas
// Implementation of trash methods for the folder repository
impl FolderFsRepository {
// Obtiene la ruta completa a la papelera
// Gets the full path to the trash directory
fn get_trash_dir(&self) -> PathBuf {
self.get_root_path().join(".trash").join("folders")
}
// Crea una ruta única en la papelera para la carpeta
// Creates a unique path in the trash for the folder
async fn create_trash_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<PathBuf> {
let trash_dir = self.get_trash_dir();
// Asegurarse que el directorio de la papelera existe
// Ensure the trash directory exists
if !trash_dir.exists() {
fs::create_dir_all(&trash_dir).await
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
}
// Crear una ruta única para la carpeta en la papelera
// Create a unique path for the folder in the trash
Ok(trash_dir.join(folder_id))
}
}
// Implementación de los métodos públicos del trait FolderRepository relacionados con la papelera
// Implementation of public FolderRepository trait methods related to trash
// Implementation of internal methods for trash functionality
// These will be enabled when the trash feature is re-enabled
impl FolderFsRepository {
/// Helper method that will be used for trash functionality
pub(crate) async fn _trash_move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> {
debug!("Moviendo carpeta a la papelera: {}", folder_id);
debug!("Moving folder to trash: {}", folder_id);
// Obtener la ruta física de la carpeta
// Get the physical path of the folder
let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => path,
Err(e) => {
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e);
error!("Error getting folder path {}: {:?}", folder_id, e);
return Err(e);
}
};
let folder_path_buf = PathBuf::from(folder_path.to_string());
// Verificamos que la carpeta existe
// Verify the folder exists
if !folder_path_buf.exists() {
return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id)));
}
// Crear directorio en la papelera
// Create directory in the trash
let trash_folder_path = self.create_trash_folder_path(folder_id).await?;
// Mover la carpeta físicamente a la papelera
// Physically move the folder to the trash
match fs::rename(&folder_path_buf, &trash_folder_path).await {
Ok(_) => {
debug!("Carpeta movida a papelera: {} -> {}", folder_path_buf.display(), trash_folder_path.display());
debug!("Folder moved to trash: {} -> {}", folder_path_buf.display(), trash_folder_path.display());
// Actualizar el mapeo al nuevo path en la papelera
// Update the mapping to the new path in the trash
if let Err(e) = self.update_mapped_folder_path(folder_id, &trash_folder_path).await {
error!("Error actualizando mapeo de carpeta en papelera: {}", e);
error!("Error updating folder mapping in trash: {}", e);
return Err(e);
}
Ok(())
},
Err(e) => {
error!("Error moviendo carpeta a papelera: {}", e);
error!("Error moving folder to trash: {}", e);
Err(FolderRepositoryError::StorageError(e.to_string()))
}
}
}
/// Restaura una carpeta desde la papelera a su ubicación original
/// Restores a folder from the trash to its original location
pub(crate) async fn _trash_restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> {
debug!("Restaurando carpeta {} a {}", folder_id, original_path);
debug!("Restoring folder {} to {}", folder_id, original_path);
// Obtener la ruta actual en la papelera
// Get the current path in the trash
let current_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path),
Err(e) => {
error!("Error obteniendo ruta actual de la carpeta {}: {:?}", folder_id, e);
error!("Error getting current folder path {}: {:?}", folder_id, e);
return Err(e);
}
};
// Convertir la ruta original a PathBuf
// Convert the original path to PathBuf
let original_path_buf = PathBuf::from(original_path);
// Asegurar que el directorio padre de destino existe
// Ensure the destination parent directory exists
if let Some(parent) = original_path_buf.parent() {
if !parent.exists() {
fs::create_dir_all(parent).await
.map_err(|e| {
error!("Error creando directorio padre para restauración: {}", e);
error!("Error creating parent directory for restoration: {}", e);
FolderRepositoryError::StorageError(e.to_string())
})?;
}
}
// Mover la carpeta de la papelera a su ubicación original
// Move the folder from the trash to its original location
match fs::rename(&current_path, &original_path_buf).await {
Ok(_) => {
debug!("Carpeta restaurada: {} -> {}", current_path.display(), original_path_buf.display());
debug!("Folder restored: {} -> {}", current_path.display(), original_path_buf.display());
// Actualizar el mapeo a la ruta original
// Update the mapping to the original path
if let Err(e) = self.update_mapped_folder_path(folder_id, &original_path_buf).await {
error!("Error actualizando mapeo de carpeta restaurada: {}", e);
error!("Error updating restored folder mapping: {}", e);
return Err(e);
}
Ok(())
},
Err(e) => {
error!("Error restaurando carpeta: {}", e);
error!("Error restoring folder: {}", e);
Err(FolderRepositoryError::StorageError(e.to_string()))
}
}
}
/// Elimina una carpeta permanentemente (usado por la papelera)
/// Permanently deletes a folder (used by the trash)
pub(crate) async fn _trash_delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
debug!("Eliminando carpeta permanentemente: {}", folder_id);
debug!("Permanently deleting folder: {}", folder_id);
// Similar a delete_folder pero sin validaciones adicionales
// Similar to delete_folder but without additional validations
let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path),
Err(e) => {
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e);
error!("Error getting folder path {}: {:?}", folder_id, e);
return Err(e);
}
};
// Eliminar la carpeta recursivamente
// Delete the folder recursively
if folder_path.exists() {
match fs::remove_dir_all(&folder_path).await {
Ok(_) => {
debug!("Carpeta eliminada permanentemente: {}", folder_path.display());
debug!("Folder permanently deleted: {}", folder_path.display());
},
Err(e) => {
error!("Error eliminando carpeta permanentemente: {}", e);
// No reportar error si la carpeta ya no existe
error!("Error permanently deleting folder: {}", e);
// Don't report error if the folder no longer exists
if e.kind() != std::io::ErrorKind::NotFound {
return Err(FolderRepositoryError::StorageError(e.to_string()));
}
@@ -153,16 +153,16 @@ impl FolderFsRepository {
}
}
// Eliminar el mapeo
// Remove the mapping
if let Err(e) = self.remove_mapped_folder_id(folder_id).await {
error!("Error eliminando mapeo de la carpeta: {}", e);
error!("Error removing folder mapping: {}", e);
return Err(e);
}
debug!("Carpeta eliminada permanentemente con éxito: {}", folder_id);
debug!("Folder permanently deleted successfully: {}", folder_id);
Ok(())
}
}
// Re-exportaciones necesarias para el compilador
// Re-exports needed by the compiler
use crate::infrastructure::repositories::repository_errors::FolderRepositoryError;
@@ -20,9 +20,9 @@ impl CalendarEventPgRepository {
#[async_trait]
impl CalendarEventRepository for CalendarEventPgRepository {
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
// Este método necesitaría una implementación completa que construya el CalendarEvent
// desde el resultado de la query, utilizando métodos del constructor
// Para esta demostración, vamos a retornar el mismo evento
// This method would need a full implementation that builds the CalendarEvent
// from the query result, using constructor methods
// For this demonstration, we return the same event
sqlx::query(
r#"
@@ -50,7 +50,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.await
.map_err(|e| DomainError::database_error(format!("Failed to create calendar event: {}", e)))?;
// Devolvemos el mismo evento en vez de un resultado
// We return the same event instead of a result
Ok(event)
}
@@ -86,8 +86,8 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.await
.map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?;
// En una implementación completa, recuperaríamos el evento actualizado
// Por simplicidad, devolvemos el mismo evento que recibimos
// In a full implementation, we would retrieve the updated event
// For simplicity, we return the same event we received
Ok(event)
}
@@ -176,9 +176,9 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?
.ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?;
// En una implementación real, construiríamos un objeto CalendarEvent completo
// Por simplicidad, creamos un objeto con valores predeterminados para
// demostrar el enfoque sin macros
// In a real implementation, we would build a complete CalendarEvent object
// For simplicity, we create an object with default values to
// demonstrate the approach without macros
let event = CalendarEvent::with_id(
row.get("id"),
@@ -32,14 +32,14 @@ impl CalendarRepository for CalendarPgRepository {
.bind(calendar.owner_id())
.bind(calendar.description())
.bind(calendar.color())
.bind(false) // is_public no existe como campo
.bind(false) // is_public doesn't exist as a field
.bind(calendar.created_at())
.bind(calendar.updated_at())
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to create calendar: {}", e)))?;
// Construir el objeto Calendar utilizando su constructor with_id
// Build the Calendar object using its with_id constructor
let result = Calendar::with_id(
row.get("id"),
row.get("name"),
@@ -66,14 +66,14 @@ impl CalendarRepository for CalendarPgRepository {
.bind(calendar.name())
.bind(calendar.description())
.bind(calendar.color())
.bind(false) // is_public no existe como campo
.bind(false) // is_public doesn't exist as a field
.bind(now)
.bind(calendar.id())
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to update calendar: {}", e)))?;
// Construir el objeto Calendar utilizando su constructor with_id
// Build the Calendar object using its with_id constructor
let result = Calendar::with_id(
row.get("id"),
row.get("name"),
@@ -8,7 +8,7 @@ use crate::application::dtos::favorites_dto::FavoriteItemDto;
use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind};
/// Implementación PostgreSQL del puerto de persistencia de favoritos.
/// PostgreSQL implementation of the favorites persistence port.
pub struct FavoritesPgRepository {
db_pool: Arc<PgPool>,
}
@@ -8,7 +8,7 @@ use crate::application::dtos::recent_dto::RecentItemDto;
use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind};
/// Implementación PostgreSQL del puerto de persistencia de elementos recientes.
/// PostgreSQL implementation of the recent items persistence port.
pub struct RecentItemsPgRepository {
db_pool: Arc<PgPool>,
}
@@ -10,7 +10,7 @@ use crate::application::ports::auth_ports::SessionStoragePort;
use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implementar From<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 {
fn from(err: sqlx::Error) -> Self {
SessionPgRepository::map_sqlx_error(err)
@@ -26,14 +26,14 @@ impl SessionPgRepository {
Self { pool }
}
// Método auxiliar para mapear errores SQL a errores de dominio
// Helper method to map SQL errors to domain errors
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
match err {
sqlx::Error::RowNotFound => {
SessionRepositoryError::NotFound("Sesión no encontrada".to_string())
SessionRepositoryError::NotFound("Session not found".to_string())
},
_ => SessionRepositoryError::DatabaseError(
format!("Error de base de datos: {}", err)
format!("Database error: {}", err)
),
}
}
@@ -41,9 +41,9 @@ impl SessionPgRepository {
#[async_trait]
impl SessionRepository for SessionPgRepository {
/// Crea una nueva sesión utilizando una transacción
/// Creates a new session using a transaction
async fn create_session(&self, session: Session) -> SessionRepositoryResult<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();
with_transaction(
@@ -51,7 +51,7 @@ impl SessionRepository for SessionPgRepository {
"create_session",
|tx| {
Box::pin(async move {
// Insertar la sesión
// Insert the session
sqlx::query(
r#"
INSERT INTO auth.sessions (
@@ -74,8 +74,8 @@ impl SessionRepository for SessionPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
// Opcionalmente, actualizar el último login del usuario
// dentro de la misma transacción
// Optionally, update the user's last login
// within the same transaction
sqlx::query(
r#"
UPDATE auth.users
@@ -87,12 +87,12 @@ impl SessionRepository for SessionPgRepository {
.execute(&mut **tx)
.await
.map_err(|e| {
// Convertimos el error pero sin interrumpir la creación
// de la sesión si falla la actualización
tracing::warn!("No se pudo actualizar last_login_at para usuario {}: {}",
// Convert the error but without interrupting session
// creation if the update fails
tracing::warn!("Could not update last_login_at for user {}: {}",
session_clone.user_id(), e);
SessionRepositoryError::DatabaseError(format!(
"Sesión creada pero no se pudo actualizar last_login_at: {}", e
"Session created but could not update last_login_at: {}", e
))
})?;
@@ -104,7 +104,7 @@ impl SessionRepository for SessionPgRepository {
Ok(session)
}
/// Obtiene una sesión por ID
/// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> {
let row = sqlx::query(
r#"
@@ -132,7 +132,7 @@ impl SessionRepository for SessionPgRepository {
))
}
/// Obtiene una sesión por token de actualización
/// Gets a session by refresh token
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session> {
let row = sqlx::query(
r#"
@@ -160,7 +160,7 @@ impl SessionRepository for SessionPgRepository {
))
}
/// Obtiene todas las sesiones de un usuario
/// Gets all sessions for a user
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>> {
let rows = sqlx::query(
r#"
@@ -195,16 +195,16 @@ impl SessionRepository for SessionPgRepository {
Ok(sessions)
}
/// Revoca una sesión específica utilizando una transacción
/// Revokes a specific session using a transaction
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
let id = session_id.to_string(); // Clone para uso en closure
let id = session_id.to_string(); // Clone for use in closure
with_transaction(
&self.pool,
"revoke_session",
|tx| {
Box::pin(async move {
// Revocar la sesión
// Revoke the session
let result = sqlx::query(
r#"
UPDATE auth.sessions
@@ -218,14 +218,14 @@ impl SessionRepository for SessionPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
// Si encontramos la sesión, podemos registrar un evento de seguridad
// If we found the session, we can log a security event
if let Some(row) = result {
let user_id: String = row.try_get("user_id").unwrap_or_default();
// Registrar evento de seguridad (en una tabla de seguridad)
// Esto es opcional pero muestra cómo se puede realizar operaciones
// adicionales en la misma transacción
tracing::info!("Sesión con ID {} del usuario {} revocada", id, user_id);
// Log security event (in a security table)
// This is optional but shows how additional operations
// can be performed in the same transaction
tracing::info!("Session with ID {} for user {} revoked", id, user_id);
}
Ok(())
@@ -234,16 +234,16 @@ impl SessionRepository for SessionPgRepository {
).await
}
/// Revoca todas las sesiones de un usuario utilizando una transacción
/// Revokes all sessions for a user using a transaction
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<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(
&self.pool,
"revoke_all_user_sessions",
|tx| {
Box::pin(async move {
// Revocar todas las sesiones del usuario
// Revoke all sessions for the user
let result = sqlx::query(
r#"
UPDATE auth.sessions
@@ -258,9 +258,9 @@ impl SessionRepository for SessionPgRepository {
let affected = result.rows_affected();
// Registrar evento de seguridad
// Log security event
if affected > 0 {
tracing::info!("Revocadas {} sesiones del usuario {}", affected, user_id_clone);
tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone);
}
Ok(affected)
@@ -269,7 +269,7 @@ impl SessionRepository for SessionPgRepository {
).await
}
/// Elimina sesiones expiradas
/// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64> {
let now = Utc::now();
@@ -288,7 +288,7 @@ impl SessionRepository for SessionPgRepository {
}
}
// Implementación del puerto de almacenamiento para la capa de aplicación
// Implementation of the storage port for the application layer
#[async_trait]
impl SessionStoragePort for SessionPgRepository {
async fn create_session(&self, session: Session) -> Result<Session, DomainError> {
@@ -9,7 +9,7 @@ use crate::application::ports::auth_ports::UserStoragePort;
use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implementar From<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 {
fn from(err: sqlx::Error) -> Self {
UserPgRepository::map_sqlx_error(err)
@@ -25,26 +25,26 @@ impl UserPgRepository {
Self { pool }
}
// Método auxiliar para mapear errores SQL a errores de dominio
// Helper method to map SQL errors to domain errors
pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
match err {
sqlx::Error::RowNotFound => {
UserRepositoryError::NotFound("Usuario no encontrado".to_string())
UserRepositoryError::NotFound("User not found".to_string())
},
sqlx::Error::Database(db_err) => {
if db_err.code().map_or(false, |code| code == "23505") {
// Código para violación de unicidad en PostgreSQL
// PostgreSQL uniqueness violation code
UserRepositoryError::AlreadyExists(
"Usuario o email ya existe".to_string()
"User or email already exists".to_string()
)
} else {
UserRepositoryError::DatabaseError(
format!("Error de base de datos: {}", db_err)
format!("Database error: {}", db_err)
)
}
},
_ => UserRepositoryError::DatabaseError(
format!("Error de base de datos: {}", err)
format!("Database error: {}", err)
),
}
}
@@ -52,7 +52,7 @@ impl UserPgRepository {
#[async_trait]
impl UserRepository for UserPgRepository {
/// Crea un nuevo usuario utilizando una transacción
/// Creates a new user using a transaction
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
// Creamos una copia del usuario para el closure
let user_clone = user.clone();
@@ -61,14 +61,14 @@ impl UserRepository for UserPgRepository {
&self.pool,
"create_user",
|tx| {
// Necesitamos mover el closure a un BoxFuture para devolver dentro
// de la llamada with_transaction
// We need to move the closure into a BoxFuture to return inside
// the with_transaction call
Box::pin(async move {
// Usamos los getters para extraer los valores
// Convertimos user.role() a string para pasarlo como texto plano
// Use getters to extract the values
// Convert user.role() to string to pass it as plain text
let role_str = user_clone.role().to_string();
// Modificar el SQL para hacer un cast explícito al tipo auth.userrole
// Modify the SQL to do an explicit cast to the auth.userrole type
let _result = sqlx::query(
r#"
INSERT INTO auth.users (
@@ -87,7 +87,7 @@ impl UserRepository for UserPgRepository {
.bind(user_clone.username())
.bind(user_clone.email())
.bind(user_clone.password_hash())
.bind(&role_str) // Convertir a string pero con cast explícito en SQL
.bind(&role_str) // Convert to string but with explicit cast in SQL
.bind(user_clone.storage_quota_bytes())
.bind(user_clone.storage_used_bytes())
.bind(user_clone.created_at())
@@ -100,18 +100,18 @@ impl UserRepository for UserPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
// Podríamos realizar operaciones adicionales aquí,
// como configurar permisos, roles, etc.
// We could perform additional operations here,
// such as configuring permissions, roles, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
}
).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> {
let row = sqlx::query(
r#"
@@ -153,7 +153,7 @@ impl UserRepository for UserPgRepository {
))
}
/// Obtiene un usuario por nombre de usuario
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
@@ -195,7 +195,7 @@ impl UserRepository for UserPgRepository {
))
}
/// Obtiene un usuario por correo electrónico
/// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
@@ -237,9 +237,9 @@ impl UserRepository for UserPgRepository {
))
}
/// Actualiza un usuario existente utilizando una transacción
/// Updates an existing user using a transaction
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
// Creamos una copia del usuario para el closure
// Create a copy of the user for the closure
let user_clone = user.clone();
with_transaction(
@@ -247,7 +247,7 @@ impl UserRepository for UserPgRepository {
"update_user",
|tx| {
Box::pin(async move {
// Actualizar el usuario
// Update the user
sqlx::query(
r#"
UPDATE auth.users
@@ -278,8 +278,8 @@ impl UserRepository for UserPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
// Podríamos realizar operaciones adicionales aquí dentro
// de la misma transacción, como actualizar permisos, etc.
// We could perform additional operations here inside
// the same transaction, such as updating permissions, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
@@ -289,7 +289,7 @@ impl UserRepository for UserPgRepository {
Ok(user)
}
/// Actualiza solo el uso de almacenamiento de un usuario
/// Updates only the storage usage of a user
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> {
sqlx::query(
r#"
@@ -309,7 +309,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Actualiza la fecha de último inicio de sesión
/// Updates the last login date
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> {
sqlx::query(
r#"
@@ -328,7 +328,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Lista usuarios con paginación
/// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query(
r#"
@@ -378,7 +378,7 @@ impl UserRepository for UserPgRepository {
Ok(users)
}
/// Activa o desactiva un usuario
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> {
sqlx::query(
r#"
@@ -398,7 +398,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Cambia la contraseña de un usuario
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> {
sqlx::query(
r#"
@@ -418,9 +418,9 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Cambia el rol de un usuario
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> {
// Convertir el rol a string para el binding
// Convert the role to string for the binding
let role_str = role.to_string();
sqlx::query(
@@ -441,7 +441,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Lista usuarios por rol
/// Lists users by role
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query(
r#"
@@ -490,7 +490,7 @@ impl UserRepository for UserPgRepository {
Ok(users)
}
/// Elimina un usuario
/// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
sqlx::query(
r#"
@@ -548,7 +548,7 @@ impl UserRepository for UserPgRepository {
))
}
/// Actualiza la cuota de almacenamiento de un usuario
/// Updates a user's storage quota
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> {
sqlx::query(
r#"
@@ -568,7 +568,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Cuenta el número total de usuarios
/// Counts the total number of users
async fn count_users(&self) -> UserRepositoryResult<i64> {
let row = sqlx::query(
"SELECT COUNT(*) as count FROM auth.users"
@@ -581,7 +581,7 @@ impl UserRepository for UserPgRepository {
Ok(count)
}
/// Obtiene estadísticas de almacenamiento agregadas
/// Gets aggregated storage statistics
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats> {
let row = sqlx::query(
r#"
@@ -610,7 +610,7 @@ impl UserRepository for UserPgRepository {
}
}
// Implementación del puerto de almacenamiento para la capa de aplicación
// Storage port implementation for the application layer
#[async_trait]
impl UserStoragePort for UserPgRepository {
async fn create_user(&self, user: User) -> Result<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)]
struct ShareRecord {
id: String,
@@ -38,12 +38,12 @@ impl ShareFsRepository {
Self { config }
}
/// Obtiene la ruta del archivo JSON donde se almacenan los enlaces compartidos
/// Gets the path to the JSON file where shared links are stored
fn get_shares_path(&self) -> String {
format!("{}/shares.json", self.config.storage_path.display())
}
/// Lee todos los enlaces compartidos del archivo JSON
/// Reads all shared links from the JSON file
async fn read_shares(&self) -> Result<Vec<ShareRecord>, io::Error> {
let path = self.get_shares_path();
let path = Path::new(&path);
@@ -58,12 +58,12 @@ impl ShareFsRepository {
Ok(shares)
}
/// Guarda todos los enlaces compartidos en el archivo JSON
/// Saves all shared links to the JSON file
async fn write_shares(&self, shares: &[ShareRecord]) -> Result<(), io::Error> {
let path = self.get_shares_path();
let json = serde_json::to_string_pretty(shares)?;
// Asegúrate de que el directorio existe
// Make sure the directory exists
let dir = Path::new(&path).parent().unwrap();
if !dir.exists() {
fs::create_dir_all(dir).await?
@@ -72,7 +72,7 @@ impl ShareFsRepository {
fs::write(path, json).await
}
/// Convierte un registro del sistema de archivos a una entidad de dominio
/// Converts a file system record to a domain entity
fn to_entity(&self, record: &ShareRecord) -> Share {
let item_type = ShareItemType::try_from(record.item_type.as_str())
.unwrap_or(ShareItemType::File);
@@ -97,7 +97,7 @@ impl ShareFsRepository {
)
}
/// Convierte una entidad de dominio a un registro para el sistema de archivos
/// Converts a domain entity to a file system record
fn to_record(&self, share: &Share) -> ShareRecord {
ShareRecord {
id: share.id().to_string(),
@@ -122,16 +122,16 @@ impl ShareStoragePort for ShareFsRepository {
let mut shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Verifica si el enlace ya existe
// Check if the link already exists
let existing_index = shares.iter().position(|s| s.id == share.id());
let record = self.to_record(share);
if let Some(index) = existing_index {
// Actualización
// Update
shares[index] = record;
} else {
// Inserción
// Insert
shares.push(record);
}
@@ -190,16 +190,16 @@ impl ShareStoragePort for ShareFsRepository {
let mut shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Busca el índice del enlace a actualizar
// Find the index of the link to update
let index = shares.iter().position(|s| s.id == share.id())
.ok_or_else(|| {
DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id()))
})?;
// Actualiza el registro
// Update the record
shares[index] = self.to_record(share);
// Guarda los cambios
// Save changes
self.write_shares(&shares).await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
@@ -210,16 +210,16 @@ impl ShareStoragePort for ShareFsRepository {
let mut shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Encuentra el índice del enlace a eliminar
// Find the index of the link to delete
let initial_len = shares.len();
shares.retain(|s| s.id != id);
// Si no se eliminó ningún enlace, significa que no existía
// If no link was deleted, it means it didn't exist
if shares.len() == initial_len {
return Err(DomainError::not_found("Share", format!("Share with ID {} not found for deletion", id)));
}
// Guarda los cambios
// Save changes
self.write_shares(&shares).await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
@@ -230,15 +230,15 @@ impl ShareStoragePort for ShareFsRepository {
let shares = self.read_shares().await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Filtra los enlaces del usuario
// Filter the user's links
let user_shares: Vec<ShareRecord> = shares.into_iter()
.filter(|s| s.created_by == user_id)
.collect();
// Calcula el total
// Calculate the total
let total = user_shares.len();
// Aplica la paginación
// Apply pagination
let paginated: Vec<Share> = user_shares.iter()
.skip(offset)
.take(limit)
@@ -12,7 +12,7 @@ use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::ports::outbound::IdMappingPort;
/// Estructura para almacenar elementos en la papelera en formato JSON
/// Structure for storing trash items in JSON format
#[derive(Debug, Serialize, Deserialize)]
struct TrashedItemEntry {
id: String,
@@ -25,7 +25,7 @@ struct TrashedItemEntry {
deletion_date: String,
}
/// Implementación del repositorio de papelera usando el sistema de archivos
/// Trash repository implementation using the file system
pub struct TrashFsRepository {
trash_dir: PathBuf,
trash_index_path: PathBuf,
@@ -45,7 +45,7 @@ impl TrashFsRepository {
}
}
/// Asegura que existe el directorio de papelera
/// Ensures the trash directory exists
async fn ensure_trash_dir(&self) -> Result<()> {
debug!("Checking if trash directory exists: {}", self.trash_dir.display());
if !self.trash_dir.exists() {
@@ -105,7 +105,7 @@ impl TrashFsRepository {
Ok(())
}
/// Obtiene todas las entradas del índice de papelera
/// Gets all entries from the trash index
async fn get_trash_entries(&self) -> Result<Vec<TrashedItemEntry>> {
self.ensure_trash_dir().await?;
@@ -134,7 +134,7 @@ impl TrashFsRepository {
Ok(entries)
}
/// Guarda todas las entradas en el índice de papelera
/// Saves all entries to the trash index
async fn save_trash_entries(&self, entries: Vec<TrashedItemEntry>) -> Result<()> {
self.ensure_trash_dir().await?;
@@ -155,7 +155,7 @@ impl TrashFsRepository {
Ok(())
}
/// Convierte una entrada JSON a entidad TrashedItem
/// Converts a JSON entry to a TrashedItem entity
fn entry_to_trashed_item(&self, entry: TrashedItemEntry) -> Result<TrashedItem> {
let item_type = match entry.item_type.as_str() {
"file" => TrashedItemType::File,
@@ -206,7 +206,7 @@ impl TrashFsRepository {
))
}
/// Convierte una entidad TrashedItem a entrada JSON
/// Converts a TrashedItem entity to a JSON entry
fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry {
TrashedItemEntry {
id: item.id().to_string(),
@@ -228,9 +228,9 @@ impl TrashFsRepository {
impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> {
debug!("Añadiendo elemento a la papelera: id={}, user={}", item.id(), item.user_id());
debug!("Adding item to trash: id={}, user={}", item.id(), item.user_id());
// Aseguramos que existe el directorio de la papelera para este usuario
// Ensure the trash directory exists for this user
let user_trash_dir = self.trash_dir.join("files").join(item.user_id().to_string());
debug!("User trash directory path: {}", user_trash_dir.display());
@@ -268,7 +268,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &Uuid) -> Result<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?;
@@ -290,7 +290,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
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?;
@@ -311,7 +311,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
debug!("Restaurando elemento de la papelera: id={}, user={}", id, user_id);
debug!("Restoring item from trash: id={}, user={}", id, user_id);
let mut entries = self.get_trash_entries().await?;
@@ -333,16 +333,16 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
debug!("Eliminando permanentemente elemento de la papelera: id={}, user={}", id, user_id);
debug!("Permanently deleting item from trash: id={}, user={}", id, user_id);
// Simplemente eliminamos la entrada del índice
// Los archivos físicos se eliminarán a través del repositorio correspondiente
// Simply remove the entry from the index
// Physical files will be deleted through the corresponding repository
self.restore_from_trash(id, user_id).await
}
#[instrument(skip(self))]
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
debug!("Limpiando papelera para usuario: {}", user_id);
debug!("Clearing trash for user: {}", user_id);
let mut entries = self.get_trash_entries().await?;
let user_id_str = user_id.to_string();
@@ -355,7 +355,7 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
debug!("Buscando elementos de papelera expirados");
debug!("Looking for expired trash items");
let entries = self.get_trash_entries().await?;
let now = Utc::now();
+109 -109
View File
@@ -5,71 +5,71 @@ use tokio::sync::{Mutex, Semaphore};
use std::time::{Duration, Instant};
use tracing::debug;
/// Tamaño por defecto de los buffers en el pool
/// Default buffer size in the pool
pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB
/// Número máximo por defecto de buffers en el pool
/// Default maximum number of buffers in the pool
pub const DEFAULT_MAX_BUFFERS: usize = 100;
/// Tiempo de vida por defecto de un buffer inactivo (en segundos)
/// Default time-to-live for an inactive buffer (in seconds)
pub const DEFAULT_BUFFER_TTL: u64 = 60;
/// Buffer pooling para optimizar operaciones de lectura/escritura
/// Buffer pooling to optimize read/write operations
pub struct BufferPool {
/// Pool de buffers disponibles
/// Pool of available buffers
pool: Mutex<VecDeque<PooledBuffer>>,
/// Semáforo para limitar el número máximo de buffers
/// Semaphore to limit the maximum number of buffers
limit: Semaphore,
/// Tamaño de los buffers en el pool
/// Size of buffers in the pool
buffer_size: usize,
/// Estadísticas del pool
/// Pool statistics
stats: Mutex<BufferPoolStats>,
/// Tiempo de vida de un buffer inactivo
/// Time-to-live for an inactive buffer
buffer_ttl: Duration,
}
/// Estructura para tracking de estadísticas del pool
/// Structure for tracking pool statistics
#[derive(Debug, Clone, Default)]
pub struct BufferPoolStats {
/// Número total de operaciones de get
/// Total number of get operations
pub gets: usize,
/// Número de hits del pool (reutilización exitosa)
/// Number of pool hits (successful reuse)
pub hits: usize,
/// Número de misses (creación de nuevo buffer)
/// Number of misses (new buffer creation)
pub misses: usize,
/// Número de retornos al pool
/// Number of returns to the pool
pub returns: usize,
/// Número de eviction por TTL
/// Number of TTL evictions
pub evictions: usize,
/// Número máximo de buffers alcanzado
/// Maximum number of buffers reached
pub max_buffers_reached: usize,
/// Esperas por semáforo
/// Semaphore waits
pub waits: usize,
}
/// Buffer del pool con metadatos para gestión
/// Pool buffer with management metadata
struct PooledBuffer {
/// Buffer real de bytes
/// Actual byte buffer
buffer: Vec<u8>,
/// Timestamp de cuándo se añadió/retornó al pool
/// Timestamp of when it was added/returned to the pool
last_used: Instant,
}
/// Buffer prestado del pool con cleanup automático
/// Borrowed buffer from the pool with automatic cleanup
#[derive(Clone)]
pub struct BorrowedBuffer {
/// Buffer actual
/// Current buffer
buffer: Vec<u8>,
/// Tamaño real utilizado del buffer
/// Actual used size of the buffer
used_size: usize,
/// Referencia al pool para retornar
/// Reference to the pool for returning
pool: Arc<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,
}
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> {
Arc::new(Self {
pool: Mutex::new(VecDeque::with_capacity(max_buffers)),
@@ -80,7 +80,7 @@ impl BufferPool {
})
}
/// Crea un pool con configuración por defecto
/// Creates a pool with default configuration
pub fn default() -> Arc<Self> {
Self::new(
DEFAULT_BUFFER_SIZE,
@@ -89,25 +89,25 @@ impl BufferPool {
)
}
/// Obtiene un buffer del pool o crea uno nuevo si es necesario.
/// Gets a buffer from the pool or creates a new one if needed.
/// This version takes an Arc<Self> to ensure the BorrowedBuffer keeps a proper
/// reference to the shared pool (not a clone).
#[allow(unused_variables)]
pub async fn get_buffer(self: &Arc<Self>) -> BorrowedBuffer {
// Incrementar contador de gets
// Increment get counter
{
let mut stats = self.stats.lock().await;
stats.gets += 1;
}
// Control de concurrencia
// Concurrency control
// Acquire a semaphore permit. If none available, wait.
// We forget() the permit so it doesn't auto-release on drop.
// Instead, the permit is manually released in return_buffer/Drop via add_permits(1).
match self.limit.try_acquire() {
Ok(permit) => permit.forget(),
Err(_) => {
// No hay permisos disponibles, esperamos
// No permits available, waiting
{
let mut stats = self.stats.lock().await;
stats.waits += 1;
@@ -121,15 +121,15 @@ impl BufferPool {
}
};
// Intentar obtener un buffer existente del pool
// Try to get an existing buffer from the pool
let mut pool_locked = self.pool.lock().await;
let pool_arc = Arc::clone(self);
if let Some(mut pooled_buffer) = pool_locked.pop_front() {
// Verificar si el buffer ha expirado
// Check if the buffer has expired
if pooled_buffer.last_used.elapsed() > self.buffer_ttl {
// Buffer expirado, descartamos y creamos uno nuevo
// Expired buffer, discard and create a new one
let mut stats = self.stats.lock().await;
stats.evictions += 1;
stats.misses += 1;
@@ -137,8 +137,8 @@ impl BufferPool {
debug!("Buffer pool: evicted expired buffer");
// Crear nuevo buffer (reutilizando el permiso)
drop(pool_locked); // Liberar el lock antes de retornar
// Create new buffer (reusing the permit)
drop(pool_locked); // Release the lock before returning
BorrowedBuffer {
buffer: vec![0; self.buffer_size],
@@ -147,15 +147,15 @@ impl BufferPool {
return_to_pool: true,
}
} else {
// Buffer válido, lo reutilizamos
// Valid buffer, reuse it
let mut stats = self.stats.lock().await;
stats.hits += 1;
drop(stats);
// Liberar el lock antes de retornar
// Release the lock before returning
drop(pool_locked);
// Limpiar buffer por seguridad
// Clear buffer for security
pooled_buffer.buffer.fill(0);
BorrowedBuffer {
@@ -166,12 +166,12 @@ impl BufferPool {
}
}
} else {
// No hay buffers disponibles, creamos uno nuevo
// No buffers available, create a new one
let mut stats = self.stats.lock().await;
stats.misses += 1;
drop(stats);
// Liberar el lock antes de retornar
// Release the lock before returning
drop(pool_locked);
debug!("Buffer pool: creating new buffer");
@@ -185,9 +185,9 @@ impl BufferPool {
}
}
/// Retorna un buffer al pool
/// Returns a buffer to the pool
async fn return_buffer(&self, mut buffer: Vec<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 {
debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})",
buffer.capacity(), self.buffer_size);
@@ -196,10 +196,10 @@ impl BufferPool {
return;
}
// Resize para asegurar capacidad correcta
// Resize to ensure correct capacity
buffer.resize(self.buffer_size, 0);
// Añadir al pool
// Add to the pool
let mut pool_locked = self.pool.lock().await;
pool_locked.push_back(PooledBuffer {
@@ -207,7 +207,7 @@ impl BufferPool {
last_used: Instant::now(),
});
// Actualizar estadísticas
// Update statistics
let mut stats = self.stats.lock().await;
stats.returns += 1;
@@ -217,24 +217,24 @@ impl BufferPool {
self.limit.add_permits(1);
}
/// Limpia buffers expirados del pool
/// Cleans expired buffers from the pool
pub async fn clean_expired_buffers(&self) {
let _now = Instant::now();
let mut pool_locked = self.pool.lock().await;
// Contar expirados
// Count expired
let count_before = pool_locked.len();
// Filtrar manteniendo solo los no expirados
// Filter keeping only non-expired
pool_locked.retain(|buffer| {
buffer.last_used.elapsed() <= self.buffer_ttl
});
// Contar cuántos se eliminaron
// Count how many were removed
let removed = count_before - pool_locked.len();
if removed > 0 {
// Actualizar estadísticas
// Update statistics
let mut stats = self.stats.lock().await;
stats.evictions += removed;
@@ -242,21 +242,21 @@ impl BufferPool {
}
}
/// Obtiene estadísticas actuales del pool
/// Gets current pool statistics
pub async fn get_stats(&self) -> BufferPoolStats {
self.stats.lock().await.clone()
}
/// Inicia la tarea periódica de limpieza
/// Starts the periodic cleanup task
pub fn start_cleaner(pool: Arc<Self>) {
tokio::spawn(async move {
let interval = Duration::from_secs(30); // Limpiar cada 30 segundos
let interval = Duration::from_secs(30); // Clean every 30 seconds
loop {
tokio::time::sleep(interval).await;
pool.clean_expired_buffers().await;
// Loguear estadísticas periódicamente
// Log statistics periodically
let stats = pool.get_stats().await;
debug!("Buffer pool stats: gets={}, hits={}, misses={}, hit_ratio={:.2}%, returns={}, \
evictions={}, max_reached={}, waits={}",
@@ -286,31 +286,31 @@ impl Clone for BufferPool {
}
impl BorrowedBuffer {
/// Accede al buffer interno
/// Accesses the internal buffer
pub fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.buffer
}
/// Obtiene una referencia a los datos utilizados
/// Gets a reference to the used data
pub fn as_slice(&self) -> &[u8] {
&self.buffer[..self.used_size]
}
/// Establece cuántos bytes se utilizaron realmente
/// Sets how many bytes were actually used
pub fn set_used(&mut self, size: usize) {
self.used_size = min(size, self.buffer.len());
}
/// Convierte en un Vec<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> {
// Marcar para no devolver al pool
// Mark to not return to pool
self.return_to_pool = false;
// Crear un nuevo vector solo con los datos utilizados
// Create a new vector with only the used data
self.buffer[..self.used_size].to_vec()
}
/// Copia datos a este buffer y actualiza el tamaño usado
/// Copies data to this buffer and updates the used size
pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
let copy_size = min(data.len(), self.buffer.len());
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
@@ -318,32 +318,32 @@ impl BorrowedBuffer {
copy_size
}
/// Impide que el buffer se devuelva al pool al destruirse
/// Prevents the buffer from being returned to the pool on destruction
pub fn do_not_return(mut self) -> Self {
self.return_to_pool = false;
self
}
/// Obtiene el tamaño total del buffer
/// Gets the total buffer size
pub fn capacity(&self) -> usize {
self.buffer.len()
}
/// Obtiene el tamaño usado del buffer
/// Gets the used buffer size
pub fn used_size(&self) -> usize {
self.used_size
}
}
// Cuando se hace drop de un BorrowedBuffer, lo devuelve al pool
// When a BorrowedBuffer is dropped, it is returned to the pool
impl Drop for BorrowedBuffer {
fn drop(&mut self) {
if self.return_to_pool {
// Tomar posesión del buffer y crear un clone del pool
// Take ownership of the buffer and create a clone of the pool
let buffer = std::mem::take(&mut self.buffer);
let pool = self.pool.clone();
// Spawn del return para que el drop no bloquee
// Spawn the return so that drop doesn't block
// return_buffer will release the semaphore permit
tokio::spawn(async move {
pool.return_buffer(buffer).await;
@@ -361,39 +361,39 @@ mod tests {
#[tokio::test]
async fn test_buffer_pooling() {
// Crear pool pequeño para testing
// Create small pool for testing
let pool = BufferPool::new(1024, 5, 60);
// Obtener un buffer
// Get a buffer
let mut buffer1 = pool.get_buffer().await;
buffer1.copy_from_slice(b"test data");
assert_eq!(buffer1.as_slice(), b"test data");
// Obtener otro buffer
// Get another buffer
let buffer2 = pool.get_buffer().await;
// Verificar stats
// Verify stats
let stats = pool.get_stats().await;
assert_eq!(stats.gets, 2);
assert_eq!(stats.hits, 0); // sin hits todavía
assert_eq!(stats.misses, 2); // todos son misses
assert_eq!(stats.hits, 0); // no hits yet
assert_eq!(stats.misses, 2); // all are misses
// Devolver buffer1 al pool (implícitamente por drop)
// Return buffer1 to pool (implicitly via drop)
drop(buffer1);
// Permitir que el return asíncrono ocurra
// Allow the async return to occur
tokio::time::sleep(Duration::from_millis(10)).await;
// Obtener otro buffer (debería reutilizar el retornado)
// Get another buffer (should reuse the returned one)
let buffer3 = pool.get_buffer().await;
// Verificar stats actualizados
// Verify updated stats
let stats = pool.get_stats().await;
assert_eq!(stats.gets, 3);
assert_eq!(stats.hits, 1); // ahora debería haber un hit
assert_eq!(stats.returns, 1); // un buffer retornado
assert_eq!(stats.hits, 1); // now there should be a hit
assert_eq!(stats.returns, 1); // one buffer returned
// Limpiar
// Cleanup
drop(buffer2);
drop(buffer3);
}
@@ -402,19 +402,19 @@ mod tests {
async fn test_buffer_operations() {
let pool = BufferPool::new(1024, 10, 60);
// Obtener buffer
// Get buffer
let mut buffer = pool.get_buffer().await;
// Escribir datos
// Write data
buffer.copy_from_slice(b"Hello, world!");
assert_eq!(buffer.used_size(), 13);
assert_eq!(buffer.as_slice(), b"Hello, world!");
// Convertir a vec y verificar
let vec = buffer.into_vec(); // Esto impide retornar al pool
// Convert to vec and verify
let vec = buffer.into_vec(); // This prevents returning to pool
assert_eq!(vec, b"Hello, world!");
// Verificar que no se incrementan los returns (buffer no retornado)
// Verify that returns are not incremented (buffer not returned)
tokio::time::sleep(Duration::from_millis(10)).await;
let stats = pool.get_stats().await;
assert_eq!(stats.returns, 0);
@@ -422,76 +422,76 @@ mod tests {
#[tokio::test]
async fn test_pool_limit() {
// Pool con solo 3 buffers
// Pool with only 3 buffers
let pool = BufferPool::new(1024, 3, 60);
// Obtener 3 buffers (alcanza el límite)
// Get 3 buffers (reaches the limit)
let buffer1 = pool.get_buffer().await;
let buffer2 = pool.get_buffer().await;
let buffer3 = pool.get_buffer().await;
// Verificar stats
// Verify stats
let stats = pool.get_stats().await;
assert_eq!(stats.gets, 3);
assert_eq!(stats.waits, 0); // sin esperas todavía
assert_eq!(stats.waits, 0); // no waits yet
// Intentar obtener un 4º buffer en una tarea separada (debería esperar)
// Try to get a 4th buffer in a separate task (should wait)
let pool_clone = pool.clone();
let handle = tokio::spawn(async move {
let _buffer4 = pool_clone.get_buffer().await;
true
});
// Dar tiempo para que la tarea intente tomar el buffer
// Give time for the task to try to take the buffer
tokio::time::sleep(Duration::from_millis(50)).await;
// Verificar que hay una espera
// Verify there is a wait
let stats = pool.get_stats().await;
assert_eq!(stats.waits, 1);
// Liberar un buffer
// Release a buffer
drop(buffer1);
// Dar tiempo para el retorno asíncrono y para que la tarea en espera obtenga su buffer
// Give time for the async return and for the waiting task to get its buffer
tokio::time::sleep(Duration::from_millis(50)).await;
// Verificar que la tarea pudo continuar
// Verify the task was able to continue
assert!(handle.await.unwrap());
// Limpiar
// Cleanup
drop(buffer2);
drop(buffer3);
}
#[tokio::test]
async fn test_ttl_expiration() {
// Pool con TTL muy corto para testing
let pool = BufferPool::new(1024, 5, 1); // 1 segundo TTL
// Pool with very short TTL for testing
let pool = BufferPool::new(1024, 5, 1); // 1 second TTL
// Obtener y devolver un buffer
// Get and return a buffer
let buffer = pool.get_buffer().await;
drop(buffer);
// Permitir que el return asíncrono ocurra
// Allow the async return to occur
tokio::time::sleep(Duration::from_millis(50)).await;
// Verificar que hay un buffer en el pool
// Verify there is a buffer in the pool
let stats = pool.get_stats().await;
assert_eq!(stats.returns, 1);
// Esperar a que expire el TTL
// Wait for the TTL to expire
tokio::time::sleep(Duration::from_secs(2)).await;
// Limpiar expirados
// Clean expired
pool.clean_expired_buffers().await;
// Obtener otro buffer (debería ser un miss ya que el anterior expiró)
// Get another buffer (should be a miss since the previous one expired)
let _buffer2 = pool.get_buffer().await;
// Verificar stats
// Verify stats
let stats = pool.get_stats().await;
assert_eq!(stats.evictions, 1); // un buffer expirado
assert_eq!(stats.hits, 0); // sin hits (el buffer expiró)
assert_eq!(stats.misses, 2); // dos misses (1er y 3er get)
assert_eq!(stats.evictions, 1); // one expired buffer
assert_eq!(stats.hits, 0); // no hits (the buffer expired)
assert_eq!(stats.misses, 2); // two misses (1st and 3rd get)
}
}
@@ -16,16 +16,16 @@ use crate::application::ports::compression_ports::{
use crate::domain::errors::DomainError;
use crate::infrastructure::services::buffer_pool::BufferPool;
/// Nivel de compresión para ficheros
/// Compression level for files
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionLevel {
/// Sin compresión (solo para transferencia)
/// No compression (transfer only)
None = 0,
/// Compresión rápida con menor ratio
/// Fast compression with lower ratio
Fast = 1,
/// Compresión balanceada (por defecto)
/// Balanced compression (default)
Default = 6,
/// Compresión máxima (más lenta)
/// Maximum compression (slower)
Best = 9,
}
@@ -40,49 +40,49 @@ impl From<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
/// Interfaz para servicios de compresión
/// Interface for compression services
#[async_trait]
pub trait CompressionService: Send + Sync {
/// Comprime datos en memoria
/// Compresses data in memory
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>>;
/// Descomprime datos en memoria
/// Decompresses data in memory
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)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
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)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
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;
}
/// Implementación de servicios de compresión usando Gzip
/// Gzip compression service implementation
pub struct GzipCompressionService {
/// Pool de buffers para optimización de memoria
/// Buffer pool for memory optimization
buffer_pool: Option<Arc<BufferPool>>,
}
impl GzipCompressionService {
/// Crea una nueva instancia del servicio
/// Creates a new service instance
pub fn new() -> Self {
Self {
buffer_pool: None,
}
}
/// Crea una nueva instancia del servicio con buffer pool
/// Creates a new service instance with buffer pool
pub fn new_with_buffer_pool(buffer_pool: Arc<BufferPool>) -> Self {
Self {
buffer_pool: Some(buffer_pool),
@@ -92,64 +92,64 @@ impl GzipCompressionService {
#[async_trait]
impl CompressionService for GzipCompressionService {
/// Comprime datos en memoria usando Gzip
/// Compresses data in memory using Gzip
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<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 {
// Estimar el tamaño de la compresión (aproximadamente 80% del original para casos típicos)
// Estimate the compression size (approximately 80% of original for typical cases)
let estimated_size = (data.len() as f64 * 0.8) as usize;
// Obtener un buffer del pool
// Get a buffer from the pool
let buffer = pool.get_buffer().await;
// Comprobar si el buffer es suficientemente grande
// Check if the buffer is large enough
if buffer.capacity() >= estimated_size {
// Ejecutar la compresión en un worker thread usando el buffer
// Run compression in a worker thread using the buffer
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone();
// Comprimir datos
// Clonar los datos para evitar problemas de lifetime
// Compress data
// Clone the data to avoid lifetime issues
let data_owned = data.to_vec();
let result = tokio::task::spawn_blocking(move || {
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into());
// Intentar bloquear el mutex (no debería fallar ya que estamos en un hilo separado)
// Try to lock the mutex (should not fail since we are in a separate thread)
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer,
};
// Leer directamente en el buffer
// Read directly into the buffer
let read_bytes = encoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes);
Ok(()) as io::Result<()>
}).await;
// Verificar resultado
// Verify result
match result {
Ok(Ok(())) => {
// Obtener el buffer y convertirlo a Vec<u8>
// Get the buffer and convert it to Vec<u8>
let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone();
drop(buffer); // Liberar el mutex primero
drop(buffer); // Release the mutex first
return Ok(cloned_buffer.into_vec());
},
Ok(Err(e)) => {
error!("Error en compresión con buffer pool: {}", e);
// Continuar con implementación estándar
error!("Compression error with buffer pool: {}", e);
// Fall back to standard implementation
},
Err(e) => {
error!("Error en task de compresión con buffer pool: {}", e);
// Continuar con implementación estándar
error!("Compression task error with buffer pool: {}", e);
// Fall back to standard implementation
}
}
}
}
// Implementación estándar si no hay buffer pool o el buffer es insuficiente
// Clonar los datos para evitar problemas de lifetime
// Standard implementation if there is no buffer pool or the buffer is insufficient
// Clone the data to avoid lifetime issues
let data_owned = data.to_vec();
tokio::task::spawn_blocking(move || {
@@ -158,79 +158,79 @@ impl CompressionService for GzipCompressionService {
encoder.read_to_end(&mut compressed)?;
Ok(compressed)
}).await.unwrap_or_else(|e| {
error!("Error en task de compresión: {}", e);
error!("Compression task error: {}", e);
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
})
}
/// Descomprime datos en memoria
/// Decompresses data in memory
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<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 {
// Estimar el tamaño de la descompresión (aproximadamente 5x del comprimido para casos típicos)
// Estimate the decompression size (approximately 5x of compressed for typical cases)
let estimated_size = compressed_data.len() * 5;
// Obtener un buffer del pool
// Get a buffer from the pool
let buffer = pool.get_buffer().await;
// Comprobar si el buffer es suficientemente grande
// Check if the buffer is large enough
if buffer.capacity() >= estimated_size {
// Clonar datos comprimidos para mover al worker
// Clone compressed data to move to the worker
let data = compressed_data.to_vec();
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone();
// Descomprimir datos
// Decompress data
let result = tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]);
// Intentar bloquear el mutex
// Try to lock the mutex
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer,
};
// Leer directamente en el buffer
// Read directly into the buffer
let read_bytes = decoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes);
Ok(()) as io::Result<()>
}).await;
// Verificar resultado
// Verify result
match result {
Ok(Ok(())) => {
// Obtener el buffer y convertirlo a Vec<u8>
// Get the buffer and convert it to Vec<u8>
let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone();
drop(buffer); // Liberar el mutex primero
drop(buffer); // Release the mutex first
return Ok(cloned_buffer.into_vec());
},
Ok(Err(e)) => {
error!("Error en descompresión con buffer pool: {}", e);
// Continuar con implementación estándar
error!("Decompression error with buffer pool: {}", e);
// Fall back to standard implementation
},
Err(e) => {
error!("Error en task de descompresión con buffer pool: {}", e);
// Continuar con implementación estándar
error!("Decompression task error with buffer pool: {}", e);
// Fall back to standard implementation
}
}
}
}
// Implementación estándar si no hay buffer pool o el buffer es insuficiente
let data = compressed_data.to_vec(); // Clonar para mover al worker
// Standard implementation if there is no buffer pool or the buffer is insufficient
let data = compressed_data.to_vec(); // Clone to move to the worker
tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}).await.unwrap_or_else(|e| {
error!("Error en task de descompresión: {}", e);
error!("Decompression task error: {}", e);
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
})
}
/// Comprime un stream de bytes
/// Compresses a byte stream
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
-> impl Stream<Item = io::Result<Bytes>> + Send
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)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
@@ -310,14 +310,14 @@ impl CompressionService for GzipCompressionService {
})
}
/// Determina si un archivo debe ser comprimido basado en su tipo MIME y tamaño
/// Determines whether a file should be compressed based on its MIME type and size
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
// No comprimir archivos muy pequeños (overhead)
// Do not compress very small files (overhead)
if size < COMPRESSION_SIZE_THRESHOLD {
return false;
}
// No comprimir archivos ya comprimidos
// Do not compress already compressed files
if mime_type.starts_with("image/")
&& !mime_type.contains("svg")
&& !mime_type.contains("bmp") {
@@ -345,7 +345,7 @@ impl CompressionService for GzipCompressionService {
return false;
}
// Comprimir archivos de texto, documentos, y otros tipos compresibles
// Compress text files, documents, and other compressible types
true
}
}
@@ -389,19 +389,19 @@ mod tests {
async fn test_compress_decompress_data() {
let service = GzipCompressionService::new();
// Datos de prueba
// Test data
let data = "Hello, world! ".repeat(1000).into_bytes();
// Comprimir
// Compress
let compressed = CompressionService::compress_data(&service, &data, CompressionLevel::Default).await.unwrap();
// Verificar que la compresión reduce el tamaño
// Verify that compression reduces the size
assert!(compressed.len() < data.len());
// Descomprimir
// Decompress
let decompressed = CompressionService::decompress_data(&service, &compressed).await.unwrap();
// Verificar que los datos originales se recuperan correctamente
// Verify that the original data is recovered correctly
assert_eq!(decompressed, data);
}
@@ -409,30 +409,30 @@ mod tests {
async fn test_compress_decompress_stream() {
let service = GzipCompressionService::new();
// Crear datos de prueba
// Create test data
let chunks = vec![
Ok(Bytes::from("Hello, ")),
Ok(Bytes::from("world! ")),
Ok(Bytes::from("This is a test of streaming compression.")),
];
// Convertir a stream
// Convert to stream
let input_stream = futures::stream::iter(chunks);
// Comprimir el stream
// Compress the stream
let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default);
// Recolectar los bytes comprimidos
// Collect the compressed bytes
let compressed_bytes = compressed_stream
.try_fold(Vec::new(), |mut acc, chunk| async move {
acc.extend_from_slice(&chunk);
Ok(acc)
}).await.unwrap();
// Descomprimir los datos
// Decompress the data
let decompressed = CompressionService::decompress_data(&service, &compressed_bytes).await.unwrap();
// Verificar resultado
// Verify result
let expected = "Hello, world! This is a test of streaming compression.";
assert_eq!(String::from_utf8(decompressed).unwrap(), expected);
}
@@ -441,17 +441,17 @@ mod tests {
fn test_should_compress() {
let service = GzipCompressionService::new();
// Casos que no deberían comprimirse
// Cases that should not be compressed
assert!(!CompressionService::should_compress(&service, "image/jpeg", 100 * 1024));
assert!(!CompressionService::should_compress(&service, "video/mp4", 10 * 1024 * 1024));
assert!(!CompressionService::should_compress(&service, "application/zip", 5 * 1024 * 1024));
// Casos que sí deberían comprimirse
// Cases that should be compressed
assert!(CompressionService::should_compress(&service, "text/html", 100 * 1024));
assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024));
assert!(CompressionService::should_compress(&service, "text/plain", 1024 * 1024));
// Archivos pequeños no deberían comprimirse independientemente del tipo
// Small files should not be compressed regardless of type
assert!(!CompressionService::should_compress(&service, "text/html", 10 * 1024));
}
}
+122 -122
View File
@@ -13,61 +13,61 @@ use crate::domain::entities::file::File;
use crate::common::config::AppConfig;
/// Tipos de entradas en caché
/// Cache entry types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheEntryType {
/// Archivo
/// File
File,
/// Directorio
/// Directory
Directory,
/// Tipo desconocido
/// Unknown type
Unknown,
}
/// Estadísticas de caché para monitoreo
/// Cache statistics for monitoring
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
/// Número de hits en caché
/// Number of cache hits
pub hits: usize,
/// Número de misses en caché
/// Number of cache misses
pub misses: usize,
/// Número de invalidaciones manuales
/// Number of manual invalidations
pub invalidations: usize,
/// Número de expiraciones automáticas
/// Number of automatic expirations
pub expirations: usize,
/// Número de inserciones en caché
/// Number of cache inserts
pub inserts: usize,
/// Tiempo total ahorrado (milisegundos)
/// Total time saved (milliseconds)
pub time_saved_ms: u64,
}
/// Metadatos completos de archivo en caché
/// Complete cached file metadata
#[derive(Debug, Clone)]
pub struct FileMetadata {
/// Ruta absoluta del archivo
/// Absolute file path
pub path: PathBuf,
/// Si el archivo existe físicamente
/// Whether the file physically exists
pub exists: bool,
/// Tipo de entrada (archivo, directorio)
/// Entry type (file, directory)
pub entry_type: CacheEntryType,
/// Tamaño en bytes (para archivos)
/// Size in bytes (for files)
pub size: Option<u64>,
/// Tipo MIME (para archivos)
/// MIME type (for files)
pub mime_type: Option<String>,
/// Timestamp de creación (UNIX epoch seconds)
/// Creation timestamp (UNIX epoch seconds)
pub created_at: Option<u64>,
/// Timestamp de modificación (UNIX epoch seconds)
/// Modification timestamp (UNIX epoch seconds)
pub modified_at: Option<u64>,
/// Acceso previo (usado para LRU)
/// Previous access (used for LRU)
pub last_access: Instant,
/// Tiempo de expiración de la caché
/// Cache expiration time
pub expires_at: Instant,
/// Número de accesos a esta entrada
/// Number of accesses to this entry
pub access_count: usize,
}
impl FileMetadata {
/// Crea una nueva entrada de metadatos
/// Creates a new metadata entry
pub fn new(
path: PathBuf,
exists: bool,
@@ -94,56 +94,56 @@ impl FileMetadata {
}
}
/// Actualiza el tiempo de último acceso
/// Updates the last access time
pub fn touch(&mut self) {
self.last_access = Instant::now();
self.access_count += 1;
}
/// Verifica si la entrada ha expirado
/// Checks if the entry has expired
pub fn is_expired(&self) -> bool {
Instant::now() > self.expires_at
}
/// Actualiza el tiempo de expiración con un nuevo TTL
/// Updates the expiration time with a new TTL
pub fn update_expiry(&mut self, ttl: Duration) {
self.expires_at = Instant::now() + ttl;
}
}
/// Caché avanzada de metadatos de archivos
/// Advanced file metadata cache
pub struct FileMetadataCache {
/// Caché principal de metadatos
/// Main metadata cache
metadata_cache: RwLock<HashMap<PathBuf, FileMetadata>>,
/// Cola LRU para administración de caché
/// LRU queue for cache management
lru_queue: RwLock<VecDeque<PathBuf>>,
/// Estadísticas de uso del caché
/// Cache usage statistics
stats: RwLock<CacheStats>,
/// Configuración global de la aplicación
/// Global application configuration
config: AppConfig,
/// TTL adaptativo para entradas populares
/// Adaptive TTL for popular entries
ttl_multiplier: f64,
/// Umbral de popularidad para TTL extendido
/// Popularity threshold for extended TTL
popularity_threshold: usize,
/// Tamaño máximo de caché
/// Maximum cache size
max_entries: usize,
}
impl FileMetadataCache {
/// Crea una nueva instancia de caché de metadatos
/// Creates a new metadata cache instance
pub fn new(config: AppConfig, max_entries: usize) -> Self {
Self {
metadata_cache: RwLock::new(HashMap::with_capacity(max_entries)),
lru_queue: RwLock::new(VecDeque::with_capacity(max_entries)),
stats: RwLock::new(CacheStats::default()),
config,
ttl_multiplier: 5.0, // Entradas populares tienen 5x TTL
popularity_threshold: 10, // Después de 10 accesos se considera popular
ttl_multiplier: 5.0, // Popular entries have 5x TTL
popularity_threshold: 10, // After 10 accesses it's considered popular
max_entries,
}
}
/// Crea un objeto FileMetadata a partir de un objeto File
/// Creates a FileMetadata object from a File object
pub fn create_metadata_from_file(file: &File, abs_path: PathBuf) -> FileMetadata {
let entry_type = CacheEntryType::File;
let size = Some(file.size());
@@ -151,8 +151,8 @@ impl FileMetadataCache {
let created_at = Some(file.created_at());
let modified_at = Some(file.modified_at());
// Usar un TTL estándar
let ttl = Duration::from_secs(60); // 1 minuto
// Use a standard TTL
let ttl = Duration::from_secs(60); // 1 minute
FileMetadata::new(
abs_path,
@@ -166,28 +166,28 @@ impl FileMetadataCache {
)
}
/// Crea una instancia por defecto
/// Creates a default instance
pub fn default() -> Self {
Self::new(AppConfig::default(), 10_000)
}
/// Crea una instancia de caché con configuración por defecto
/// Creates a cache instance with default configuration
pub fn default_with_config(config: AppConfig) -> Self {
Self::new(config, 50_000) // Caché más grande para sistema en producción
Self::new(config, 50_000) // Larger cache for production system
}
/// Obtiene los metadatos de un archivo si están en caché
/// Gets file metadata if cached
pub async fn get_metadata(&self, path: &Path) -> Option<FileMetadata> {
let start_time = Instant::now();
let mut cache = self.metadata_cache.write().await;
if let Some(metadata) = cache.get_mut(path) {
// Verificar si ha expirado
// Check if expired
if metadata.is_expired() {
// Eliminar de caché si expiró
// Remove from cache if expired
cache.remove(path);
// Actualizar estadísticas
// Update statistics
let mut stats = self.stats.write().await;
stats.misses += 1;
stats.expirations += 1;
@@ -197,10 +197,10 @@ impl FileMetadataCache {
return None;
}
// Actualizar tiempo de acceso
// Update access time
metadata.touch();
// Para entradas populares, extender TTL
// For popular entries, extend TTL
if metadata.access_count >= self.popularity_threshold {
let new_ttl = match metadata.entry_type {
CacheEntryType::File => Duration::from_millis(
@@ -209,33 +209,33 @@ impl FileMetadataCache {
CacheEntryType::Directory => Duration::from_millis(
(self.config.timeouts.dir_operation_ms as f64 * self.ttl_multiplier) as u64
),
_ => Duration::from_secs(60), // 1 minuto por defecto
_ => Duration::from_secs(60), // 1 minute by default
};
metadata.update_expiry(new_ttl);
debug!("Extended TTL for popular entry: {}", path.display());
}
// Calcular tiempo ahorrado aproximado
// Calculate approximate time saved
let elapsed = start_time.elapsed().as_millis() as u64;
let estimated_io_time: u64 = 10; // Asumimos 10ms mínimo para operación de IO
let estimated_io_time: u64 = 10; // We assume 10ms minimum for IO operation
let time_saved = estimated_io_time.saturating_sub(elapsed);
// Actualizar estadísticas
// Update statistics
let mut stats = self.stats.write().await;
stats.hits += 1;
stats.time_saved_ms += time_saved;
debug!("Cache hit for: {}", path.display());
// Mantener también la cola LRU actualizada
// Also keep the LRU queue updated
self.update_lru(path.to_path_buf()).await;
// Clonar para retornar
// Clone to return
return Some(metadata.clone());
}
// No encontrado en caché
// Not found in cache
let mut stats = self.stats.write().await;
stats.misses += 1;
@@ -243,20 +243,20 @@ impl FileMetadataCache {
None
}
/// Actualiza la cola LRU
/// Updates the LRU queue
async fn update_lru(&self, path: PathBuf) {
let mut lru = self.lru_queue.write().await;
// Eliminar si ya existe
// Remove if already exists
if let Some(pos) = lru.iter().position(|p| p == &path) {
lru.remove(pos);
}
// Agregar al final (más reciente)
// Add to the end (most recent)
lru.push_back(path);
}
/// Verifica si un archivo existe
/// Checks if a file exists
pub async fn exists(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.exists);
@@ -265,7 +265,7 @@ impl FileMetadataCache {
None
}
/// Verifica si un path es un directorio
/// Checks if a path is a directory
pub async fn is_dir(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.entry_type == CacheEntryType::Directory);
@@ -274,7 +274,7 @@ impl FileMetadataCache {
None
}
/// Verifica si un path es un archivo
/// Checks if a path is a file
pub async fn is_file(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.entry_type == CacheEntryType::File);
@@ -283,7 +283,7 @@ impl FileMetadataCache {
None
}
/// Obtiene el tamaño de un archivo
/// Gets the size of a file
pub async fn get_size(&self, path: &Path) -> Option<u64> {
if let Some(metadata) = self.get_metadata(path).await {
return metadata.size;
@@ -292,7 +292,7 @@ impl FileMetadataCache {
None
}
/// Obtiene el tipo MIME de un archivo
/// Gets the MIME type of a file
pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
if let Some(metadata) = self.get_metadata(path).await {
return metadata.mime_type;
@@ -301,12 +301,12 @@ impl FileMetadataCache {
None
}
/// Refresca los metadatos de un path
/// Refreshes metadata for a path
pub async fn refresh_metadata(&self, path: &Path) -> Result<FileMetadata, std::io::Error> {
// Realizar lectura real del sistema de archivos
// Perform actual filesystem read
let metadata = fs::metadata(path).await?;
// Determinar tipo de entrada
// Determine entry type
let entry_type = if metadata.is_dir() {
CacheEntryType::Directory
} else if metadata.is_file() {
@@ -315,21 +315,21 @@ impl FileMetadataCache {
CacheEntryType::Unknown
};
// Obtener tamaño para archivos
// Get size for files
let size = if metadata.is_file() {
Some(metadata.len())
} else {
None
};
// Obtener tipo MIME para archivos
// Get MIME type for files
let mime_type = if metadata.is_file() {
Some(from_path(path).first_or_octet_stream().to_string())
} else {
None
};
// Obtener timestamps
// Get timestamps
let created_at = metadata.created()
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
.ok();
@@ -338,14 +338,14 @@ impl FileMetadataCache {
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
.ok();
// Determinar TTL apropiado
// Determine appropriate TTL
let ttl = if metadata.is_dir() {
Duration::from_millis(self.config.timeouts.dir_operation_ms)
} else {
Duration::from_millis(self.config.timeouts.file_operation_ms)
};
// Crear entrada de metadatos
// Create metadata entry
let file_metadata = FileMetadata::new(
path.to_path_buf(),
true,
@@ -357,34 +357,34 @@ impl FileMetadataCache {
ttl,
);
// Actualizar caché
// Update cache
self.update_cache(file_metadata.clone()).await;
Ok(file_metadata)
}
/// Actualiza la caché con nuevos metadatos
/// Updates the cache with new metadata
pub async fn update_cache(&self, metadata: FileMetadata) {
// Evitar caché llena antes de insertar
// Avoid full cache before inserting
self.ensure_capacity().await;
let path = metadata.path.clone();
// Insertar en caché
// Insert into cache
{
let mut cache = self.metadata_cache.write().await;
cache.insert(path.clone(), metadata);
// Actualizar estadísticas
// Update statistics
let mut stats = self.stats.write().await;
stats.inserts += 1;
}
// Actualizar la cola LRU
// Update the LRU queue
self.update_lru(path).await;
}
/// Asegura que hay espacio en la caché
/// Ensures there is space in the cache
async fn ensure_capacity(&self) {
let cache_size = {
let cache = self.metadata_cache.read().await;
@@ -392,15 +392,15 @@ impl FileMetadataCache {
};
if cache_size >= self.max_entries {
self.evict_lru_entries(cache_size / 10).await; // Liberar 10%
self.evict_lru_entries(cache_size / 10).await; // Free up 10%
}
}
/// Elimina entradas menos recientemente usadas
/// Removes least recently used entries
async fn evict_lru_entries(&self, count: usize) {
let mut paths_to_remove = Vec::with_capacity(count);
// Obtener entries a eliminar de la cola LRU
// Get entries to remove from the LRU queue
{
let mut lru = self.lru_queue.write().await;
for _ in 0..count {
@@ -412,7 +412,7 @@ impl FileMetadataCache {
}
}
// Eliminar de la caché principal
// Remove from the main cache
{
let mut cache = self.metadata_cache.write().await;
for path in paths_to_remove {
@@ -423,19 +423,19 @@ impl FileMetadataCache {
debug!("Evicted {} LRU entries from cache", count);
}
/// Invalidar una entrada específica de caché
/// Invalidate a specific cache entry
pub async fn invalidate(&self, path: &Path) {
// Eliminar de la caché principal
// Remove from the main cache
{
let mut cache = self.metadata_cache.write().await;
cache.remove(path);
// Actualizar estadísticas
// Update statistics
let mut stats = self.stats.write().await;
stats.invalidations += 1;
}
// Eliminar de la cola LRU
// Remove from the LRU queue
let path_buf = path.to_path_buf();
{
let mut lru = self.lru_queue.write().await;
@@ -447,12 +447,12 @@ impl FileMetadataCache {
debug!("Invalidated cache entry for: {}", path.display());
}
/// Invalidar recursivamente entradas bajo un directorio
/// Recursively invalidate entries under a directory
pub async fn invalidate_directory(&self, dir_path: &Path) {
let dir_str = dir_path.to_string_lossy().to_string();
let mut paths_to_remove = Vec::new();
// Encontrar todos los paths que comienzan con el directorio
// Find all paths that start with the directory
{
let cache = self.metadata_cache.read().await;
for path in cache.keys() {
@@ -463,13 +463,13 @@ impl FileMetadataCache {
}
}
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.invalidations += paths_to_remove.len();
}
// Eliminar cada path encontrado
// Remove each found path
for path in paths_to_remove {
self.invalidate(&path).await;
}
@@ -477,18 +477,18 @@ impl FileMetadataCache {
debug!("Invalidated directory and contents: {}", dir_path.display());
}
/// Obtener estadísticas actuales de la caché
/// Get current cache statistics
pub async fn get_stats(&self) -> CacheStats {
let stats = self.stats.read().await;
stats.clone()
}
/// Limpia todas las entradas expiradas de la caché
/// Clears all expired entries from the cache
pub async fn clear_expired(&self) {
let now = Instant::now();
let mut paths_to_remove = Vec::new();
// Encontrar entradas expiradas
// Find expired entries
{
let cache = self.metadata_cache.read().await;
for (path, metadata) in cache.iter() {
@@ -498,16 +498,16 @@ impl FileMetadataCache {
}
}
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.expirations += paths_to_remove.len();
}
// Guardar la cantidad de entradas para el logging
// Save the number of entries for logging
let num_paths = paths_to_remove.len();
// Eliminar entradas expiradas
// Remove expired entries
for path in paths_to_remove {
self.invalidate(&path).await;
}
@@ -515,19 +515,19 @@ impl FileMetadataCache {
debug!("Cleared {} expired entries from cache", num_paths);
}
/// Inicia el proceso de limpieza periódica
/// Starts the periodic cleanup process
pub fn start_cleanup_task(cache: Arc<Self>) -> BoxFuture<'static, ()> {
Box::pin(async move {
let cleanup_interval = Duration::from_secs(60); // Cada minuto
let cleanup_interval = Duration::from_secs(60); // Every minute
loop {
// Esperar el intervalo
// Wait for the interval
time::sleep(cleanup_interval).await;
// Limpiar entradas expiradas
// Clean expired entries
cache.clear_expired().await;
// Registrar estadísticas
// Log statistics
let stats = cache.get_stats().await;
let cache_size = {
let cache_map = cache.metadata_cache.read().await;
@@ -550,12 +550,12 @@ impl FileMetadataCache {
})
}
/// Precarga metadatos de directorios completos (útil para inicialización)
/// Preloads metadata for entire directories (useful for initialization)
pub async fn preload_directory(&self, dir_path: &Path, recursive: bool, max_depth: usize) -> Result<usize, std::io::Error> {
self._preload_directory_internal(dir_path, recursive, max_depth, 0).await
}
/// Implementación interna de precarga con seguimiento de profundidad
/// Internal preload implementation with depth tracking
async fn _preload_directory_internal(
&self,
dir_path: &Path,
@@ -568,20 +568,20 @@ impl FileMetadataCache {
return Ok(0);
}
// Obtener entradas del directorio
// Get directory entries
let mut entries = fs::read_dir(dir_path).await?;
let mut count = 0;
// Procesar cada entrada
// Process each entry
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let metadata = fs::metadata(&path).await?;
// Refrescar metadatos de esta entrada
// Refresh metadata for this entry
self.refresh_metadata(&path).await?;
count += 1;
// Recursivamente procesar subdirectorios si es necesario
// Recursively process subdirectories if needed
if recursive && metadata.is_dir() {
// Box to break recursion
count += self._preload_directory_internal(
@@ -657,37 +657,37 @@ mod tests {
#[tokio::test]
async fn test_cache_operations() {
// Crear directorio temporal para pruebas
// Create temporary directory for tests
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
// Crear un archivo de prueba
// Create a test file
let mut file = File::create(&file_path).await.unwrap();
file.write_all(b"test content").await.unwrap();
file.flush().await.unwrap();
drop(file);
// Crear caché
// Create cache
let config = AppConfig::default();
let cache = FileMetadataCache::new(config, 1000);
// Verificar miss inicial
// Verify initial miss
assert!(cache.exists(&file_path).await.is_none());
// Refrescar y verificar hit
// Refresh and verify hit
let metadata = cache.refresh_metadata(&file_path).await.unwrap();
assert_eq!(metadata.entry_type, CacheEntryType::File);
assert_eq!(metadata.size, Some(12)); // "test content" = 12 bytes
// Verificar que ahora existe en caché
// Verify it now exists in cache
assert_eq!(cache.exists(&file_path).await, Some(true));
assert_eq!(cache.is_file(&file_path).await, Some(true));
// Invalidar y verificar que ya no existe en caché
// Invalidate and verify it no longer exists in cache
cache.invalidate(&file_path).await;
assert!(cache.exists(&file_path).await.is_none());
// Verificar estadísticas
// Verify statistics
let stats = cache.get_stats().await;
assert_eq!(stats.inserts, 1);
assert_eq!(stats.invalidations, 1);
@@ -696,7 +696,7 @@ mod tests {
#[tokio::test]
async fn test_directory_operations() {
// Crear estructura de directorios para pruebas
// Create directory structure for tests
let temp_dir = tempdir().unwrap();
// Canonicalize to handle macOS /var -> /private/var symlinks
let base_path = temp_dir.path().canonicalize().unwrap();
@@ -709,24 +709,24 @@ mod tests {
File::create(&file1).await.unwrap();
File::create(&file2).await.unwrap();
// Crear caché
// Create cache
let config = AppConfig::default();
let cache = FileMetadataCache::new(config, 1000);
// Precargar directorio recursivamente
// Preload directory recursively
// preload_directory caches the *contents* of the directory, not the root itself
let count = cache.preload_directory(&base_path, true, 2).await.unwrap();
assert_eq!(count, 3); // subdir, file1, file2
// Verificar existencia en caché (solo contenido, no la raíz)
// Verify existence in cache (only contents, not the root)
assert_eq!(cache.is_dir(&sub_dir).await, Some(true));
assert_eq!(cache.is_file(&file1).await, Some(true));
assert_eq!(cache.is_file(&file2).await, Some(true));
// Invalidar directorio y contenido
// Invalidate directory and contents
cache.invalidate_directory(&base_path).await;
// Verificar que nada existe en caché
// Verify nothing exists in cache
assert!(cache.exists(&sub_dir).await.is_none());
assert!(cache.exists(&file1).await.is_none());
assert!(cache.exists(&file2).await.is_none());
@@ -179,7 +179,7 @@ impl IdMappingOptimizer {
{
let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) {
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.get_id_hits += 1;
@@ -189,19 +189,19 @@ impl IdMappingOptimizer {
}
}
// Si no está en caché, agregar a la cola de batch
// If not in cache, add to batch queue
{
let mut batch_queue = self.pending_batch.lock().await;
batch_queue.path_to_id_requests.insert(path_str);
}
// No encontrado en caché, debe procesarse en batch
// Not found in cache, must be processed in batch
Ok(None)
}
/// Procesa las solicitudes pendientes en batch
/// Processes pending requests in batch
async fn process_batch(&self) -> Result<BatchResult, IdMappingError> {
// Adquirir permiso para operación batch
// Acquire permit for batch operation
let _permit = self.batch_limiter.acquire().await.unwrap();
// Get pending requests
@@ -214,13 +214,13 @@ impl IdMappingOptimizer {
(paths, ids)
};
// Crear resultados
// Create results
let mut result = BatchResult {
path_to_id: HashMap::with_capacity(path_requests.len()),
id_to_path: HashMap::with_capacity(id_requests.len()),
};
// Procesar solicitudes path->id en batch
// Process path->id requests in batch
for path_str in path_requests {
let path = StoragePath::from_string(&path_str);
match self.base_service.get_or_create_id(&path).await {
@@ -230,12 +230,12 @@ impl IdMappingOptimizer {
},
Err(e) => {
error!("Error batch-processing path {}: {}", path_str, e);
// Continuar con las demás solicitudes
// Continue with remaining requests
}
}
}
// Procesar solicitudes id->path en batch
// Process id->path requests in batch
for id in id_requests {
match self.base_service.get_path_by_id(&id).await {
Ok(path) => {
@@ -245,7 +245,7 @@ impl IdMappingOptimizer {
},
Err(e) => {
error!("Error batch-processing ID {}: {}", id, e);
// Continuar con las demás solicitudes
// Continue with remaining requests
}
}
}
@@ -266,14 +266,14 @@ impl IdMappingOptimizer {
}
}
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.batch_operations += 1;
stats.batch_items_processed += result.path_to_id.len() + result.id_to_path.len();
}
// Guardar los cambios al disco en segundo plano
// Save changes to disk in the background
let service_clone = self.base_service.clone();
tokio::spawn(async move {
if let Err(e) = service_clone.save_pending_changes().await {
@@ -284,15 +284,15 @@ impl IdMappingOptimizer {
Ok(result)
}
/// Fuerza el procesamiento de solicitudes pendientes si hay suficientes
/// Forces processing of pending requests if there are enough
async fn trigger_batch_if_needed(&self, min_batch_size: usize) -> Result<(), IdMappingError> {
// Verificar si hay suficientes solicitudes pendientes
// Check if there are enough pending requests
let should_process = {
let batch_queue = self.pending_batch.lock().await;
batch_queue.path_to_id_requests.len() + batch_queue.id_to_path_requests.len() >= min_batch_size
};
// Procesar si es necesario
// Process if necessary
if should_process {
self.process_batch().await?;
}
@@ -300,17 +300,17 @@ impl IdMappingOptimizer {
Ok(())
}
/// Precargar un conjunto de rutas para obtener sus IDs en batch
/// Preload a set of paths to get their IDs in batch
pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
// Solo proceder si hay rutas para cargar
// Only proceed if there are paths to load
if paths.is_empty() {
return Ok(());
}
// Rutas que debemos cargar (las que no están en caché)
// Paths we need to load (those not in cache)
let mut paths_to_load = Vec::new();
// Verificar primero el caché
// Check cache first
{
let cache = self.path_to_id_cache.read().await;
for path in paths {
@@ -321,12 +321,12 @@ impl IdMappingOptimizer {
}
}
// Si todos estaban en caché, terminar
// If all were in cache, finish
if paths_to_load.is_empty() {
return Ok(());
}
// Agregar rutas a la cola para procesamiento batch
// Add paths to queue for batch processing
{
let mut batch_queue = self.pending_batch.lock().await;
for path in paths_to_load {
@@ -334,23 +334,23 @@ impl IdMappingOptimizer {
}
}
// Ejecutar procesamiento batch inmediatamente
// Execute batch processing immediately
self.process_batch().await?;
Ok(())
}
/// Precargar un conjunto de IDs para obtener sus rutas en batch
/// Preload a set of IDs to get their paths in batch
pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
// Solo proceder si hay IDs para cargar
// Only proceed if there are IDs to load
if ids.is_empty() {
return Ok(());
}
// IDs que debemos cargar (los que no están en caché)
// IDs we need to load (those not in cache)
let mut ids_to_load = Vec::new();
// Verificar primero el caché
// Check cache first
{
let cache = self.id_to_path_cache.read().await;
for id in ids {
@@ -360,12 +360,12 @@ impl IdMappingOptimizer {
}
}
// Si todos estaban en caché, terminar
// If all were in cache, finish
if ids_to_load.is_empty() {
return Ok(());
}
// Agregar IDs a la cola para procesamiento batch
// Add IDs to queue for batch processing
{
let mut batch_queue = self.pending_batch.lock().await;
for id in ids_to_load {
@@ -373,7 +373,7 @@ impl IdMappingOptimizer {
}
}
// Ejecutar procesamiento batch inmediatamente
// Execute batch processing immediately
self.process_batch().await?;
Ok(())
@@ -383,7 +383,7 @@ impl IdMappingOptimizer {
#[async_trait]
impl IdMappingPort for IdMappingOptimizer {
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.get_id_queries += 1;
@@ -391,7 +391,7 @@ impl IdMappingPort for IdMappingOptimizer {
let path_str = path.to_string();
// Verificar primero en el caché
// Check cache first
{
let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) {
@@ -443,7 +443,7 @@ impl IdMappingPort for IdMappingOptimizer {
}
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
// Actualizar estadísticas
// Update statistics
{
let mut stats = self.stats.write().await;
stats.path_by_id_queries += 1;
@@ -493,21 +493,21 @@ impl IdMappingPort for IdMappingOptimizer {
}
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
// Invalidar caché para este ID
// Invalidate cache for this ID
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
// Eliminar la entrada del ID
// Remove the ID entry
if let Some((old_path, _)) = id_cache.remove(id) {
path_cache.remove(&old_path);
}
}
// Actualizar en el servicio base
// Update in the base service
let result = self.base_service.update_path(id, new_path).await?;
// Actualizar caché con el nuevo mapeo
// Update cache with new mapping
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
@@ -523,25 +523,25 @@ impl IdMappingPort for IdMappingOptimizer {
}
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
// Invalidar caché para este ID
// Invalidate cache for this ID
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
// Eliminar la entrada del ID
// Remove the ID entry
if let Some((path, _)) = id_cache.remove(id) {
path_cache.remove(&path);
}
}
// Eliminar en el servicio base
// Remove from the base service
self.base_service.remove_id(id).await?;
Ok(())
}
async fn save_changes(&self) -> Result<(), DomainError> {
// Delegar al servicio base
// Delegate to the base service
self.base_service.save_changes().await?;
Ok(())
@@ -569,15 +569,15 @@ mod tests {
let path = StoragePath::from_string("/test/file.txt");
// Primera llamada debería usar el servicio base
// First call should use the base service
let id = optimizer.get_or_create_id(&path).await.unwrap();
assert!(!id.is_empty(), "ID should not be empty");
// Segunda llamada debería usar caché
// Second call should use cache
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Same path should return same ID");
// Verificar estadísticas de caché
// Verify cache statistics
let stats = optimizer.get_stats().await;
assert_eq!(stats.get_id_queries, 2, "Should have 2 queries");
assert_eq!(stats.get_id_hits, 1, "Should have 1 hit");
@@ -587,27 +587,27 @@ mod tests {
async fn test_batch_processing() {
let (_, optimizer) = create_test_service().await;
// Crear un lote de rutas
// Create a batch of paths
let mut paths = Vec::new();
for i in 0..50 {
paths.push(StoragePath::from_string(&format!("/test/batch/file{}.txt", i)));
}
// Precargar las rutas
// Preload the paths
optimizer.preload_paths(paths.clone()).await.unwrap();
// Verificar que todas están en caché
// Verify all are in cache
for path in &paths {
let id = optimizer.get_or_create_id(path).await.unwrap();
assert!(!id.is_empty(), "ID should be available for path");
}
// Verificar estadísticas
// Verify statistics
let stats = optimizer.get_stats().await;
assert_eq!(stats.batch_operations, 1, "Should have 1 batch operation");
assert!(stats.batch_items_processed >= 50, "Should have processed at least 50 items");
// Verificar que todas las consultas posteriores son hits en caché
// Verify all subsequent queries are cache hits
assert_eq!(stats.get_id_hits, 50, "All subsequente queries should be cache hits");
}
@@ -615,21 +615,21 @@ mod tests {
async fn test_cache_cleanup() {
let (_, optimizer) = create_test_service().await;
// Crear algunas entradas
// Create some entries
let path = StoragePath::from_string("/test/cleanup.txt");
let id = optimizer.get_or_create_id(&path).await.unwrap();
// Verificar estadísticas iniciales
// Verify initial statistics
{
let stats = optimizer.get_stats().await;
assert_eq!(stats.get_id_queries, 1, "Should have 1 query");
assert_eq!(stats.get_id_hits, 0, "Should have 0 hits");
}
// Ejecutar limpieza (no debería eliminar nada todavía)
// Run cleanup (should not remove anything yet)
optimizer.cleanup_cache().await;
// Verificar que el caché sigue funcionando
// Verify cache is still working
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Cache should still work after cleanup");
@@ -12,7 +12,7 @@ use crate::common::errors::{DomainError, ErrorKind};
use crate::application::ports::outbound::IdMappingPort;
use crate::common::config::TimeoutConfig;
/// Error específico para el servicio de mapeo de IDs
/// Specific error for the ID mapping service
#[derive(Debug, thiserror::Error)]
pub enum IdMappingError {
#[error("ID not found: {0}")]
@@ -31,7 +31,7 @@ pub enum IdMappingError {
Other(String),
}
// Implementar conversión de IdMappingError a DomainError
// Implement conversion from IdMappingError to DomainError
impl From<IdMappingError> for DomainError {
fn from(err: IdMappingError) -> Self {
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)]
struct IdMap {
path_to_id: HashMap<String, String>,
id_to_path: HashMap<String, String>, // Campo para búsqueda bidireccional eficiente
version: u32, // Versión para detectar cambios
id_to_path: HashMap<String, String>, // Field for efficient bidirectional lookup
version: u32, // Version to detect changes
}
/// Servicio para gestionar mapeos entre rutas y IDs únicos
/// Service to manage mappings between paths and unique IDs
pub struct IdMappingService {
map_path: PathBuf,
id_map: RwLock<IdMap>,
save_mutex: Mutex<()>, // Para evitar múltiples guardados concurrentes
save_mutex: Mutex<()>, // To prevent multiple concurrent saves
timeouts: TimeoutConfig,
pending_save: RwLock<bool>, // Indica si hay cambios pendientes
pending_save: RwLock<bool>, // Indicates if there are pending changes
}
impl IdMappingService {
/// Crea un nuevo servicio de mapeo de IDs
/// Creates a new ID mapping service
pub async fn new(map_path: PathBuf) -> Result<Self, DomainError> {
let timeouts = TimeoutConfig::default();
let id_map = Self::load_id_map(&map_path, &timeouts).await?;
@@ -91,7 +91,7 @@ impl IdMappingService {
})
}
/// Crea un servicio de mapeo de IDs en memoria (para pruebas)
/// Creates an in-memory ID mapping service (for testing)
///
/// Similar functionality as new_in_memory but with a simpler signature for dummy use
pub fn dummy() -> Self {
@@ -104,7 +104,7 @@ impl IdMappingService {
}
}
/// Crea un servicio de mapeo de IDs en memoria (para pruebas - versión original)
/// Creates an in-memory ID mapping service (for testing - original version)
pub fn new_in_memory() -> Self {
Self {
map_path: PathBuf::from("memory"),
@@ -115,10 +115,10 @@ impl IdMappingService {
}
}
/// Carga el mapa de IDs desde disco con manejo robusto de errores
/// Loads the ID map from disk with robust error handling
async fn load_id_map(map_path: &PathBuf, timeouts: &TimeoutConfig) -> Result<IdMap, DomainError> {
if map_path.exists() {
// Intentar leer con timeout para evitar bloqueos indefinidos
// Try to read with timeout to avoid indefinite blocking
let read_result = time::timeout(
timeouts.lock_timeout(),
fs::read_to_string(map_path)
@@ -127,10 +127,10 @@ impl IdMappingService {
let content = read_result.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to read ID map from {}: {}", map_path.display(), e)))?;
// Parsear el JSON
// Parse the JSON
match serde_json::from_str::<IdMap>(&content) {
Ok(mut map) => {
// Reconstruir el mapa inverso si es necesario
// Rebuild the inverse map if necessary
if map.id_to_path.is_empty() && !map.path_to_id.is_empty() {
let mut rebuild_count = 0;
for (path, id) in &map.path_to_id {
@@ -146,7 +146,7 @@ impl IdMappingService {
},
Err(e) => {
tracing::error!("Error parsing ID map: {}", e);
// Intentar hacer un respaldo del archivo corrupto
// Try to backup the corrupted file
let backup_path = map_path.with_extension("json.bak");
if let Err(copy_err) = tokio::fs::copy(map_path, &backup_path).await {
tracing::error!("Failed to backup corrupted map file: {}", copy_err);
@@ -158,18 +158,18 @@ impl IdMappingService {
return Ok(IdMap {
path_to_id: HashMap::new(),
id_to_path: HashMap::new(),
version: 1, // Iniciar con versión 1
version: 1, // Start with version 1
});
}
}
}
// Devolver un mapa vacío si el archivo no existe y crear el archivo
// Return an empty map if the file doesn't exist and create the file
tracing::info!("No existing ID map found, creating new empty map");
let empty_map = IdMap {
path_to_id: HashMap::new(),
id_to_path: HashMap::new(),
version: 1, // Iniciar con versión 1
version: 1, // Start with version 1
};
// Ensure directory exists
@@ -198,16 +198,16 @@ impl IdMappingService {
Ok(empty_map)
}
/// Guarda el mapa de IDs en disco de manera segura
/// Saves the ID map to disk safely
async fn save_id_map(&self) -> Result<(), DomainError> {
// Adquirir bloqueo exclusivo para salvar
// Acquire exclusive lock for saving
let _lock = time::timeout(
self.timeouts.lock_timeout(),
self.save_mutex.lock()
).await
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring save lock for ID mapping"))?;
// Crear JSON con el lock de lectura para minimizar el tiempo de bloqueo
// Create JSON with read lock to minimize lock hold time
let json = {
let mut map = time::timeout(
self.timeouts.lock_timeout(),
@@ -215,7 +215,7 @@ impl IdMappingService {
).await
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring write lock for ID mapping"))?;
// Incrementar versión sólo si hay cambios por guardar
// Increment version only if there are pending changes to save
let pending = *self.pending_save.read().await;
if pending {
map.version += 1;
@@ -227,16 +227,16 @@ impl IdMappingService {
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to serialize ID map to JSON: {}", e)))?
};
// Escribir a un archivo temporal primero para evitar corrupción
// Write to a temporary file first to avoid corruption
let temp_path = self.map_path.with_extension("json.tmp");
fs::write(&temp_path, &json).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to write temporary ID map to {}: {}", temp_path.display(), e)))?;
// Realizar el rename atómico
// Perform the atomic rename
fs::rename(&temp_path, &self.map_path).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to rename temporary ID map to {}: {}", self.map_path.display(), e)))?;
// Resetear flag de pendientes
// Reset pending flag
{
let mut pending = self.pending_save.write().await;
*pending = false;
@@ -246,22 +246,22 @@ impl IdMappingService {
Ok(())
}
/// Genera un ID único
/// Generates a unique ID
fn generate_id(&self) -> String {
Uuid::new_v4().to_string()
}
/// Marca cambios como pendientes
/// Marks changes as pending
async fn mark_pending(&self) {
let mut pending = self.pending_save.write().await;
*pending = true;
}
/// Obtiene el ID para una ruta o genera uno nuevo si no existe
/// Gets the ID for a path or generates a new one if it doesn't exist
pub async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, IdMappingError> {
let path_str = path.to_string();
// Primer intento con lock de lectura (más eficiente)
// First attempt with read lock (more efficient)
{
let read_result = match time::timeout(
self.timeouts.lock_timeout(),
@@ -276,7 +276,7 @@ impl IdMappingService {
}
}
// Si no se encuentra, adquirir lock de escritura
// If not found, acquire write lock
let write_result = match time::timeout(
self.timeouts.lock_timeout(),
self.id_map.write()
@@ -287,18 +287,18 @@ impl IdMappingService {
let mut map = write_result;
// Verificar nuevamente (podría haberse agregado mientras esperábamos el lock)
// Check again (it could have been added while we were waiting for the lock)
if let Some(id) = map.path_to_id.get(&path_str) {
return Ok(id.clone());
}
// Generar un nuevo ID y almacenarlo
// Generate a new ID and store it
let id = self.generate_id();
map.path_to_id.insert(path_str.clone(), id.clone());
map.id_to_path.insert(id.clone(), path_str);
// Marcar como pendiente para guardar
drop(map); // Liberar el write lock antes de adquirir otro
// Mark as pending for saving
drop(map); // Release the write lock before acquiring another
self.mark_pending().await;
tracing::debug!("Created new ID mapping: {} -> {}", path.to_string(), id);
@@ -306,7 +306,7 @@ impl IdMappingService {
Ok(id)
}
/// Obtiene una ruta por su ID con manejo de timeout
/// Gets a path by its ID with timeout handling
pub async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, IdMappingError> {
let read_result = match time::timeout(
self.timeouts.lock_timeout(),
@@ -323,7 +323,7 @@ impl IdMappingService {
Err(IdMappingError::NotFound(id.to_string()))
}
/// Actualiza el mapeo de un ID existente a una nueva ruta
/// Updates the mapping of an existing ID to a new path
pub async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), IdMappingError> {
let write_result = match time::timeout(
self.timeouts.lock_timeout(),
@@ -335,17 +335,17 @@ impl IdMappingService {
let mut map = write_result;
// Buscar la ruta anterior para eliminarla
// Find the previous path to remove it
if let Some(old_path) = map.id_to_path.get(id).cloned() {
map.path_to_id.remove(&old_path);
// Registrar la nueva ruta
// Register the new path
let new_path_str = new_path.to_string();
map.path_to_id.insert(new_path_str.clone(), id.to_string());
map.id_to_path.insert(id.to_string(), new_path_str);
// Marcar como pendiente
drop(map); // Liberar el write lock antes de adquirir otro
// Mark as pending
drop(map); // Release the write lock before acquiring another
self.mark_pending().await;
tracing::debug!("Updated path mapping for ID {}: {} -> {}",
@@ -357,7 +357,7 @@ impl IdMappingService {
}
}
/// Elimina un ID del mapa
/// Removes an ID from the map
pub async fn remove_id(&self, id: &str) -> Result<(), IdMappingError> {
let write_result = match time::timeout(
self.timeouts.lock_timeout(),
@@ -369,12 +369,12 @@ impl IdMappingService {
let mut map = write_result;
// Buscar la ruta para eliminarla
// Find the path to remove it
if let Some(path) = map.id_to_path.remove(id) {
map.path_to_id.remove(&path);
// Marcar como pendiente
drop(map); // Liberar el write lock antes de adquirir otro
// Mark as pending
drop(map); // Release the write lock before acquiring another
self.mark_pending().await;
tracing::debug!("Removed ID mapping: {} -> {}", id, path);
@@ -384,9 +384,9 @@ impl IdMappingService {
}
}
/// Guarda cambios pendientes al disco inmediatamente, sin debounce
/// Saves pending changes to disk immediately, without debounce
pub async fn save_pending_changes(&self) -> Result<(), IdMappingError> {
// Verificar si hay cambios pendientes
// Check if there are pending changes
{
let pending = self.pending_save.read().await;
if !*pending {
@@ -394,12 +394,12 @@ impl IdMappingService {
}
}
// Guardar inmediatamente (sin debounce ni spawn)
// Save immediately (without debounce or spawn)
match self.save_id_map().await {
Ok(_) => {
tracing::info!("ID mappings saved successfully to disk at {}", self.map_path.display());
// Verificar explícitamente que el archivo existe y tiene tamaño
// Explicitly verify that the file exists and has size
match std::fs::metadata(&self.map_path) {
Ok(metadata) => {
if metadata.len() > 0 {
@@ -410,7 +410,7 @@ impl IdMappingService {
},
Err(e) => {
tracing::error!("Failed to verify saved map file: {}", e);
// Intentar un segundo guardado si la verificación falla
// Try a second save if verification fails
if let Err(retry_err) = self.save_id_map().await {
tracing::error!("Second save attempt also failed: {}", retry_err);
return Err(IdMappingError::IoError(std::io::Error::new(
@@ -426,7 +426,7 @@ impl IdMappingService {
},
Err(e) => {
tracing::error!("Failed to save ID map to {}: {}", self.map_path.display(), e);
// Intentar un segundo guardado con retraso en caso de error
// Try a second save with delay in case of error
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
match self.save_id_map().await {
Ok(_) => {
@@ -448,31 +448,31 @@ impl IdMappingService {
#[async_trait]
impl IdMappingPort for IdMappingService {
/// Obtiene el ID para una ruta o genera uno nuevo si no existe
/// Gets the ID for a path or generates a new one if it doesn't exist
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
self.get_or_create_id(path).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get or create ID for path: {}: {}", path.to_string(), e)))
}
/// Obtiene una ruta por su ID con manejo de timeout
/// Gets a path by its ID with timeout handling
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
self.get_path_by_id(id).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get path for ID: {}: {}", id, e)))
}
/// Actualiza el mapeo de un ID existente a una nueva ruta
/// Updates the mapping of an existing ID to a new path
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
self.update_path(id, new_path).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to update path for ID: {} to {}: {}", id, new_path.to_string(), e)))
}
/// Elimina un ID del mapa
/// Removes an ID from the map
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
self.remove_id(id).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to remove ID: {}: {}", id, e)))
}
/// Guarda cambios pendientes al disco
/// Saves pending changes to disk
async fn save_changes(&self) -> Result<(), DomainError> {
self.save_pending_changes().await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to save pending ID mapping changes: {}", e)))
@@ -481,7 +481,7 @@ impl IdMappingPort for IdMappingService {
// The extension methods were moved to the IdMappingPort trait as default implementations
// Implementar Clone para poder usar en tokio::spawn
// Implement Clone to allow use in tokio::spawn
/// Synchronous helper for contexts where we can't use async
impl IdMappingService {
/// Create a new service synchronously (only for stubs and initialization)
@@ -499,13 +499,13 @@ impl IdMappingService {
impl Clone for IdMappingService {
fn clone(&self) -> Self {
// No podemos clonar directamente los RwLock/Mutex,
// pero podemos crear nuevas instancias que apunten al mismo Arc interno
// Sin embargo, en este caso simplemente necesitamos la map_path
// We cannot directly clone the RwLock/Mutex,
// but we can create new instances that point to the same internal Arc
// However, in this case we simply need the map_path
Self {
map_path: self.map_path.clone(),
id_map: RwLock::new(IdMap::default()), // Esto no se usa en el task asíncrono
save_mutex: Mutex::new(()), // Esto tampoco
id_map: RwLock::new(IdMap::default()), // This is not used in the async task
save_mutex: Mutex::new(()), // Neither is this
timeouts: self.timeouts.clone(),
pending_save: RwLock::new(false),
}
@@ -530,7 +530,7 @@ mod tests {
assert!(!id.is_empty(), "ID should not be empty");
// Verificar que el mismo ID se devuelve para la misma ruta
// Verify that the same ID is returned for the same path
let id2 = service.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Same path should return same ID");
}
@@ -557,7 +557,7 @@ mod tests {
let temp_dir = tempdir().unwrap();
let map_path = temp_dir.path().join("id_map.json");
// Crear y poblar el servicio
// Create and populate the service
let service = IdMappingService::new(map_path.clone()).await.unwrap();
let path1 = StoragePath::from_string("/test/file1.txt");
@@ -565,16 +565,16 @@ mod tests {
let id1 = service.get_or_create_id(&path1).await.unwrap();
let id2 = service.get_or_create_id(&path2).await.unwrap();
// Guardar cambios
// Save changes
service.save_pending_changes().await.unwrap();
// Esperar para asegurar que el guardado asíncrono termine
// Wait to ensure the async save completes
tokio::time::sleep(Duration::from_millis(500)).await;
// Crear un nuevo servicio que debería cargar el mismo mapa
// Create a new service that should load the same map
let service2 = IdMappingService::new(map_path).await.unwrap();
// Verificar que los IDs coinciden
// Verify that the IDs match
let loaded_id1 = service2.get_or_create_id(&path1).await.unwrap();
let loaded_id2 = service2.get_or_create_id(&path2).await.unwrap();
@@ -591,7 +591,7 @@ mod tests {
let service = std::sync::Arc::new(IdMappingService::new(map_path).await.unwrap());
// Crear múltiples tareas que intentan acceder simultáneamente
// Create multiple tasks that attempt simultaneous access
let mut tasks = Vec::new();
for i in 0..100 {
let path = StoragePath::from_string(&format!("/test/concurrent/file{}.txt", i));
@@ -602,15 +602,15 @@ mod tests {
}));
}
// Esperar a que todas terminen
// Wait for all to finish
let results = join_all(tasks).await;
// Verificar que todas tuvieron éxito
// Verify that all succeeded
for result in results {
assert!(result.unwrap().is_ok(), "Concurrent operations should succeed");
}
// Guardar cambios
// Save changes
service.save_pending_changes().await.unwrap();
}
}
+3 -3
View File
@@ -110,7 +110,7 @@ impl TokenServicePort for JwtTokenService {
DomainError::new(
ErrorKind::InternalError,
"TokenService",
format!("Error al generar token: {}", e)
format!("Error generating token: {}", e)
)
})
}
@@ -126,12 +126,12 @@ impl TokenServicePort for JwtTokenService {
.map_err(|e| {
match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado")
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expired")
},
_ => DomainError::new(
ErrorKind::AccessDenied,
"TokenService",
format!("Token inválido: {}", e)
format!("Invalid token: {}", e)
),
}
})?;
+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
//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs).
//! This service was moved from domain/services because it implements application traits
//! (StoragePort, StorageMediator) and has file system dependencies (tokio::fs).
//!
//! StoragePath (Value Object) permanece en domain/services/path_service.rs
//! StoragePath (Value Object) remains in domain/services/path_service.rs
use std::path::{Path, PathBuf};
use async_trait::async_trait;
@@ -15,18 +15,18 @@ use crate::application::services::storage_mediator::{StorageMediator, StorageMed
use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath;
/// Servicio de infraestructura para manejar operaciones con rutas de almacenamiento
/// Infrastructure service for handling storage path operations
pub struct PathService {
root_path: PathBuf,
}
impl PathService {
/// Crea un nuevo servicio de rutas con una raíz específica
/// Creates a new path service with a specific root
pub fn new(root_path: PathBuf) -> Self {
Self { root_path }
}
/// Convierte una ruta del dominio a una ruta física absoluta
/// Converts a domain path to an absolute physical path
pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
let mut path = self.root_path.clone();
for segment in storage_path.segments() {
@@ -35,7 +35,7 @@ impl PathService {
path
}
/// Convierte una ruta física a una ruta de dominio
/// Converts a physical path to a domain path
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
physical_path.strip_prefix(&self.root_path).ok().map(|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 {
folder_path.join(file_name)
}
/// Verifica si una ruta es directamente hija de otra
/// Checks if a path is a direct child of another
pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
if let Some(child_parent) = potential_child.parent() {
&child_parent == parent_path
@@ -63,7 +63,7 @@ impl PathService {
}
}
/// Verifica si una ruta está en la raíz
/// Checks if a path is at the root
pub fn is_in_root(&self, path: &StoragePath) -> bool {
path.parent().map_or(true, |p| p.is_empty())
}
@@ -73,9 +73,9 @@ impl PathService {
&self.root_path
}
/// Valida una ruta para asegurar que no contiene componentes peligrosos
/// Validates a path to ensure it doesn't contain dangerous components
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
// Verificar que no haya segmentos vacíos
// Check for empty segments
if path.segments().iter().any(|s| s.is_empty()) {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -84,7 +84,7 @@ impl PathService {
));
}
// Verificar que no haya caracteres peligrosos
// Check for dangerous characters
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
for segment in path.segments() {
if segment.contains(&dangerous_chars[..]) {
@@ -95,7 +95,7 @@ impl PathService {
));
}
// Verificar que no empiece con . (oculto en Unix)
// Check that it doesn't start with . (hidden in Unix)
if segment.starts_with('.') && segment != ".well-known" {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -120,13 +120,13 @@ impl StoragePort for PathService {
}
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
// Primero validar la ruta
// First validate the path
self.validate_path(storage_path)?;
// Resolver a ruta física
// Resolve to physical path
let physical_path = self.resolve_path(storage_path);
// Crear directorios si no existen
// Create directories if they don't exist
if !physical_path.exists() {
fs::create_dir_all(&physical_path).await
.map_err(|e| DomainError::new(
@@ -7,7 +7,7 @@ use crate::common::errors::Result;
use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::ports::trash_ports::TrashUseCase;
/// Servicio para la limpieza automática de elementos expirados en la papelera
/// Service for automatic cleanup of expired items in the trash
pub struct TrashCleanupService {
trash_service: Arc<dyn TrashUseCase>,
trash_repository: Arc<dyn TrashRepository>,
@@ -23,75 +23,75 @@ impl TrashCleanupService {
Self {
trash_service,
trash_repository,
cleanup_interval_hours: cleanup_interval_hours.max(1), // Mínimo 1 hora
cleanup_interval_hours: cleanup_interval_hours.max(1), // Minimum 1 hour
}
}
/// Inicia el trabajo de limpieza periódica
/// Starts the periodic cleanup job
#[instrument(skip(self))]
pub async fn start_cleanup_job(&self) {
let trash_repository = self.trash_repository.clone();
let trash_service = self.trash_service.clone();
let interval_hours = self.cleanup_interval_hours;
info!("Iniciando trabajo de limpieza de papelera con intervalo de {} horas", interval_hours);
info!("Starting trash cleanup job with interval of {} hours", interval_hours);
tokio::spawn(async move {
let interval_duration = Duration::from_secs(interval_hours * 60 * 60);
let mut interval = time::interval(interval_duration);
// Primera ejecución inmediata
// First immediate execution
Self::cleanup_expired_items(trash_repository.clone(), trash_service.clone()).await
.unwrap_or_else(|e| error!("Error en la limpieza inicial de la papelera: {:?}", e));
.unwrap_or_else(|e| error!("Error in initial trash cleanup: {:?}", e));
loop {
interval.tick().await;
debug!("Ejecutando tarea programada de limpieza de papelera");
debug!("Running scheduled trash cleanup task");
if let Err(e) = Self::cleanup_expired_items(
trash_repository.clone(),
trash_service.clone()
).await {
error!("Error en la limpieza programada de la papelera: {:?}", e);
error!("Error in scheduled trash cleanup: {:?}", e);
}
}
});
}
/// Limpia los elementos expirados en la papelera
/// Cleans up expired items in the trash
#[instrument(skip(trash_repository, trash_service))]
async fn cleanup_expired_items(
trash_repository: Arc<dyn TrashRepository>,
trash_service: Arc<dyn TrashUseCase>,
) -> Result<()> {
debug!("Comenzando limpieza de elementos expirados en la papelera");
debug!("Starting cleanup of expired items in the trash");
// Obtener todos los elementos expirados
// Get all expired items
let expired_items = trash_repository.get_expired_items().await?;
if expired_items.is_empty() {
debug!("No hay elementos expirados para limpiar");
debug!("No expired items to clean up");
return Ok(());
}
info!("Encontrados {} elementos expirados para eliminar", expired_items.len());
info!("Found {} expired items to delete", expired_items.len());
// Eliminar cada elemento expirado
// Delete each expired item
for item in expired_items {
let trash_id = item.id().to_string();
let user_id = item.user_id().to_string();
debug!("Eliminando elemento expirado: id={}, user={}", trash_id, user_id);
debug!("Deleting expired item: id={}, user={}", trash_id, user_id);
// Si falla una eliminación, continuar con las demás
// If a deletion fails, continue with the rest
if let Err(e) = trash_service.delete_permanently(&trash_id, &user_id).await {
error!("Error eliminando elemento expirado {}: {:?}", trash_id, e);
error!("Error deleting expired item {}: {:?}", trash_id, e);
} else {
debug!("Elemento expirado eliminado correctamente: {}", trash_id);
debug!("Expired item deleted successfully: {}", trash_id);
}
}
info!("Limpieza de papelera completada");
info!("Trash cleanup completed");
Ok(())
}
}
+47 -47
View File
@@ -13,47 +13,47 @@ use crate::{
};
use std::sync::Arc;
/// Error relacionado con la creación de archivos ZIP
/// Error related to ZIP file creation
#[derive(Debug, Error)]
pub enum ZipError {
#[error("Error de IO: {0}")]
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Error de ZIP: {0}")]
#[error("ZIP error: {0}")]
ZipError(#[from] zip::result::ZipError),
#[error("Error al leer el archivo: {0}")]
#[error("Error reading file: {0}")]
FileReadError(String),
#[error("Error al obtener contenido de carpeta: {0}")]
#[error("Error getting folder contents: {0}")]
FolderContentsError(String),
#[error("Carpeta no encontrada: {0}")]
#[error("Folder not found: {0}")]
FolderNotFound(String),
}
// Implementar From<ZipError> para DomainError para permitir el uso de ?
// Implement From<ZipError> for DomainError to allow the use of ?
impl From<ZipError> for DomainError {
fn from(err: ZipError) -> Self {
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 {
fn from(err: zip::result::ZipError) -> Self {
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
}
}
/// Servicio para crear archivos ZIP
/// Service for creating ZIP files
pub struct ZipService {
file_service: Arc<dyn FileRetrievalUseCase>,
folder_service: Arc<dyn FolderUseCase>,
}
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 {
Self {
file_service,
@@ -61,33 +61,33 @@ impl ZipService {
}
}
/// Crea un archivo ZIP con el contenido de una carpeta y todas sus subcarpetas
/// Retorna los bytes del ZIP
/// Creates a ZIP file with the contents of a folder and all its subfolders
/// Returns the ZIP bytes
pub async fn create_folder_zip(&self, folder_id: &str, folder_name: &str) -> Result<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 {
Ok(folder) => folder,
Err(e) => {
error!("Error al obtener carpeta {}: {}", folder_id, e);
error!("Error getting folder {}: {}", folder_id, e);
return Err(ZipError::FolderNotFound(folder_id.to_string()).into());
}
};
// Crear un buffer en memoria para el ZIP
// Create an in-memory buffer for the ZIP
let buf = Cursor::new(Vec::new());
let mut zip = ZipWriter::new(buf);
// Establecer opciones de compresión
// Set compression options
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
// Objeto para seguir las carpetas procesadas y evitar ciclos
// Object to track processed folders and avoid cycles
let mut processed_folders = std::collections::HashSet::new();
// Procesamos la carpeta raíz y construimos el ZIP
// Process the root folder and build the ZIP
self.process_folder_recursively(
&mut zip,
&folder,
@@ -96,20 +96,20 @@ impl ZipService {
&mut processed_folders
).await?;
// Finalizar el ZIP y obtener los bytes
// Finalize the ZIP and get the bytes
let mut zip_buf = zip.finish()?;
let mut bytes = Vec::new();
match zip_buf.read_to_end(&mut bytes) {
Ok(_) => Ok(bytes),
Err(e) => {
error!("Error al leer ZIP finalizado: {}", e);
error!("Error reading finalized ZIP: {}", e);
Err(ZipError::IoError(e).into())
}
}
}
// Implementación alternativa para evitar recursión en async
// Alternative implementation to avoid recursion in async
async fn process_folder_recursively(
&self,
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
@@ -118,63 +118,63 @@ impl ZipService {
options: &SimpleFileOptions,
processed_folders: &mut std::collections::HashSet<String>
) -> Result<()> {
// Estructura para representar el trabajo pendiente
// Structure to represent pending work
struct PendingFolder {
folder: FolderDto,
path: String,
}
// Cola de trabajo para procesamiento iterativo
// Work queue for iterative processing
let mut work_queue = vec![PendingFolder {
folder: folder.clone(),
path: path.to_string(),
}];
// Procesar la cola mientras haya elementos
// Process the queue while there are elements
while let Some(current) = work_queue.pop() {
let folder_id = current.folder.id.to_string();
// Evitar ciclos
// Avoid cycles
if processed_folders.contains(&folder_id) {
continue;
}
processed_folders.insert(folder_id.clone());
// Crear la entrada de directorio en el ZIP
// Create the directory entry in the ZIP
let folder_path = format!("{}/", current.path);
match zip.add_directory(&folder_path, *options) {
Ok(_) => debug!("Carpeta agregada al ZIP: {}", folder_path),
Ok(_) => debug!("Folder added to ZIP: {}", folder_path),
Err(e) => {
warn!("No se pudo agregar carpeta al ZIP (puede que ya exista): {}", e);
// Continuamos aunque falle crear el directorio (podría estar duplicado)
warn!("Could not add folder to ZIP (it may already exist): {}", e);
// Continue even if creating the directory fails (it could be a duplicate)
}
}
// Agregar archivos de la carpeta al ZIP
// Add files from the folder to the ZIP
let files = match self.file_service.list_files(Some(&folder_id)).await {
Ok(files) => files,
Err(e) => {
error!("Error al listar archivos en carpeta {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error al listar archivos: {}", e)).into());
error!("Error listing files in folder {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error listing files: {}", e)).into());
}
};
// Agregar cada archivo al ZIP
// Add each file to the ZIP
for file in files {
self.add_file_to_zip(zip, &file, &folder_path, options).await?;
}
// Procesar subcarpetas
// Process subfolders
let subfolders = match self.folder_service.list_folders(Some(&folder_id)).await {
Ok(folders) => folders,
Err(e) => {
error!("Error al listar subcarpetas en {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error al listar subcarpetas: {}", e)).into());
error!("Error listing subfolders in {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error listing subfolders: {}", e)).into());
}
};
// Agregar subcarpetas a la cola
// Add subfolders to the queue
for subfolder in subfolders {
let subfolder_path = format!("{}/{}", current.path, subfolder.name);
work_queue.push(PendingFolder {
@@ -187,7 +187,7 @@ impl ZipService {
Ok(())
}
// Agrega un archivo al ZIP
// Adds a file to the ZIP
async fn add_file_to_zip(
&self,
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
@@ -196,34 +196,34 @@ impl ZipService {
options: &SimpleFileOptions,
) -> Result<()> {
let file_path = format!("{}{}", folder_path, file.name);
info!("Agregando archivo al ZIP: {}", file_path);
info!("Adding file to ZIP: {}", file_path);
// Obtener el contenido del archivo
// Get the file content
let file_id = file.id.to_string();
let content = match self.file_service.get_file_content(&file_id).await {
Ok(content) => content,
Err(e) => {
error!("Error al leer contenido del archivo {}: {}", file_id, e);
return Err(ZipError::FileReadError(format!("Error al leer archivo {}: {}", file_id, e)).into());
error!("Error reading file content {}: {}", file_id, e);
return Err(ZipError::FileReadError(format!("Error reading file {}: {}", file_id, e)).into());
}
};
// Escribir archivo al ZIP
// Write file to the ZIP
match zip.start_file_from_path(std::path::Path::new(&file_path), *options) {
Ok(_) => {
match zip.write_all(&content) {
Ok(_) => {
debug!("Archivo agregado al ZIP: {}", file_path);
debug!("File added to ZIP: {}", file_path);
Ok(())
},
Err(e) => {
error!("Error al escribir contenido del archivo {}: {}", file_path, e);
error!("Error writing file content {}: {}", file_path, e);
Err(ZipError::IoError(e).into())
}
}
},
Err(e) => {
error!("Error al iniciar archivo en ZIP {}: {}", file_path, e);
error!("Error starting file in ZIP {}: {}", file_path, e);
Err(ZipError::ZipError(e).into())
}
}
+29 -29
View File
@@ -15,7 +15,7 @@ use crate::application::dtos::user_dto::{
use crate::interfaces::errors::AppError;
pub fn auth_routes() -> Router<Arc<AppState>> {
// Rutas que NO requieren autenticación
// Routes that do NOT require authentication
let public_routes = Router::new()
.route("/register", post(register))
.route("/login", post(login))
@@ -27,14 +27,14 @@ pub fn auth_routes() -> Router<Arc<AppState>> {
.route("/oidc/callback", get(oidc_callback))
.route("/oidc/exchange", post(oidc_exchange));
// Rutas que SÍ requieren autenticación - usamos route_layer para aplicar middleware
// El middleware usará el state que se pase con .with_state() desde main.rs
// Routes that DO require authentication - we use route_layer to apply middleware
// The middleware will use the state passed with .with_state() from main.rs
let protected_routes = Router::new()
.route("/me", get(get_current_user))
.route("/change-password", put(change_password))
.route("/logout", post(logout));
// Combinar rutas públicas y protegidas
// Combine public and protected routes
public_routes.merge(protected_routes)
}
@@ -53,7 +53,7 @@ async fn register(
},
None => {
tracing::error!("Auth service not configured");
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
return Err(AppError::internal_error("Authentication service not configured"));
}
};
@@ -153,7 +153,7 @@ async fn login(
},
None => {
tracing::error!("Auth service not configured");
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
return Err(AppError::internal_error("Authentication service not configured"));
}
};
@@ -174,7 +174,7 @@ async fn login(
// Ensure the response has the expected fields
if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() {
tracing::error!("Login response contains empty tokens for user: {}", dto.username);
return Err(AppError::internal_error("Error generando tokens de autenticación"));
return Err(AppError::internal_error("Error generating authentication tokens"));
}
Ok((StatusCode::OK, Json(auth_response)))
@@ -198,7 +198,7 @@ async fn refresh_token(
// Normal process for real tokens
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
let auth_response = auth_service.auth_application_service.refresh_token(dto).await?;
@@ -214,37 +214,37 @@ async fn get_current_user(
) -> Result<impl IntoResponse, AppError> {
// Normal process for all users
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Extraer y validar el token directamente
// Extract and validate the token directly
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
// Validar el token y obtener claims
// Validate the token and get claims
let claims = auth_service.token_service.validate_token(token)
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
let user_id = claims.sub;
// Primero, actualizar las estadísticas de uso de almacenamiento
// IMPORTANTE: Esperamos el cálculo para devolver datos actualizados
// First, update the storage usage statistics
// IMPORTANT: We await the calculation to return updated data
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
// Calcular storage de forma síncrona (esperamos el resultado)
// Calculate storage synchronously (we await the result)
match storage_usage_service.update_user_storage_usage(&user_id).await {
Ok(usage) => {
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
},
Err(e) => {
// Solo log de warning, no fallar la petición completa
// Only log a warning, don't fail the entire request
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
}
}
}
// Ahora obtener los datos del usuario CON el almacenamiento actualizado
// Now get the user data WITH the updated storage
let user = auth_service.auth_application_service.get_user_by_id(&user_id).await?;
Ok((StatusCode::OK, Json(user)))
@@ -256,18 +256,18 @@ async fn change_password(
Json(dto): Json<ChangePasswordDto>,
) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Extraer y validar el token directamente
// Extract and validate the token directly
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
// Validar el token y obtener claims
// Validate the token and get claims
let claims = auth_service.token_service.validate_token(token)
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
auth_service.auth_application_service.change_password(&claims.sub, dto).await?;
@@ -279,18 +279,18 @@ async fn logout(
headers: HeaderMap,
) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Extraer y validar el token directamente
// Extract and validate the token directly
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
// Validar el token y obtener claims
// Validate the token and get claims
let claims = auth_service.token_service.validate_token(token)
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
// Use access token for logout (we don't have refresh token in headers)
auth_service.auth_application_service.logout(&claims.sub, token).await?;
@@ -314,7 +314,7 @@ async fn get_system_status(
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Count admin users to determine if system is initialized
let admin_count = auth_service.auth_application_service.count_admin_users().await
+84 -84
View File
@@ -13,86 +13,86 @@ use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
use crate::interfaces::api::handlers::ApiResult;
/// Estado compartido para el handler de batch
/// Shared state for the batch handler
#[derive(Clone)]
pub struct BatchHandlerState {
pub batch_service: Arc<BatchOperationService>,
}
/// DTO para las solicitudes de operaciones en lote de archivos
/// DTO for batch file operation requests
#[derive(Debug, Deserialize)]
pub struct BatchFileOperationRequest {
/// IDs de los archivos a procesar
/// IDs of the files to process
pub file_ids: Vec<String>,
/// ID de la carpeta destino (opcional)
/// Target folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")]
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)]
pub struct BatchFolderOperationRequest {
/// IDs de las carpetas a procesar
/// IDs of the folders to process
pub folder_ids: Vec<String>,
/// Si la operación debe ser recursiva
/// Whether the operation should be recursive
#[serde(default)]
pub recursive: bool,
/// ID de la carpeta destino (opcional)
/// Target folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>,
}
/// DTO para las solicitudes de creación en lote de carpetas
/// DTO for batch folder creation requests
#[derive(Debug, Deserialize)]
pub struct BatchCreateFoldersRequest {
/// Detalles de las carpetas a crear
/// Details of the folders to create
pub folders: Vec<CreateFolderDetail>,
}
/// Detalle para creación de una carpeta
/// Detail for folder creation
#[derive(Debug, Deserialize)]
pub struct CreateFolderDetail {
/// Nombre de la carpeta
/// Folder name
pub name: String,
/// ID de la carpeta padre (opcional)
/// Parent folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}
/// DTO para los resultados de operaciones en lote
/// DTO for batch operation results
#[derive(Debug, Serialize)]
pub struct BatchOperationResponse<T> {
/// Entidades procesadas exitosamente
/// Successfully processed entities
pub successful: Vec<T>,
/// Operaciones fallidas con sus mensajes de error
/// Failed operations with their error messages
pub failed: Vec<FailedOperation>,
/// Estadísticas de la operación
/// Operation statistics
pub stats: BatchOperationStats,
}
/// Operación fallida en un lote
/// Failed operation in a batch
#[derive(Debug, Serialize)]
pub struct FailedOperation {
/// Identificador de la entidad que falló
/// Identifier of the entity that failed
pub id: String,
/// Mensaje de error
/// Error message
pub error: String,
}
/// Estadísticas de una operación por lotes
/// Statistics for a batch operation
#[derive(Debug, Serialize)]
pub struct BatchOperationStats {
/// Número total de operaciones
/// Total number of operations
pub total: usize,
/// Número de operaciones exitosas
/// Number of successful operations
pub successful: usize,
/// Número de operaciones fallidas
/// Number of failed operations
pub failed: usize,
/// Tiempo total de ejecución en milisegundos
/// Total execution time in milliseconds
pub execution_time_ms: u128,
}
/// Convierte BatchStats del dominio a DTO
/// Converts domain BatchStats to DTO
impl From<BatchStats> for BatchOperationStats {
fn from(stats: BatchStats) -> 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>
where
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(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -139,35 +139,35 @@ pub async fn move_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.move_files(request.file_ids, request.target_folder_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<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 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para copiar múltiples archivos en lote
/// Handler for copying multiple files in batch
pub async fn copy_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -177,35 +177,35 @@ pub async fn copy_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.copy_files(request.file_ids, request.target_folder_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<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 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para eliminar múltiples archivos en lote
/// Handler for deleting multiple files in batch
pub async fn delete_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -215,13 +215,13 @@ pub async fn delete_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.delete_files(request.file_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Crear respuesta personalizada para IDs de string
// Create custom response for string IDs
let response = BatchOperationResponse {
successful: result.successful,
failed: result.failed.into_iter()
@@ -230,26 +230,26 @@ pub async fn delete_files_batch(
stats: result.stats.into(),
};
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para eliminar múltiples carpetas en lote
/// Handler for deleting multiple folders in batch
pub async fn delete_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
// Verify there are folders to process
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -259,13 +259,13 @@ pub async fn delete_folders_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.delete_folders(request.folder_ids, request.recursive)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Crear respuesta personalizada para IDs de string
// Create custom response for string IDs
let response = BatchOperationResponse {
successful: result.successful,
failed: result.failed.into_iter()
@@ -274,26 +274,26 @@ pub async fn delete_folders_batch(
stats: result.stats.into(),
};
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para crear múltiples carpetas en lote
/// Handler for creating multiple folders in batch
pub async fn create_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchCreateFoldersRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
// Verify there are folders to process
if request.folders.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -303,41 +303,41 @@ pub async fn create_folders_batch(
).into_response());
}
// Transformar el formato para el servicio
// Transform the format for the service
let folders = request.folders
.into_iter()
.map(|detail| (detail.name, detail.parent_id))
.collect();
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.create_folders(folders)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<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 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::CREATED // Todas exitosas
StatusCode::CREATED // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para obtener múltiples archivos en lote
/// Handler for getting multiple files in batch
pub async fn get_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -347,35 +347,35 @@ pub async fn get_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.get_multiple_files(request.file_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<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 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para obtener múltiples carpetas en lote
/// Handler for getting multiple folders in batch
pub async fn get_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
// Verify there are folders to process
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -385,24 +385,24 @@ pub async fn get_folders_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.get_multiple_folders(request.folder_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<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 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
+24 -24
View File
@@ -11,14 +11,14 @@ use tracing::{error, info};
use crate::application::ports::recent_ports::RecentItemsUseCase;
use crate::interfaces::middleware::auth::AuthUser;
/// Parámetros de consulta para obtener elementos recientes
/// Query parameters for getting recent items
#[derive(Deserialize)]
pub struct GetRecentParams {
#[serde(default)]
limit: Option<i32>,
}
/// Obtener elementos recientes del usuario
/// Get user's recent items
pub async fn get_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -28,22 +28,22 @@ pub async fn get_recent_items(
match recent_service.get_recent_items(user_id, params.limit).await {
Ok(items) => {
info!("Recuperados {} elementos recientes para usuario", items.len());
info!("Retrieved {} recent items for user", items.len());
(StatusCode::OK, Json(items)).into_response()
},
Err(err) => {
error!("Error al recuperar elementos recientes: {}", err);
error!("Error retrieving recent items: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al recuperar elementos recientes: {}", err)
"error": format!("Failed to retrieve recent items: {}", err)
}))
).into_response()
}
}
}
/// Registrar acceso a un elemento
/// Record access to an item
pub async fn record_item_access(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -51,39 +51,39 @@ pub async fn record_item_access(
) -> impl IntoResponse {
let user_id = &auth_user.id;
// Validar tipo de elemento
// Validate item type
if item_type != "file" && item_type != "folder" {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "El tipo de elemento debe ser 'file' o 'folder'"
"error": "Item type must be 'file' or 'folder'"
}))
).into_response();
}
match recent_service.record_item_access(user_id, &item_id, &item_type).await {
Ok(_) => {
info!("Registrado acceso a {} '{}' en recientes", item_type, item_id);
info!("Recorded access to {} '{}' in recents", item_type, item_id);
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Acceso registrado correctamente"
"message": "Access recorded successfully"
}))
).into_response()
},
Err(err) => {
error!("Error al registrar acceso en recientes: {}", err);
error!("Error recording access in recents: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al registrar acceso: {}", err)
"error": format!("Failed to record access: {}", err)
}))
).into_response()
}
}
}
/// Eliminar un elemento de recientes
/// Remove an item from recents
pub async fn remove_from_recent(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -94,36 +94,36 @@ pub async fn remove_from_recent(
match recent_service.remove_from_recent(user_id, &item_id, &item_type).await {
Ok(removed) => {
if removed {
info!("Eliminado {} '{}' de recientes", item_type, item_id);
info!("Removed {} '{}' from recents", item_type, item_id);
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Elemento eliminado de recientes"
"message": "Item removed from recents"
}))
).into_response()
} else {
info!("Elemento {} '{}' no estaba en recientes", item_type, item_id);
info!("Item {} '{}' was not in recents", item_type, item_id);
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"message": "Elemento no estaba en recientes"
"message": "Item was not in recents"
}))
).into_response()
}
},
Err(err) => {
error!("Error al eliminar de recientes: {}", err);
error!("Error removing from recents: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al eliminar de recientes: {}", err)
"error": format!("Failed to remove from recents: {}", err)
}))
).into_response()
}
}
}
/// Limpiar todos los elementos recientes
/// Clear all recent items
pub async fn clear_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -132,20 +132,20 @@ pub async fn clear_recent_items(
match recent_service.clear_recent_items(user_id).await {
Ok(_) => {
info!("Limpiados todos los elementos recientes para usuario");
info!("Cleared all recent items for user");
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Elementos recientes limpiados correctamente"
"message": "Recent items cleared successfully"
}))
).into_response()
},
Err(err) => {
error!("Error al limpiar elementos recientes: {}", err);
error!("Error clearing recent items: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al limpiar elementos recientes: {}", err)
"error": format!("Failed to clear recent items: {}", err)
}))
).into_response()
}
+51 -51
View File
@@ -10,34 +10,34 @@ use crate::application::dtos::search_dto::SearchCriteriaDto;
use crate::common::di::AppState;
/**
* Manejador para las operaciones de búsqueda a través de la API.
* Handler for search operations through the API.
*
* Este manejador expone endpoints relacionados con la funcionalidad de búsqueda,
* permitiendo a los usuarios buscar archivos y carpetas usando diversos criterios.
* This handler exposes endpoints related to search functionality,
* allowing users to search for files and folders using various criteria.
*/
pub struct SearchHandler;
impl SearchHandler {
/**
* Realiza una búsqueda basada en los criterios proporcionados como parámetros de consulta.
* Performs a search based on the criteria provided as query parameters.
*
* Este endpoint permite búsquedas simples directamente con parámetros URL.
* This endpoint allows simple searches directly with URL parameters.
*
* @param state Estado de la aplicación con servicios
* @param query_params Parámetros de búsqueda como query string
* @return Respuesta HTTP con los resultados de la búsqueda
* @param state Application state with services
* @param query_params Search parameters as query string
* @return HTTP response with the search results
*/
pub async fn search_files_get(
State(state): State<AppState>,
Query(params): Query<SearchParams>,
) -> impl IntoResponse {
info!("API: Búsqueda de archivos con parámetros: {:?}", params);
info!("API: File search with parameters: {:?}", params);
// Extraer el servicio de búsqueda o devolver error si no está disponible
// Extract the search service or return error if not available
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
error!("Search service not available");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
@@ -47,7 +47,7 @@ impl SearchHandler {
}
};
// Convertir parámetros de búsqueda a DTO
// Convert search parameters to DTO
let search_criteria = SearchCriteriaDto {
name_contains: params.query,
file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()),
@@ -63,15 +63,15 @@ impl SearchHandler {
offset: params.offset.unwrap_or(0),
};
// Realizar la búsqueda
// Perform the search
match search_service.search(search_criteria).await {
Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
info!("Search completed, {} files and {} folders found",
results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response()
},
Err(err) => {
error!("Error en búsqueda: {}", err);
error!("Search error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
@@ -83,26 +83,26 @@ impl SearchHandler {
}
/**
* Realiza una búsqueda avanzada basada en un objeto de criterios JSON completo.
* Performs an advanced search based on a complete JSON criteria object.
*
* Este endpoint permite búsquedas más complejas con todos los criterios posibles
* proporcionados en el cuerpo de la solicitud.
* This endpoint allows more complex searches with all possible criteria
* provided in the request body.
*
* @param state Estado de la aplicación con servicios
* @param criteria Criterios de búsqueda completos
* @return Respuesta HTTP con los resultados de la búsqueda
* @param state Application state with services
* @param criteria Complete search criteria
* @return HTTP response with the search results
*/
pub async fn search_files_post(
State(state): State<AppState>,
Json(criteria): Json<SearchCriteriaDto>,
) -> impl IntoResponse {
info!("API: Búsqueda avanzada de archivos");
info!("API: Advanced file search");
// Extraer el servicio de búsqueda o devolver error si no está disponible
// Extract the search service or return error if not available
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
error!("Search service not available");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
@@ -112,15 +112,15 @@ impl SearchHandler {
}
};
// Realizar la búsqueda
// Perform the search
match search_service.search(criteria).await {
Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
info!("Search completed, {} files and {} folders found",
results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response()
},
Err(err) => {
error!("Error en búsqueda: {}", err);
error!("Search error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
@@ -132,24 +132,24 @@ impl SearchHandler {
}
/**
* Limpia la caché de resultados de búsqueda.
* Clears the search results cache.
*
* Este endpoint es útil para forzar búsquedas frescas después de cambios
* significativos en el sistema de archivos.
* This endpoint is useful for forcing fresh searches after significant
* changes in the file system.
*
* @param state Estado de la aplicación con servicios
* @return Respuesta HTTP indicando éxito o error
* @param state Application state with services
* @return HTTP response indicating success or error
*/
pub async fn clear_search_cache(
State(state): State<AppState>,
) -> impl IntoResponse {
info!("API: Limpiando caché de búsqueda");
info!("API: Clearing search cache");
// Extraer el servicio de búsqueda o devolver error si no está disponible
// Extract the search service or return error if not available
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
error!("Search service not available");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
@@ -159,10 +159,10 @@ impl SearchHandler {
}
};
// Limpiar la caché
// Clear the cache
match search_service.clear_search_cache().await {
Ok(_) => {
info!("Caché de búsqueda limpiada correctamente");
info!("Search cache cleared successfully");
(
StatusCode::OK,
Json(json!({
@@ -171,7 +171,7 @@ impl SearchHandler {
).into_response()
},
Err(err) => {
error!("Error al limpiar caché de búsqueda: {}", err);
error!("Error clearing search cache: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
@@ -183,43 +183,43 @@ impl SearchHandler {
}
}
/// Parámetros de búsqueda para el endpoint GET
/// Search parameters for the GET endpoint
#[derive(Debug, serde::Deserialize)]
pub struct SearchParams {
/// Texto a buscar en nombres de archivos y carpetas
/// Text to search for in file and folder names
pub query: Option<String>,
/// Filtro por tipos de archivo (extensiones separadas por comas)
/// Filter by file types (comma-separated extensions)
#[serde(rename = "type")]
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>,
/// Filtrar elementos creados antes de esta fecha (timestamp)
/// Filter items created before this date (timestamp)
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>,
/// Filtrar elementos modificados antes de esta fecha (timestamp)
/// Filter items modified before this date (timestamp)
pub modified_before: Option<u64>,
/// Tamaño mínimo en bytes
/// Minimum size in bytes
pub min_size: Option<u64>,
/// Tamaño máximo en bytes
/// Maximum size in bytes
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>,
/// Búsqueda recursiva en subcarpetas
/// Recursive search in subfolders
pub recursive: Option<bool>,
/// Límite de resultados para paginación
/// Result limit for pagination
pub limit: Option<usize>,
/// Desplazamiento para paginación
/// Offset for pagination
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::interfaces::middleware::auth::AuthUser;
/// Obtiene todos los elementos en la papelera para el usuario actual
/// Gets all items in the trash for the current user
#[instrument(skip_all)]
pub async fn get_trash_items(
State(state): State<AppState>,
@@ -19,7 +19,7 @@ pub async fn get_trash_items(
// privilege escalation attacks.
let effective_user = auth_user.id.clone();
debug!("Solicitud para listar elementos en papelera para usuario {}", effective_user);
debug!("Request to list trash items for user {}", effective_user);
let trash_service = match state.trash_service.as_ref() {
Some(service) => service,
@@ -34,11 +34,11 @@ pub async fn get_trash_items(
match result {
Ok(items) => {
debug!("Encontrados {} elementos en la papelera", items.len());
debug!("Found {} items in trash", items.len());
(StatusCode::OK, Json(json!(items)))
},
Err(e) => {
error!("Error al obtener elementos de la papelera: {:?}", e);
error!("Error retrieving trash items: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error retrieving trash items: {}", e)
})))
@@ -46,14 +46,14 @@ pub async fn get_trash_items(
}
}
/// Mueve un elemento (archivo o carpeta) a la papelera (función genérica, no usada directamente en rutas)
/// Moves an item (file or folder) to the trash (generic function, not used directly in routes)
#[instrument(skip_all)]
pub async fn move_to_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path((item_type, item_id)): Path<(String, String)>,
) -> (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);
let trash_service = match state.trash_service.as_ref() {
@@ -68,14 +68,14 @@ pub async fn move_to_trash(
match result {
Ok(_) => {
debug!("Elemento movido a papelera con éxito");
debug!("Item moved to trash successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Item moved to trash successfully"
})))
},
Err(e) => {
error!("Error al mover elemento a papelera: {:?}", e);
error!("Error moving item to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving item to trash: {}", e)
})))
@@ -83,14 +83,14 @@ pub async fn move_to_trash(
}
}
/// Mueve un archivo a la papelera
/// Moves a file to the trash
#[instrument(skip_all)]
pub async fn move_file_to_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path(item_id): Path<String>,
) -> (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);
let trash_service = match state.trash_service.as_ref() {
@@ -102,19 +102,19 @@ pub async fn move_file_to_trash(
}
};
// Especificar que es un archivo
// Specify that it is a file
let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await;
match result {
Ok(_) => {
debug!("Archivo movido a papelera con éxito");
debug!("File moved to trash successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "File moved to trash successfully"
})))
},
Err(e) => {
error!("Error al mover archivo a papelera: {:?}", e);
error!("Error moving file to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving file to trash: {}", e)
})))
@@ -122,14 +122,14 @@ pub async fn move_file_to_trash(
}
}
/// Mueve una carpeta a la papelera
/// Moves a folder to the trash
#[instrument(skip_all)]
pub async fn move_folder_to_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path(item_id): Path<String>,
) -> (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);
let trash_service = match state.trash_service.as_ref() {
@@ -141,19 +141,19 @@ pub async fn move_folder_to_trash(
}
};
// Especificar que es una carpeta
// Specify that it is a folder
let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await;
match result {
Ok(_) => {
debug!("Carpeta movida a papelera con éxito");
debug!("Folder moved to trash successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Folder moved to trash successfully"
})))
},
Err(e) => {
error!("Error al mover carpeta a papelera: {:?}", e);
error!("Error moving folder to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving folder to trash: {}", e)
})))
@@ -161,14 +161,14 @@ pub async fn move_folder_to_trash(
}
}
/// Restaura un elemento desde la papelera a su ubicación original
/// Restores an item from the trash to its original location
#[instrument(skip_all)]
pub async fn restore_from_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path(trash_id): Path<String>,
) -> (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() {
Some(service) => service,
@@ -182,7 +182,7 @@ pub async fn restore_from_trash(
match result {
Ok(_) => {
debug!("Elemento restaurado con éxito");
debug!("Item restored successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Item restored successfully"
@@ -199,7 +199,7 @@ pub async fn restore_from_trash(
})));
}
error!("Error al restaurar elemento de papelera: {:?}", e);
error!("Error restoring item from trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error restoring item from trash: {}", e)
})))
@@ -207,14 +207,14 @@ pub async fn restore_from_trash(
}
}
/// Elimina permanentemente un elemento de la papelera
/// Permanently deletes an item from the trash
#[instrument(skip_all)]
pub async fn delete_permanently(
State(state): State<AppState>,
auth_user: AuthUser,
Path(trash_id): Path<String>,
) -> (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() {
Some(service) => service,
@@ -228,7 +228,7 @@ pub async fn delete_permanently(
match result {
Ok(_) => {
debug!("Elemento eliminado permanentemente");
debug!("Item permanently deleted");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Item deleted permanently"
@@ -245,7 +245,7 @@ pub async fn delete_permanently(
})));
}
error!("Error al eliminar permanentemente elemento: {:?}", e);
error!("Error permanently deleting item: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error deleting item permanently: {}", e)
})))
@@ -253,13 +253,13 @@ pub async fn delete_permanently(
}
}
/// Vacía la papelera completamente para el usuario actual
/// Empties the trash completely for the current user
#[instrument(skip_all)]
pub async fn empty_trash(
State(state): State<AppState>,
auth_user: AuthUser,
) -> (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() {
Some(service) => service,
@@ -273,14 +273,14 @@ pub async fn empty_trash(
match result {
Ok(_) => {
debug!("Papelera vaciada con éxito");
debug!("Trash emptied successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Trash emptied successfully"
})))
},
Err(e) => {
error!("Error al vaciar papelera: {:?}", e);
error!("Error emptying trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error emptying trash: {}", e)
})))
+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 recent_service = app_state.recent_service.clone();
// Inicializar el servicio de operaciones por lotes
// Initialize the batch operations service
let batch_service = Arc::new(BatchOperationService::default(
file_retrieval_service.clone(),
file_management_service.clone(),
folder_service.clone()
));
// Crear estado para el manejador de operaciones por lotes
// Create state for the batch operations handler
let batch_handler_state = BatchHandlerState {
batch_service: batch_service.clone(),
};
@@ -154,14 +154,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
// Merge the routers
let files_router = basic_file_router.merge(file_operations_router);
// Crear rutas para operaciones por lotes
// Create routes for batch operations
let batch_router = Router::new()
// Operaciones de archivos
// File operations
.route("/files/move", post(batch_handler::move_files_batch))
.route("/files/copy", post(batch_handler::copy_files_batch))
.route("/files/delete", post(batch_handler::delete_files_batch))
.route("/files/get", post(batch_handler::get_files_batch))
// Operaciones de carpetas
// Folder operations
.route("/folders/delete", post(batch_handler::delete_folders_batch))
.route("/folders/create", post(batch_handler::create_folders_batch))
.route("/folders/get", post(batch_handler::get_folders_batch))
@@ -183,7 +183,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
Router::new()
};
// Implementaciones directas de handlers para compartir, sin depender de ShareHandler
// Direct handler implementations for sharing, without depending on ShareHandler
// Create routes for shared resources management (requires auth)
let share_router = if let Some(share_service) = share_service.clone() {
+31 -31
View File
@@ -11,24 +11,24 @@ use crate::common::di::AppState;
// Re-export CurrentUser from application layer for use in handlers
pub use crate::application::dtos::user_dto::CurrentUser;
// Estructura para usar en extractores de Axum
// Structure for use in Axum extractors
#[derive(Clone, Debug)]
pub struct AuthUser {
pub id: String,
pub username: String,
}
/// Extractor reutilizable que obtiene el user_id del usuario autenticado.
/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware.
/// Reusable extractor that gets the user_id of the authenticated user.
/// Automatically extracted from the `CurrentUser` inserted by the auth middleware.
///
/// Uso en handlers:
/// Usage in handlers:
/// ```ignore
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
/// ```
#[derive(Clone, Debug)]
pub struct CurrentUserId(pub String);
// Implementar FromRequestParts para AuthUser — permite usar `auth_user: AuthUser` en handlers
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
@@ -47,7 +47,7 @@ where
}
}
// Implementar FromRequestParts para CurrentUserId — extractor ligero solo para el user_id
// Implement FromRequestParts for CurrentUserId — lightweight extractor for user_id only
impl<S> FromRequestParts<S> for CurrentUserId
where
S: Send + Sync,
@@ -63,37 +63,37 @@ where
}
}
// Error para las operaciones de autenticación
// Error for authentication operations
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Token no proporcionado")]
#[error("Token not provided")]
TokenNotProvided,
#[error("Token inválido: {0}")]
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Token expirado")]
#[error("Token expired")]
TokenExpired,
#[error("Usuario no encontrado")]
#[error("User not found")]
UserNotFound,
#[error("Acceso denegado: {0}")]
#[error("Access denied: {0}")]
AccessDenied(String),
#[error("Servicio de autenticación no disponible")]
#[error("Authentication service unavailable")]
AuthServiceUnavailable,
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token no proporcionado".to_string()),
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token not provided".to_string()),
AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()),
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()),
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired".to_string()),
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "User not found".to_string()),
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Servicio de autenticación no disponible".to_string()),
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Authentication service unavailable".to_string()),
};
let body = axum::Json(serde_json::json!({
@@ -104,24 +104,24 @@ impl IntoResponse for AuthError {
}
}
/// Middleware de autenticación seguro.
/// Secure authentication middleware.
///
/// Valida el token JWT contra el servicio de autenticación configurado.
/// No acepta bypasses, tokens mock, ni parámetros de URL para saltar validación.
/// Validates the JWT token against the configured authentication service.
/// Does not accept bypasses, mock tokens, or URL parameters to skip validation.
pub async fn auth_middleware(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, AuthError> {
// Extraer el token Bearer del header Authorization
// Extract the Bearer token from the Authorization header
let token_str = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(AuthError::TokenNotProvided)?;
// Validar que el token no esté vacío
// Validate that the token is not empty
let token_str = token_str.trim();
if token_str.is_empty() {
return Err(AuthError::TokenNotProvided);
@@ -129,7 +129,7 @@ pub async fn auth_middleware(
tracing::debug!("Processing authentication token");
// Validar el token usando el servicio de autenticación
// Validate the token using the authentication service
if let Some(auth_service) = state.auth_service.as_ref() {
let token_service = &auth_service.token_service;
match token_service.validate_token(token_str) {
@@ -146,25 +146,25 @@ pub async fn auth_middleware(
},
Err(e) => {
tracing::warn!("Token validation failed: {}", e);
return Err(AuthError::InvalidToken(format!("Token inválido: {}", e)));
return Err(AuthError::InvalidToken(format!("Invalid token: {}", e)));
}
}
}
// Si no hay servicio de autenticación disponible, denegar acceso
// If no authentication service is available, deny access
tracing::error!("Auth middleware invoked but auth service is not configured");
Err(AuthError::AuthServiceUnavailable)
}
/// Middleware para verificar que el usuario autenticado tiene rol de administrador.
/// Middleware to verify that the authenticated user has an admin role.
///
/// Debe aplicarse DESPUÉS del auth_middleware, ya que depende de que
/// `CurrentUser` esté presente en las extensiones de la request.
/// Must be applied AFTER auth_middleware, as it depends on
/// `CurrentUser` being present in the request extensions.
pub async fn require_admin(
request: Request,
next: Next,
) -> Response {
// Obtener el CurrentUser insertado por auth_middleware
// Get the CurrentUser inserted by auth_middleware
if let Some(current_user) = request.extensions().get::<CurrentUser>() {
if current_user.role == "admin" {
tracing::debug!("Admin access granted for user: {}", current_user.username);
@@ -175,7 +175,7 @@ pub async fn require_admin(
tracing::warn!("Admin check failed: no authenticated user in request");
}
// Acceso denegado
let error = AuthError::AccessDenied("Se requiere rol de administrador".to_string());
// Access denied
let error = AuthError::AccessDenied("Admin role required".to_string());
error.into_response()
}
+82 -82
View File
@@ -17,39 +17,39 @@ use std::future::Future;
use bytes::Bytes;
use tracing::{debug, info};
const MAX_CACHE_ENTRIES: usize = 1000; // Máximo número de entradas en caché
const DEFAULT_MAX_AGE: u64 = 60; // Tiempo de vida por defecto en segundos
const MAX_CACHE_ENTRIES: usize = 1000; // Maximum number of cache entries
const DEFAULT_MAX_AGE: u64 = 60; // Default time-to-live in seconds
// Definición de tipos para mayor claridad
// Type definitions for clarity
type CacheKey = String;
type EntityTag = String;
/// Un valor almacenado en caché
/// A cached value
#[derive(Clone)]
struct CacheEntry {
/// El ETag calculado para este valor
/// The ETag calculated for this value
etag: EntityTag,
/// Los datos serializados en bytes
/// The serialized data in bytes
data: Option<Bytes>,
/// Las cabeceras originales
/// The original headers
headers: HeaderMap,
/// Timestamp de cuando fue almacenado
/// Timestamp of when it was stored
timestamp: SystemTime,
/// Tiempo de vida en segundos
/// Time-to-live in seconds
max_age: u64,
}
/// Cache para respuestas HTTP con soporte para ETag
/// Cache for HTTP responses with ETag support
#[derive(Clone)]
pub struct HttpCache {
/// Almacenamiento de entradas en caché
/// Cache entry storage
cache: Arc<Mutex<HashMap<CacheKey, CacheEntry>>>,
/// Tiempo de vida por defecto para las entradas
/// Default time-to-live for entries
default_max_age: u64,
}
impl HttpCache {
/// Crea una nueva instancia del caché
/// Creates a new cache instance
pub fn new() -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
@@ -57,7 +57,7 @@ impl HttpCache {
}
}
/// Crea una nueva instancia con un tiempo de vida especificado
/// Creates a new instance with a specified time-to-live
pub fn with_max_age(max_age: u64) -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
@@ -65,12 +65,12 @@ impl HttpCache {
}
}
/// Obtiene estadísticas del caché
/// Gets cache statistics
pub fn stats(&self) -> (usize, usize) {
let lock = self.cache.lock().unwrap();
let total = lock.len();
// Contar entradas válidas
// Count valid entries
let _now = SystemTime::now();
let valid = lock.values().filter(|entry| {
match entry.timestamp.elapsed() {
@@ -82,12 +82,12 @@ impl HttpCache {
(total, valid)
}
/// Limpia entradas expiradas
/// Cleans up expired entries
pub fn cleanup(&self) -> usize {
let mut lock = self.cache.lock().unwrap();
let initial_count = lock.len();
// Eliminar entradas expiradas
// Remove expired entries
let _now = SystemTime::now();
lock.retain(|_, entry| {
match entry.timestamp.elapsed() {
@@ -102,18 +102,18 @@ impl HttpCache {
removed
}
/// Establece una entrada en el caché
/// Sets an entry in the cache
fn set(&self, key: &str, etag: EntityTag, data: Option<Bytes>, headers: HeaderMap, max_age: Option<u64>) {
let mut lock = self.cache.lock().unwrap();
// Aplicar política de eviction si el caché está lleno
// Apply eviction policy if the cache is full
if lock.len() >= MAX_CACHE_ENTRIES {
debug!("Cache full, removing oldest entries");
// Eliminar el 10% de las entradas más antiguas
// Remove the oldest 10% of entries
self.evict_oldest(&mut lock, MAX_CACHE_ENTRIES / 10);
}
// Almacenar la nueva entrada
// Store the new entry
lock.insert(key.to_string(), CacheEntry {
etag,
data,
@@ -123,30 +123,30 @@ impl HttpCache {
});
}
/// Elimina las entradas más antiguas del caché
/// Removes the oldest entries from the cache
fn evict_oldest(&self, cache: &mut HashMap<CacheKey, CacheEntry>, count: usize) {
// Ordenar por timestamp
// Sort by timestamp
let mut entries: Vec<(CacheKey, SystemTime)> = cache
.iter()
.map(|(key, entry)| (key.clone(), entry.timestamp))
.collect();
// Ordenar por timestamp (más antiguo primero)
// Sort by timestamp (oldest first)
entries.sort_by(|a, b| a.1.cmp(&b.1));
// Eliminar las entradas más antiguas
// Remove the oldest entries
for (key, _) in entries.iter().take(count) {
cache.remove(key);
}
}
/// Obtiene una entrada del caché
/// Gets an entry from the cache
fn get(&self, key: &str) -> Option<CacheEntry> {
let lock = self.cache.lock().unwrap();
// Buscar la entrada
// Look up the entry
if let Some(entry) = lock.get(key) {
// Verificar si ha expirado
// Check if it has expired
match entry.timestamp.elapsed() {
Ok(elapsed) if elapsed.as_secs() < entry.max_age => {
// Entry is still valid
@@ -162,9 +162,9 @@ impl HttpCache {
None
}
/// Genera un ETag simple para un bloque de bytes
/// Generates a simple ETag for a block of bytes
fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
// Calcular hash
// Calculate hash
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
let hash = hasher.finish();
@@ -173,7 +173,7 @@ impl HttpCache {
}
}
/// Middleware de caché HTTP
/// HTTP cache middleware
pub async fn cache_middleware<T>(
cache: HttpCache,
cache_key: &str,
@@ -184,65 +184,65 @@ pub async fn cache_middleware<T>(
where
T: Serialize
{
// Solo aplicar caché para solicitudes GET
// Only apply cache for GET requests
if req.method() != Method::GET {
return Ok(next.run(req).await);
}
// Verificar si la respuesta está en caché
// Check if the response is cached
let if_none_match = req.headers()
.get("if-none-match")
.and_then(|v| v.to_str().ok());
// Si hay una entrada en caché
// If there is a cache entry
if let Some(cache_entry) = cache.get(cache_key) {
// Comprobar si el cliente ya tiene la versión actualizada
// Check if the client already has the updated version
if let Some(client_etag) = if_none_match {
if client_etag == cache_entry.etag {
// El cliente tiene la versión más reciente, enviar 304 Not Modified
// The client has the most recent version, send 304 Not Modified
debug!("Cache hit (304) for key: {}", cache_key);
return Ok(create_not_modified_response(&cache_entry));
}
}
// El cliente necesita la versión actualizada
// The client needs the updated version
if let Some(data) = &cache_entry.data {
debug!("Cache hit (200) for key: {}", cache_key);
// Crear respuesta con los datos en caché
// Create response with cached data
let mut response = Response::new(Body::from(data.clone()));
// Copiar cabeceras originales
// Copy original headers
for (key, value) in &cache_entry.headers {
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
response.headers_mut().insert(key.clone(), value.clone());
}
}
// Añadir cabeceras de caché
// Add cache headers
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
return Ok(response);
}
}
// No está en caché o ha expirado, continuar con el middleware
// Not cached or expired, continue with the middleware
debug!("Cache miss for key: {}", cache_key);
let response = next.run(req).await;
// No cachear errores
// Don't cache errors
if !response.status().is_success() {
return Ok(response);
}
// Convertir la respuesta para calcular el ETag
// Convert the response to calculate the ETag
let (parts, _body) = response.into_parts();
let bytes = axum::body::to_bytes(_body, 1024 * 1024 * 10).await.unwrap_or_default();
// Calcular ETag
// Calculate ETag
let etag = cache.calculate_etag_for_bytes(&bytes);
// Guardar en caché
// Save to cache
cache.set(
cache_key,
etag.clone(),
@@ -251,26 +251,26 @@ where
max_age
);
// Crear la respuesta con ETag
// Create the response with ETag
let mut response = Response::from_parts(parts, Body::from(bytes));
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age));
Ok(response)
}
/// Crea una respuesta 304 Not Modified
/// Creates a 304 Not Modified response
fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
let mut response = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.body(Body::empty())
.unwrap();
// Copiar cabeceras de caché
// Copy cache headers
if let Some(cache_control) = entry.headers.get("cache-control") {
response.headers_mut().insert("cache-control", cache_control.clone());
}
// Añadir ETag
// Add ETag
response.headers_mut().insert(
"etag",
HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static(""))
@@ -279,22 +279,22 @@ fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
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) {
// Añadir ETag
// Add ETag
response.headers_mut().insert(
"etag",
HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static(""))
);
// Configurar Cache-Control
// Configure Cache-Control
let cache_control = format!("public, max-age={}", max_age);
response.headers_mut().insert(
"cache-control",
HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static(""))
);
// Añadir cabecera Last-Modified
// Add Last-Modified header
let now: DateTime<Utc> = Utc::now();
let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
response.headers_mut().insert(
@@ -303,7 +303,7 @@ fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
);
}
/// Layer para aplicar middleware de caché
/// Layer for applying cache middleware
#[derive(Clone)]
pub struct HttpCacheLayer {
cache: HttpCache,
@@ -311,7 +311,7 @@ pub struct HttpCacheLayer {
}
impl HttpCacheLayer {
/// Crea una nueva capa de caché
/// Creates a new cache layer
pub fn new(cache: HttpCache) -> Self {
Self {
cache,
@@ -319,7 +319,7 @@ impl HttpCacheLayer {
}
}
/// Establece el tiempo de vida máximo
/// Sets the maximum time-to-live
pub fn with_max_age(mut self, max_age: u64) -> Self {
self.max_age = Some(max_age);
self
@@ -338,7 +338,7 @@ impl<S> Layer<S> for HttpCacheLayer {
}
}
/// Servicio que implementa la lógica de caché
/// Service that implements cache logic
#[derive(Clone)]
pub struct HttpCacheService<S> {
inner: S,
@@ -365,10 +365,10 @@ where
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
// Generar clave de caché
// Generate cache key
let cache_key = req.uri().path().to_string();
// Solo aplicar caché para solicitudes GET
// Only apply cache for GET requests
if req.method() != Method::GET {
let future = self.inner.call(req);
return Box::pin(async move {
@@ -377,42 +377,42 @@ where
});
}
// Obtener ETag del cliente
// Get client ETag
let if_none_match = req.headers()
.get("if-none-match")
.and_then(|v| v.to_str().ok());
// Verificar si hay una entrada en caché
// Check if there is a cache entry
let cache_clone = self.cache.clone();
let max_age = self.max_age;
let entry = cache_clone.get(&cache_key);
match entry {
Some(cache_entry) if if_none_match == Some(&cache_entry.etag) => {
// El cliente tiene la versión correcta, enviar 304
// The client has the correct version, send 304
debug!("Cache HIT (304): {}", cache_key);
let response = create_not_modified_response(&cache_entry);
return Box::pin(async move { Ok(response) });
},
Some(cache_entry) if cache_entry.data.is_some() => {
// El cliente necesita la versión actualizada
// The client needs the updated version
debug!("Cache HIT (200): {}", cache_key);
let mut response = Response::new(Body::from(cache_entry.data.clone().unwrap()));
// Copiar cabeceras originales
// Copy original headers
for (key, value) in &cache_entry.headers {
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
response.headers_mut().insert(key.clone(), value.clone());
}
}
// Añadir cabeceras de caché
// Add cache headers
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
return Box::pin(async move { Ok(response) });
},
_ => {
// No está en caché o ha expirado
// Not cached or expired
debug!("Cache MISS: {}", cache_key);
let future = self.inner.call(req);
let cache_clone = self.cache.clone();
@@ -423,19 +423,19 @@ where
let response = future.await.map_err(|e| e.into())?;
let response = response_map_body(response).await;
// No cachear errores
// Don't cache errors
if !response.status().is_success() {
return Ok(response);
}
// Obtener el cuerpo y calcular ETag
// Get the body and calculate ETag
let (parts, body) = response.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?;
// Calcular ETag
// Calculate ETag
let etag = cache_clone.calculate_etag_for_bytes(&bytes);
// Guardar en caché
// Save to cache
cache_clone.set(
&cache_key,
etag.clone(),
@@ -444,7 +444,7 @@ where
max_age
);
// Crear la respuesta con ETag
// Create the response with ETag
let mut response = Response::from_parts(parts, Body::from(bytes));
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache_clone.default_max_age));
@@ -455,9 +455,9 @@ where
}
}
// Función auxiliar para convertir cualquier cuerpo en Body preservando su contenido.
// Anteriormente esta función descartaba el body con Body::empty(), causando
// pérdida de datos en respuestas no cacheadas.
// Helper function to convert any body into Body preserving its content.
// Previously this function discarded the body with Body::empty(), causing
// data loss in non-cached responses.
async fn response_map_body<B>(response: Response<B>) -> Response<Body>
where
B: http_body::Body + Send + 'static,
@@ -478,10 +478,10 @@ where
Response::from_parts(parts, Body::from(collected))
}
/// Inicia una tarea de limpieza periódica para el caché
/// Starts a periodic cleanup task for the cache
pub fn start_cache_cleanup_task(cache: HttpCache) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Cada 5 minutos
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Every 5 minutes
loop {
interval.tick().await;
@@ -516,10 +516,10 @@ mod tests {
let etag2 = cache.calculate_etag_for_bytes(&data2);
let etag3 = cache.calculate_etag_for_bytes(&data3);
// Mismos datos deben generar mismo ETag
// Same data should generate the same ETag
assert_eq!(etag1, etag2);
// Datos diferentes deben generar ETags diferentes
// Different data should generate different ETags
assert_ne!(etag1, etag3);
}
@@ -527,19 +527,19 @@ mod tests {
async fn test_cache_hit_miss() {
let cache = HttpCache::new();
// Crear datos de prueba directamente como Bytes
// Create test data directly as Bytes
let bytes1 = Bytes::from(r#"{"id":1,"name":"Test"}"#);
let headers1 = HeaderMap::new();
let etag1 = cache.calculate_etag_for_bytes(&bytes1);
cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None);
// Verificar cache hit
// Verify cache hit
let entry = cache.get("test").unwrap();
assert_eq!(entry.etag, etag1);
assert_eq!(entry.data.unwrap(), bytes1);
// Verificar cache miss
// Verify cache miss
assert!(cache.get("nonexistent").is_none());
}
}
+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 domain;
pub mod application;
pub mod infrastructure;
pub mod interfaces;
// Re-exportaciones públicas comunes
// Common public re-exports
pub use application::services::folder_service::FolderService;
pub use application::services::i18n_application_service::I18nApplicationService;
pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
+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 {
position: absolute;
top: 10px;
@@ -31,12 +31,12 @@
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
}
/* Estilos para los elementos de la vista de favoritos */
/* Styles for the favorites view items */
.favorite-item {
position: relative;
}
/* Ajustes para la vista de cuadrícula */
/* Adjustments for grid view */
.file-card.favorite-item {
border-left: 3px solid #ffc107;
@@ -46,7 +46,7 @@
}
}
/* Ajustes para la vista de lista */
/* Adjustments for list view */
.file-item.favorite-item {
position: relative;
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
@@ -60,7 +60,7 @@
height: 30px;
}
/* Estilos para el estado vacío específico de favoritos */
/* Styles for the favorites-specific empty state */
.favorites-empty-state {
display: flex;
flex-direction: column;
@@ -83,7 +83,7 @@
max-width: 400px;
}
/* Animación para la estrella de favorito */
/* Animation for the favorite star */
@keyframes favorite-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
+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 {
position: absolute;
top: 10px;
@@ -20,12 +20,12 @@
}
}
/* Estilos para los elementos de la vista de recientes */
/* Styles for the recent view items */
.recent-item {
position: relative;
}
/* Ajustes para la vista de cuadrícula */
/* Adjustments for grid view */
.file-card.recent-item {
border-left: 3px solid #6c757d;
@@ -35,7 +35,7 @@
}
}
/* Ajustes para la vista de lista */
/* Adjustments for list view */
.file-item.recent-item {
position: relative;
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
@@ -49,7 +49,7 @@
height: 30px;
}
/* Estilos para el estado vacío específico de recientes */
/* Styles for the recent-specific empty state */
.recents-empty-state {
display: flex;
flex-direction: column;
@@ -72,12 +72,12 @@
max-width: 400px;
}
/* Tooltip para tiempo de acceso */
/* Tooltip for access time */
.recent-item .file-info {
cursor: help;
}
/* Animación para archivos recientes */
/* Animation for recent files */
.recent-item {
animation: recent-fade-in 0.3s ease;
}
+10 -10
View File
@@ -1271,7 +1271,7 @@ select:focus {
.file-icon.doc-icon {
width: 100px;
height: 70px;
background-color: #e2e8f0; /* Fondo gris claro */
background-color: #e2e8f0; /* Light gray background */
border-radius: 4px;
position: relative;
margin-bottom: 10px;
@@ -1436,7 +1436,7 @@ select:focus {
.file-icon.code-icon {
width: 100px;
height: 70px;
background-color: #e2e8f0; /* Fondo gris claro */
background-color: #e2e8f0; /* Light gray background */
border-radius: 4px;
position: relative;
margin-bottom: 10px;
@@ -1770,7 +1770,7 @@ select:focus {
text-align: center;
}
/* Lista de archivos - Estilo mejorado */
/* File list - Improved style */
.files-list-view {
display: flex;
flex-direction: column;
@@ -1802,7 +1802,7 @@ select:focus {
background-color: white;
}
/* Para modo papelera, ajustar columnas */
/* For trash mode, adjust columns */
.trash-item.file-item {
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 120px 100px;
}
@@ -1851,7 +1851,7 @@ select:focus {
display: none;
}
/* Iconos específicos para archivos en vista de lista */
/* Specific icons for files in list view */
.file-item .file-icon.pdf-icon {
background-color: #fee2e2;
}
@@ -2129,7 +2129,7 @@ select:focus {
transition: width 0.3s;
}
/* Notificación */
/* Notification */
.notification {
position: absolute;
top: 70px;
@@ -3327,7 +3327,7 @@ header {
}
}
/* Estilos específicos para la página Shared */
/* Specific styles for the Shared page */
.page-description {
color: #718096;
font-size: 16px;
@@ -3335,7 +3335,7 @@ header {
margin-bottom: 25px;
}
/* Mejoras para los filtros en la página Shared */
/* Improvements for the Shared page filters */
.shared-filters {
display: flex;
justify-content: space-between;
@@ -3383,7 +3383,7 @@ header {
width: 250px;
}
/* Mejoras en la tabla de elementos compartidos */
/* Improvements in the shared items table */
.shared-list-container {
background-color: white;
border-radius: 10px;
@@ -3412,7 +3412,7 @@ header {
vertical-align: middle;
}
/* Estado vacío mejorado */
/* Improved empty state */
.empty-state {
display: flex;
flex-direction: column;
+28 -28
View File
@@ -27,7 +27,7 @@
<script src="/js/fileRenderer.js"></script>
<script src="/js/fileSharing.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/inlineViewer.js"></script>
<script src="/js/app.js"></script>
@@ -94,7 +94,7 @@
<div class="top-bar">
<div class="search-container">
<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">
<i class="fas fa-search"></i>
</button>
@@ -110,8 +110,8 @@
<div class="user-menu-header">
<div class="user-menu-avatar" id="user-menu-avatar">AD</div>
<div class="user-menu-info">
<div class="user-menu-name" id="user-menu-name">Usuario</div>
<div class="user-menu-email" id="user-menu-email">usuario@oxicloud.app</div>
<div class="user-menu-name" id="user-menu-name">User</div>
<div class="user-menu-email" id="user-menu-email">user@oxicloud.app</div>
</div>
</div>
<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-label">
<i class="fas fa-database"></i>
<span data-i18n="storage.title">Almacenamiento</span>
<span data-i18n="storage.title">Storage</span>
</div>
<div class="user-menu-storage-bar">
<div class="user-menu-storage-fill" id="user-menu-storage-fill"></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 class="user-menu-divider"></div>
<button class="user-menu-item user-menu-admin" id="user-menu-admin" style="display:none">
<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 class="user-menu-item" id="user-menu-profile">
<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>
<div class="user-menu-divider" id="user-menu-admin-divider" style="display:none"></div>
<button class="user-menu-item" id="user-menu-theme">
<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-knob"></div>
</div>
</button>
<button class="user-menu-item" id="user-menu-about">
<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>
<div class="user-menu-divider"></div>
<button class="user-menu-item user-menu-logout" id="user-menu-logout">
<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>
</div>
</div>
@@ -159,38 +159,38 @@
</div>
<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="action-buttons">
<div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn">
<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>
</button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn">
<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 class="upload-dropdown-item" id="upload-folder-btn">
<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>
</div>
</div>
<button class="btn btn-secondary" id="new-folder-btn">
<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>
</div>
<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>
</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>
</button>
</div>
@@ -198,7 +198,7 @@
<div class="dropzone" id="dropzone">
<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="folder-input" style="display: none;" webkitdirectory directory multiple>
<div class="upload-progress">
@@ -222,10 +222,10 @@
<!-- List View (hidden by default) -->
<div class="files-list-view" id="files-list-view" style="display: none;">
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.size">Tamaño</div>
<div data-i18n="files.modified">Modificado</div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modified</div>
</div>
<!-- Files will be populated here in list view -->
</div>
@@ -240,18 +240,18 @@
<div class="modal-icon">
<i id="modal-icon" class="fas fa-folder-plus"></i>
</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">
<i class="fas fa-times"></i>
</button>
</div>
<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">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="modal-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
<button class="btn btn-primary" id="modal-confirm-btn" data-i18n="actions.confirm">Crear</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">Create</button>
</div>
</div>
</div>
@@ -281,7 +281,7 @@
<i class="fas fa-file-alt"></i> MIT License
</a>
</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>
</body>
+69 -69
View File
@@ -197,7 +197,7 @@ function setupUserMenu() {
// Theme switching could be expanded here in the future
window.ui.showNotification(
dark ? '🌙' : '☀️',
dark ? 'Modo oscuro activado (próximamente)' : 'Modo claro activado'
dark ? 'Dark mode enabled (coming soon)' : 'Light mode enabled'
);
}
});
@@ -476,12 +476,12 @@ function setupEventListeners() {
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Papelera';
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash';
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<button class="btn btn-danger" id="empty-trash-btn">
<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>
</div>
`;
@@ -519,35 +519,35 @@ function setupEventListeners() {
app.currentSection = 'files';
// Reset UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Archivos';
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn">
<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>
</button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn">
<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 class="upload-dropdown-item" id="upload-folder-btn">
<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>
</div>
</div>
<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>
</div>
<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>
</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>
</button>
</div>
@@ -623,14 +623,14 @@ function setupEventListeners() {
*/
async function loadFiles(options = {}) {
try {
console.log("Iniciando loadFiles() - cargando archivos...", options);
console.log("Starting loadFiles() - loading files...", options);
// Flag para forzar el refresco completo ignorando caché
// Flag to force complete refresh ignoring cache
const forceRefresh = options.forceRefresh || false;
// Prevenir múltiples solicitudes de carga simultáneas
// Prevent multiple simultaneous load requests
if (window.isLoadingFiles) {
console.log("Ya hay una carga de archivos en progreso, ignorando solicitud");
console.log("A file load is already in progress, ignoring request");
return;
}
@@ -640,7 +640,7 @@ async function loadFiles(options = {}) {
elements.filesGrid.innerHTML = `
<div class="files-loading-spinner">
<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>
`;
@@ -651,12 +651,12 @@ async function loadFiles(options = {}) {
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
if (userData.username) {
// Find user's home folder
console.log("Buscando carpeta de usuario para", userData.username);
console.log("Looking for user folder for", userData.username);
await findUserHomeFolder(userData.username);
}
}
// Agregar timestamp para evitar caché
// Add timestamp to avoid cache
const timestamp = new Date().getTime();
let url;
@@ -667,7 +667,7 @@ async function loadFiles(options = {}) {
url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`;
app.currentPath = app.userHomeFolderId;
ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
console.log(`Cargando carpeta del usuario: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
} else {
// Emergency fallback - this should rarely happen but prevents errors
url = `/api/folders?t=${timestamp}`;
@@ -676,7 +676,7 @@ async function loadFiles(options = {}) {
} else {
// Normal case - viewing subfolder contents
url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`;
console.log(`Cargando contenido de subcarpeta: ${app.currentPath}`);
console.log(`Loading subfolder content: ${app.currentPath}`);
}
const token = localStorage.getItem('oxicloud_token');
@@ -686,14 +686,14 @@ async function loadFiles(options = {}) {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
},
cache: 'no-store' // Instruir al navegador a no usar caché
cache: 'no-store' // Instruct the browser not to use cache
};
// Si se especifica forceRefresh, agregar un parámetro adicional para evitar caché
// If forceRefresh is specified, add an additional parameter to avoid cache
if (forceRefresh) {
url += `&force_refresh=true`;
requestOptions.headers['X-Force-Refresh'] = 'true';
console.log('Forzando refresco completo ignorando caché');
console.log('Forcing complete refresh ignoring cache');
}
console.log(`Loading files from ${url}`);
@@ -703,13 +703,13 @@ async function loadFiles(options = {}) {
if (response.status === 401 || response.status === 403) {
console.warn("Auth error when loading files, showing empty list");
// Just show empty state instead of causing redirect loops
elements.filesGrid.innerHTML = '<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 = `
<div class="list-header">
<div>Nombre</div>
<div>Tipo</div>
<div>Tamaño</div>
<div>Modificado</div>
<div>Name</div>
<div>Type</div>
<div>Size</div>
<div>Modified</div>
</div>
`;
return;
@@ -724,10 +724,10 @@ async function loadFiles(options = {}) {
elements.filesGrid.innerHTML = '';
elements.filesListView.innerHTML = `
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.size">Tamaño</div>
<div data-i18n="files.modified">Modificado</div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modified</div>
</div>
`;
@@ -766,11 +766,11 @@ async function loadFiles(options = {}) {
// Also load files in this folder
const cacheTimestamp = new Date().getTime();
let filesUrl = `/api/files?t=${cacheTimestamp}`; // Agregar timestamp para evitar problemas de caché
let filesUrl = `/api/files?t=${cacheTimestamp}`; // Add timestamp to avoid cache issues
if (app.currentPath) {
filesUrl += `&folder_id=${app.currentPath}`;
}
console.log(`Cargando archivos desde: ${filesUrl}`);
console.log(`Loading files from: ${filesUrl}`);
try {
console.log(`Fetching files from: ${filesUrl}`);
@@ -810,7 +810,7 @@ async function loadFiles(options = {}) {
console.error('Error loading folders:', error);
ui.showNotification('Error', 'Could not load files and folders');
} finally {
// Marcar que ya no estamos cargando archivos para permitir solicitudes futuras
// Mark that we are no longer loading files to allow future requests
window.isLoadingFiles = false;
}
}
@@ -839,11 +839,11 @@ async function loadTrashItems() {
elements.filesGrid.innerHTML = '';
elements.filesListView.innerHTML = `
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="trash.original_location">Ubicación original</div>
<div data-i18n="trash.deleted_date">Fecha eliminación</div>
<div data-i18n="trash.actions">Acciones</div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="trash.original_location">Original location</div>
<div data-i18n="trash.deleted_date">Deletion date</div>
<div data-i18n="trash.actions">Actions</div>
</div>
`;
@@ -864,7 +864,7 @@ async function loadTrashItems() {
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<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);
return;
@@ -877,7 +877,7 @@ async function loadTrashItems() {
} catch (error) {
console.error('Error loading trash items:', error);
window.ui.showNotification('Error', 'Error al cargar elementos de la papelera');
window.ui.showNotification('Error', 'Error loading trash items');
}
}
@@ -896,8 +896,8 @@ function addTrashItemToView(item) {
// Item type label
const typeLabel = isFile ?
(window.i18n ? window.i18n.t('files.file_types.file') : 'Archivo') :
(window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta');
(window.i18n ? window.i18n.t('files.file_types.file') : 'File') :
(window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder');
// Grid view element
const gridElement = document.createElement('div');
@@ -912,10 +912,10 @@ function addTrashItemToView(item) {
<div class="file-name">${item.name}</div>
<div class="file-info">${typeLabel} - ${formattedDate}</div>
<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>
</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>
</button>
</div>
@@ -956,10 +956,10 @@ function addTrashItemToView(item) {
<div class="path-cell">${item.original_path || '--'}</div>
<div class="date-cell">${formattedDate}</div>
<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>
</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>
</button>
</div>
@@ -995,7 +995,7 @@ async function performSearch(query) {
app.isSearchMode = true;
// Set breadcrumb for search
ui.updateBreadcrumb(`Búsqueda: "${query}"`);
ui.updateBreadcrumb(`Search: "${query}"`);
// Prepare search options
const options = {
@@ -1037,7 +1037,7 @@ async function performSearch(query) {
} catch (error) {
console.error('Search error:', error);
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
window.ui.showNotification('Error', 'Error performing search');
}
}
@@ -1076,7 +1076,7 @@ function switchToSharedView() {
}
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Compartidos';
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Shared';
// Clear breadcrumb and show root
ui.updateBreadcrumb('');
@@ -1105,7 +1105,7 @@ function switchToFilesView() {
app.currentSection = 'files';
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Archivos';
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
@@ -1122,29 +1122,29 @@ function switchToFilesView() {
<div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn">
<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>
</button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn">
<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 class="upload-dropdown-item" id="upload-folder-btn">
<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>
</div>
</div>
<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>
</div>
<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>
</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>
</button>
</div>
@@ -1219,7 +1219,7 @@ function switchToFavoritesView() {
}
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favoritos';
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favorites';
// Clear breadcrumb and show root
ui.updateBreadcrumb('');
@@ -1235,10 +1235,10 @@ function switchToFavoritesView() {
<!-- No actions needed for favorites view -->
</div>
<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>
</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>
</button>
</div>
@@ -1278,7 +1278,7 @@ function switchToFavoritesView() {
filesGrid.innerHTML = `
<div class="empty-state">
<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>
`;
}
@@ -1308,7 +1308,7 @@ function switchToRecentFilesView() {
}
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recientes';
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recent';
// Clear breadcrumb and show root
ui.updateBreadcrumb('');
@@ -1322,14 +1322,14 @@ function switchToRecentFilesView() {
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<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>
</div>
<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>
</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>
</button>
</div>
@@ -1341,7 +1341,7 @@ function switchToRecentFilesView() {
if (window.recent) {
window.recent.clearRecentFiles();
window.recent.displayRecentFiles();
window.ui.showNotification('Limpieza completada', 'Se ha limpiado el historial de archivos recientes');
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
}
});
@@ -1378,7 +1378,7 @@ function switchToRecentFilesView() {
filesGrid.innerHTML = `
<div class="empty-state">
<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>
`;
}
@@ -1450,7 +1450,7 @@ window.refreshUserData = refreshUserData;
function showUserProfileModal() {
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const username = userData.username || 'Usuario';
const username = userData.username || 'User';
const email = userData.email || '';
const role = userData.role || 'user';
const initials = username.substring(0, 2).toUpperCase();
@@ -1847,7 +1847,7 @@ async function findUserHomeFolder(username) {
* Logout - clear all auth data and redirect to login
*/
function logout() {
// Nombres de variables según auth.js
// Variable names as per auth.js
const TOKEN_KEY = 'oxicloud_token';
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
+20 -20
View File
@@ -34,7 +34,7 @@ const LANGUAGE_TEXTS = {
subtitle: 'Por favor, selecciona tu idioma',
continue: 'Continuar',
autodetected: 'Hemos detectado tu idioma',
moreLanguages: 'Más idiomas...',
moreLanguages: 'More languages...',
modalTitle: 'Seleccionar idioma',
searchPlaceholder: 'Buscar idioma...'
},
@@ -494,8 +494,8 @@ document.addEventListener('DOMContentLoaded', () => {
console.error('Error showing initial panel:', err);
});
// Siempre limpiar los contadores al cargar la página de login
// para asegurar que no quedamos atrapados en un bucle
// Always clear counters when loading the login page
// to ensure we don't get trapped in a loop
console.log('Login page loaded, clearing all counters');
sessionStorage.removeItem('redirect_count');
localStorage.removeItem('refresh_attempts');
@@ -589,14 +589,14 @@ if (isLoginPage && loginForm) {
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
// Extraer fecha de expiración desde el token JWT
// Extract expiration date from the JWT token
let parsedExpiry = false;
const tokenParts = token.split('.');
if (tokenParts.length === 3) {
try {
const payload = JSON.parse(atob(tokenParts[1]));
if (payload.exp) {
// payload.exp está en segundos desde epoch
// payload.exp is in seconds since epoch
const expiryDate = new Date(payload.exp * 1000);
// Verify the date is valid
@@ -642,7 +642,7 @@ if (isLoginPage && loginForm) {
// Redirect to main app
redirectToMainApp();
} catch (error) {
loginError.textContent = error.message || 'Error al iniciar sesión';
loginError.textContent = error.message || 'Error logging in';
loginError.style.display = 'block';
}
});
@@ -664,7 +664,7 @@ if (isLoginPage && registerForm) {
// Validate passwords match
if (password !== confirmPassword) {
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
registerError.textContent = errorMsg;
registerError.style.display = 'block';
return;
@@ -674,7 +674,7 @@ if (isLoginPage && registerForm) {
const data = await register(username, email, password);
// Show success message
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.';
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
registerSuccess.textContent = successMsg;
registerSuccess.style.display = 'block';
@@ -687,7 +687,7 @@ if (isLoginPage && registerForm) {
registerPanel.style.display = 'none';
}, 2000);
} catch (error) {
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al registrar cuenta';
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error registering account';
registerError.textContent = error.message || errorMsg;
registerError.style.display = 'block';
}
@@ -710,7 +710,7 @@ if (isLoginPage && adminSetupForm) {
// Validate passwords match
if (password !== confirmPassword) {
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
adminSetupError.textContent = errorMsg;
adminSetupError.style.display = 'block';
return;
@@ -721,7 +721,7 @@ if (isLoginPage && adminSetupForm) {
const data = await register('admin', email, password, 'admin');
// Show success message in the GUI instead of alert
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : '¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.';
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
if (adminSetupSuccess) {
adminSetupSuccess.textContent = successMsg;
@@ -736,7 +736,7 @@ if (isLoginPage && adminSetupForm) {
}, 2000);
} catch (error) {
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al crear cuenta de administrador';
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error creating admin account';
adminSetupError.textContent = error.message || errorMsg;
adminSetupError.style.display = 'block';
}
@@ -792,10 +792,10 @@ async function login(username, password) {
if (!response.ok) {
try {
const errorData = await response.json();
throw new Error(errorData.error || 'Falló la autenticación');
throw new Error(errorData.error || 'Authentication failed');
} catch (jsonError) {
// If the error response is not valid JSON
throw new Error(`Error de autenticación (${response.status}): ${response.statusText}`);
throw new Error(`Authentication error (${response.status}): ${response.statusText}`);
}
}
@@ -806,7 +806,7 @@ async function login(username, password) {
return data;
} catch (jsonError) {
console.error('Error parsing login response:', jsonError);
throw new Error('Error al procesar la respuesta del servidor');
throw new Error('Error processing server response');
}
} catch (error) {
console.error('Login error:', error);
@@ -848,10 +848,10 @@ async function register(username, email, password, role = 'user') {
if (!response.ok) {
try {
const errorData = await response.json();
throw new Error(errorData.error || 'Error en el registro');
throw new Error(errorData.error || 'Registration error');
} catch (jsonError) {
// If the error response is not valid JSON
throw new Error(`Error de registro (${response.status}): ${response.statusText}`);
throw new Error(`Registration error (${response.status}): ${response.statusText}`);
}
}
@@ -862,7 +862,7 @@ async function register(username, email, password, role = 'user') {
return data;
} catch (jsonError) {
console.error('Error parsing registration response:', jsonError);
throw new Error('Error al procesar la respuesta del servidor');
throw new Error('Error processing server response');
}
} catch (error) {
console.error('Registration error:', error);
@@ -883,7 +883,7 @@ async function fetchUserData(token) {
});
if (!response.ok) {
throw new Error('Error al obtener datos del usuario');
throw new Error('Error fetching user data');
}
return await response.json();
@@ -1092,7 +1092,7 @@ function redirectToMainApp() {
window.location.href = '/login.html?error=redirect_fatal';
} catch (e) {
// Nothing more we can do
alert('Error crítico en la redirección. Por favor, recarga la página e intenta nuevamente.');
alert('Critical redirect error. Please reload the page and try again.');
}
}
+26 -26
View File
@@ -30,7 +30,7 @@ const contextMenus = {
window.favorites.removeFromFavorites(folder.id, 'folder');
// Update menu item text
document.getElementById('favorite-folder-option').querySelector('span').textContent =
window.i18n ? window.i18n.t('actions.favorite') : 'Añadir a favoritos';
window.i18n ? window.i18n.t('actions.favorite') : 'Add to favorites';
} else {
// Add to favorites
window.favorites.addToFavorites(
@@ -41,7 +41,7 @@ const contextMenus = {
);
// Update menu item text
document.getElementById('favorite-folder-option').querySelector('span').textContent =
window.i18n ? window.i18n.t('actions.unfavorite') : 'Quitar de favoritos';
window.i18n ? window.i18n.t('actions.unfavorite') : 'Remove from favorites';
}
}
window.ui.closeContextMenu();
@@ -143,7 +143,7 @@ const contextMenus = {
window.favorites.removeFromFavorites(file.id, 'file');
// Update menu item text
document.getElementById('favorite-file-option').querySelector('span').textContent =
window.i18n ? window.i18n.t('actions.favorite') : 'Añadir a favoritos';
window.i18n ? window.i18n.t('actions.favorite') : 'Add to favorites';
} else {
// Add to favorites
window.favorites.addToFavorites(
@@ -154,7 +154,7 @@ const contextMenus = {
);
// Update menu item text
document.getElementById('favorite-file-option').querySelector('span').textContent =
window.i18n ? window.i18n.t('actions.unfavorite') : 'Quitar de favoritos';
window.i18n ? window.i18n.t('actions.unfavorite') : 'Remove from favorites';
}
}
window.ui.closeFileContextMenu();
@@ -246,7 +246,7 @@ const contextMenus = {
renameInput.value = folder.name;
// Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_folder') : 'Renombrar carpeta';
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_folder') : 'Rename folder';
renameDialog.style.display = 'flex';
renameInput.focus();
renameInput.select();
@@ -264,7 +264,7 @@ const contextMenus = {
renameInput.value = file.name;
// Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_file') : 'Renombrar archivo';
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_file') : 'Rename file';
renameDialog.style.display = 'flex';
renameInput.focus();
renameInput.select();
@@ -293,8 +293,8 @@ const contextMenus = {
// Update dialog title (preserve icon)
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
const titleText = mode === 'file' ?
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Mover archivo') :
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Mover carpeta');
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Move file') :
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Move folder');
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i> <span>${titleText}</span>`;
// Load all available folders
@@ -319,7 +319,7 @@ const contextMenus = {
async renameItem() {
const newName = document.getElementById('rename-input').value.trim();
if (!newName) {
alert(window.i18n ? window.i18n.t('errors.empty_name') : 'El nombre no puede estar vacío');
alert(window.i18n ? window.i18n.t('errors.empty_name') : 'Name cannot be empty');
return;
}
@@ -361,7 +361,7 @@ const contextMenus = {
// Clear container except root option
folderSelectContainer.innerHTML = `
<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>
`;
@@ -438,8 +438,8 @@ const contextMenus = {
// Update dialog content
dialogHeader.textContent = itemType === 'file' ?
(window.i18n ? window.i18n.t('dialogs.share_file') : 'Compartir archivo') :
(window.i18n ? window.i18n.t('dialogs.share_folder') : 'Compartir carpeta');
(window.i18n ? window.i18n.t('dialogs.share_file') : 'Share file') :
(window.i18n ? window.i18n.t('dialogs.share_folder') : 'Share folder');
itemName.textContent = item.name;
@@ -470,21 +470,21 @@ const contextMenus = {
shareEl.className = 'existing-share-item';
const expiresText = share.expires_at ?
`Vence: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
'Sin vencimiento';
`Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
'No expiration';
shareEl.innerHTML = `
<div class="share-url">${share.url}</div>
<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>
</div>
<div class="share-actions">
<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 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>
</div>
`;
@@ -507,9 +507,9 @@ const contextMenus = {
const shareId = btn.getAttribute('data-share-id');
showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Eliminar enlace',
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : '¿Estás seguro de que quieres eliminar este enlace compartido?',
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar',
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Delete link',
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : 'Are you sure you want to delete this shared link?',
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
}).then(confirmed => {
if (confirmed) {
window.fileSharing.removeSharedLink(shareId);
@@ -534,7 +534,7 @@ const contextMenus = {
*/
createSharedLink() {
if (!window.app.shareDialogItem || !window.app.shareDialogItemType) {
window.ui.showNotification('Error', 'No se pudo compartir el elemento');
window.ui.showNotification('Error', 'Could not share the item');
return;
}
@@ -577,14 +577,14 @@ const contextMenus = {
shareUrl.select();
// Show success message
window.ui.showNotification('Enlace creado', 'Enlace compartido creado correctamente');
window.ui.showNotification('Link created', 'Shared link created successfully');
// Reload existing shares
this.showShareDialog(item, itemType);
} catch (error) {
console.error('Error creating shared link:', error);
window.ui.showNotification('Error', 'No se pudo crear el enlace compartido');
window.ui.showNotification('Error', 'Could not create shared link');
}
},
@@ -614,14 +614,14 @@ const contextMenus = {
const shareUrl = window.app.notificationShareUrl;
if (!email || !shareUrl) {
window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido');
window.ui.showNotification('Error', 'Please enter a valid email address');
return;
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido');
window.ui.showNotification('Error', 'Please enter a valid email address');
return;
}
@@ -630,7 +630,7 @@ const contextMenus = {
document.getElementById('notification-dialog').style.display = 'none';
} catch (error) {
console.error('Error sending notification:', error);
window.ui.showNotification('Error', 'No se pudo enviar la notificación');
window.ui.showNotification('Error', 'Could not send notification');
}
},
+17 -17
View File
@@ -230,8 +230,8 @@ const favorites = {
// Show success notification
window.ui.showNotification(
'Añadido a favoritos',
`"${name}" añadido a favoritos`
'Added to favorites',
`"${name}" added to favorites`
);
return true;
@@ -271,8 +271,8 @@ const favorites = {
// Show success notification if item was found
if (item) {
window.ui.showNotification(
'Eliminado de favoritos',
`"${item.name}" eliminado de favoritos`
'Removed from favorites',
`"${item.name}" removed from favorites`
);
}
return true;
@@ -310,10 +310,10 @@ const favorites = {
filesGrid.innerHTML = '';
filesListView.innerHTML = `
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.size">Tamaño</div>
<div data-i18n="files.modified">Modificado</div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modified</div>
</div>
`;
@@ -326,8 +326,8 @@ const favorites = {
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<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_hint') : 'Para marcar como favorito, haz clic derecho en cualquier archivo o carpeta'}</p>
<p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}</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);
return;
@@ -359,7 +359,7 @@ const favorites = {
} catch (error) {
console.error('Error displaying favorites:', error);
window.ui.showNotification('Error', 'Error al cargar elementos favoritos');
window.ui.showNotification('Error', 'Error loading favorite items');
}
},
@@ -468,7 +468,7 @@ const favorites = {
<i class="fas fa-folder"></i>
</div>
<div class="file-name">${folder.name}</div>
<div class="file-info">Carpeta</div>
<div class="file-info">Folder</div>
`;
// Click to navigate
@@ -518,7 +518,7 @@ const favorites = {
</div>
<span>${folder.name}</span>
</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="date-cell">${formattedDate}</div>
`;
@@ -559,17 +559,17 @@ const favorites = {
// Determine icon and type
let iconClass = 'fas fa-file';
let iconSpecialClass = '';
let typeLabel = 'Documento';
let typeLabel = 'Document';
if (file.mime_type) {
if (file.mime_type.startsWith('image/')) {
iconClass = 'fas fa-file-image';
iconSpecialClass = 'image-icon';
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen';
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
} else if (file.mime_type.startsWith('text/')) {
iconClass = 'fas fa-file-alt';
iconSpecialClass = 'text-icon';
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto';
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
} else if (file.mime_type.startsWith('video/')) {
iconClass = 'fas fa-file-video';
iconSpecialClass = 'video-icon';
@@ -606,7 +606,7 @@ const favorites = {
<i class="${iconClass}"></i>
</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
+91 -91
View File
@@ -49,8 +49,8 @@ const fileOps = {
try {
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`);
// Usamos la URL correcta para la subida de archivos
console.log('Formulario a enviar:', {
// We use the correct URL for file upload
console.log('Form to submit:', {
file: file.name,
size: file.size,
folder_id: targetFolderId || 'root'
@@ -59,16 +59,16 @@ const fileOps = {
const response = await fetch('/api/files/upload', {
method: 'POST',
body: formData,
// Añadir cache: 'no-store' para evitar problemas de caché durante la subida
// Add cache: 'no-store' to avoid cache issues during upload
cache: 'no-store',
headers: {
...getAuthHeaders(),
// Agregar este encabezado para forzar recargas frescas
// Add this header to force fresh reloads
'Cache-Control': 'no-cache, no-store, must-revalidate'
}
});
console.log('Respuesta del servidor:', {
console.log('Server response:', {
status: response.status,
statusText: response.statusText
});
@@ -83,11 +83,11 @@ const fileOps = {
console.log(`Successfully uploaded ${file.name}`, responseData);
// Show success notification immediately
window.ui.showNotification('Archivo subido', `${file.name} completado`);
window.ui.showNotification('File uploaded', `${file.name} completed`);
if (i === totalFiles - 1) {
// Last file uploaded - wait and reload once
console.log('Último archivo subido, esperando antes de recargar...');
console.log('Last file uploaded, waiting before reloading...');
// Wait for backend to persist
await new Promise(resolve => setTimeout(resolve, 800));
@@ -96,7 +96,7 @@ const fileOps = {
try {
await window.loadFiles({forceRefresh: true});
} catch (reloadError) {
console.error("Error recargando archivos:", reloadError);
console.error("Error reloading files:", reloadError);
}
// Hide upload UI
@@ -109,11 +109,11 @@ const fileOps = {
} else {
const errorData = await response.text();
console.error('Upload error:', errorData);
window.ui.showNotification('Error', `Error al subir el archivo: ${file.name}`);
window.ui.showNotification('Error', `Error uploading file: ${file.name}`);
}
} catch (error) {
console.error('Network error during upload:', error);
window.ui.showNotification('Error', `Error de red al subir el archivo: ${file.name}`);
window.ui.showNotification('Error', `Network error uploading file: ${file.name}`);
}
}
},
@@ -181,7 +181,7 @@ const fileOps = {
console.log(`Created folder: ${folderPath} -> ${folder.id}`);
} else {
console.error(`Error creating folder ${folderPath}:`, await response.text());
window.ui.showNotification('Error', `Error creando carpeta: ${folderName}`);
window.ui.showNotification('Error', `Error creating folder: ${folderName}`);
}
} catch (error) {
console.error(`Network error creating folder ${folderPath}:`, error);
@@ -228,7 +228,7 @@ const fileOps = {
}
// Finish up
window.ui.showNotification('Carpeta subida', `${uploadedCount} archivos subidos correctamente`);
window.ui.showNotification('Folder uploaded', `${uploadedCount} files uploaded successfully`);
await new Promise(resolve => setTimeout(resolve, 800));
@@ -253,7 +253,7 @@ const fileOps = {
try {
console.log('Creating folder with name:', name);
// Enviar la solicitud real al backend para crear la carpeta
// Send the actual request to the backend to create the folder
const response = await fetch('/api/folders', {
method: 'POST',
headers: {
@@ -268,28 +268,28 @@ const fileOps = {
});
if (response.ok) {
// Obtener la carpeta creada del backend
// Get the created folder from the backend
const folder = await response.json();
console.log('Folder created successfully:', folder);
// Añadir la carpeta a la vista de inmediato para feedback instantáneo
// Add the folder to the view immediately for instant feedback
window.ui.addFolderToView(folder);
// Esperar para permitir que el backend guarde los cambios
// Wait to allow the backend to save the changes
await new Promise(resolve => setTimeout(resolve, 1000));
// Recargar los archivos para refrescar la vista
// Reload files to refresh the view
await window.loadFiles({forceRefresh: true});
window.ui.showNotification('Carpeta creada', `"${name}" creada correctamente`);
window.ui.showNotification('Folder created', `"${name}" created successfully`);
} else {
const errorData = await response.text();
console.error('Create folder error:', errorData);
window.ui.showNotification('Error', 'Error al crear la carpeta');
window.ui.showNotification('Error', 'Error creating the folder');
}
} catch (error) {
console.error('Error creating folder:', error);
window.ui.showNotification('Error', 'Error al crear la carpeta');
window.ui.showNotification('Error', 'Error creating the folder');
}
},
@@ -315,22 +315,22 @@ const fileOps = {
if (response.ok) {
// Reload files after moving
await window.loadFiles();
window.ui.showNotification('Archivo movido', 'Archivo movido correctamente');
window.ui.showNotification('File moved', 'File moved successfully');
return true;
} else {
let errorMessage = 'Error desconocido';
let errorMessage = 'Unknown error';
try {
const errorData = await response.json();
errorMessage = errorData.error || 'Error desconocido';
errorMessage = errorData.error || 'Unknown error';
} catch (e) {
errorMessage = 'Error al procesar la respuesta del servidor';
errorMessage = 'Error processing server response';
}
window.ui.showNotification('Error', `Error al mover el archivo: ${errorMessage}`);
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
return false;
}
} catch (error) {
console.error('Error moving file:', error);
window.ui.showNotification('Error', 'Error al mover el archivo');
window.ui.showNotification('Error', 'Error moving the file');
return false;
}
},
@@ -357,22 +357,22 @@ const fileOps = {
if (response.ok) {
// Reload files after moving
await window.loadFiles();
window.ui.showNotification('Carpeta movida', 'Carpeta movida correctamente');
window.ui.showNotification('Folder moved', 'Folder moved successfully');
return true;
} else {
let errorMessage = 'Error desconocido';
let errorMessage = 'Unknown error';
try {
const errorData = await response.json();
errorMessage = errorData.error || 'Error desconocido';
errorMessage = errorData.error || 'Unknown error';
} catch (e) {
errorMessage = 'Error al procesar la respuesta del servidor';
errorMessage = 'Error processing server response';
}
window.ui.showNotification('Error', `Error al mover la carpeta: ${errorMessage}`);
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
return false;
}
} catch (error) {
console.error('Error moving folder:', error);
window.ui.showNotification('Error', 'Error al mover la carpeta');
window.ui.showNotification('Error', 'Error moving the folder');
return false;
}
},
@@ -400,26 +400,26 @@ const fileOps = {
if (response.ok) {
window.ui.showNotification(
window.i18n ? window.i18n.t('notifications.file_renamed') : 'Archivo renombrado',
window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `Archivo renombrado a "${newName}"`
window.i18n ? window.i18n.t('notifications.file_renamed') : 'File renamed',
window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
);
return true;
} else {
const errorText = await response.text();
console.error('Error response:', errorText);
let errorMessage = 'Error desconocido';
let errorMessage = 'Unknown error';
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || response.statusText;
} catch (e) {
errorMessage = errorText || response.statusText;
}
window.ui.showNotification('Error', `Error al renombrar el archivo: ${errorMessage}`);
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
return false;
}
} catch (error) {
console.error('Error renaming file:', error);
window.ui.showNotification('Error', 'Error al renombrar el archivo');
window.ui.showNotification('Error', 'Error renaming the file');
return false;
}
},
@@ -446,13 +446,13 @@ const fileOps = {
console.log('Response status:', response.status);
if (response.ok) {
window.ui.showNotification('Carpeta renombrada', `Carpeta renombrada a "${newName}"`);
window.ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
return true;
} else {
const errorText = await response.text();
console.error('Error response:', errorText);
let errorMessage = 'Error desconocido';
let errorMessage = 'Unknown error';
try {
// Try to parse as JSON
const errorData = JSON.parse(errorText);
@@ -462,12 +462,12 @@ const fileOps = {
errorMessage = errorText || response.statusText;
}
window.ui.showNotification('Error', `Error al renombrar la carpeta: ${errorMessage}`);
window.ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
return false;
}
} catch (error) {
console.error('Error renaming folder:', error);
window.ui.showNotification('Error', 'Error al renombrar la carpeta');
window.ui.showNotification('Error', 'Error renaming the folder');
return false;
}
},
@@ -480,9 +480,9 @@ const fileOps = {
*/
async deleteFile(fileId, fileName) {
const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera',
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `¿Estás seguro de que quieres mover a la papelera el archivo "${fileName}"?`,
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar',
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
});
if (!confirmed) return false;
@@ -495,7 +495,7 @@ const fileOps = {
if (response.ok) {
window.loadFiles();
window.ui.showNotification('Archivo movido a papelera', `"${fileName}" movido a la papelera`);
window.ui.showNotification('File moved to trash', `"${fileName}" moved to trash`);
return true;
} else {
// Fallback to direct deletion if trash fails
@@ -506,16 +506,16 @@ const fileOps = {
if (fallbackResponse.ok) {
window.loadFiles();
window.ui.showNotification('Archivo eliminado', `"${fileName}" eliminado correctamente`);
window.ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar el archivo');
window.ui.showNotification('Error', 'Error deleting the file');
return false;
}
}
} catch (error) {
console.error('Error deleting file:', error);
window.ui.showNotification('Error', 'Error al eliminar el archivo');
window.ui.showNotification('Error', 'Error deleting the file');
return false;
}
},
@@ -528,9 +528,9 @@ const fileOps = {
*/
async deleteFolder(folderId, folderName) {
const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera',
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName }) : `¿Estás seguro de que quieres mover a la papelera la carpeta "${folderName}" y todo su contenido?`,
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar',
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName }) : `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
});
if (!confirmed) return false;
@@ -548,7 +548,7 @@ const fileOps = {
window.ui.updateBreadcrumb('');
}
window.loadFiles();
window.ui.showNotification('Carpeta movida a papelera', `"${folderName}" movida a la papelera`);
window.ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`);
return true;
} else {
// Fallback to direct deletion if trash fails
@@ -564,23 +564,23 @@ const fileOps = {
window.ui.updateBreadcrumb('');
}
window.loadFiles();
window.ui.showNotification('Carpeta eliminada', `"${folderName}" eliminada correctamente`);
window.ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar la carpeta');
window.ui.showNotification('Error', 'Error deleting the folder');
return false;
}
}
} catch (error) {
console.error('Error deleting folder:', error);
window.ui.showNotification('Error', 'Error al eliminar la carpeta');
window.ui.showNotification('Error', 'Error deleting the folder');
return false;
}
},
/**
* Obtener elementos de la papelera
* @returns {Promise<Array>} - Lista de elementos en la papelera
* Get trash items
* @returns {Promise<Array>} - List of trash items
*/
async getTrashItems() {
try {
@@ -601,9 +601,9 @@ const fileOps = {
},
/**
* Restaurar un elemento desde la papelera
* @param {string} trashId - ID del elemento en la papelera
* @returns {Promise<boolean>} - Éxito de la operación
* Restore an item from trash
* @param {string} trashId - Trash item ID
* @returns {Promise<boolean>} - Operation success
*/
async restoreFromTrash(trashId) {
try {
@@ -617,29 +617,29 @@ const fileOps = {
});
if (response.ok) {
window.ui.showNotification('Elemento restaurado', 'Elemento restaurado correctamente');
window.ui.showNotification('Item restored', 'Item restored successfully');
return true;
} else {
window.ui.showNotification('Error', 'Error al restaurar el elemento');
window.ui.showNotification('Error', 'Error restoring the item');
return false;
}
} catch (error) {
console.error('Error restoring item from trash:', error);
window.ui.showNotification('Error', 'Error al restaurar el elemento');
window.ui.showNotification('Error', 'Error restoring the item');
return false;
}
},
/**
* Eliminar permanentemente un elemento de la papelera
* @param {string} trashId - ID del elemento en la papelera
* @returns {Promise<boolean>} - Éxito de la operación
* Permanently delete a trash item
* @param {string} trashId - Trash item ID
* @returns {Promise<boolean>} - Operation success
*/
async deletePermanently(trashId) {
const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Eliminar permanentemente',
message: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete_msg') : '¿Estás seguro de que quieres eliminar permanentemente este elemento? Esta acción no se puede deshacer.',
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Eliminar permanentemente',
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
message: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete_msg') : 'Are you sure you want to permanently delete this item? This action cannot be undone.',
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently',
});
if (!confirmed) return false;
@@ -650,28 +650,28 @@ const fileOps = {
});
if (response.ok) {
window.ui.showNotification('Elemento eliminado', 'Elemento eliminado permanentemente');
window.ui.showNotification('Item deleted', 'Item permanently deleted');
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar el elemento');
window.ui.showNotification('Error', 'Error deleting the item');
return false;
}
} catch (error) {
console.error('Error deleting item permanently:', error);
window.ui.showNotification('Error', 'Error al eliminar el elemento');
window.ui.showNotification('Error', 'Error deleting the item');
return false;
}
},
/**
* Vaciar la papelera
* @returns {Promise<boolean>} - Éxito de la operación
* Empty the trash
* @returns {Promise<boolean>} - Operation success
*/
async emptyTrash() {
const confirmed = await showConfirmDialog({
title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Vaciar papelera',
message: window.i18n ? window.i18n.t('trash.empty_confirm') : '¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos.',
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Vaciar papelera',
title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
message: window.i18n ? window.i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash',
});
if (!confirmed) return false;
@@ -682,23 +682,23 @@ const fileOps = {
});
if (response.ok) {
window.ui.showNotification('Papelera vaciada', 'La papelera ha sido vaciada correctamente');
window.ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
return true;
} else {
window.ui.showNotification('Error', 'Error al vaciar la papelera');
window.ui.showNotification('Error', 'Error emptying the trash');
return false;
}
} catch (error) {
console.error('Error emptying trash:', error);
window.ui.showNotification('Error', 'Error al vaciar la papelera');
window.ui.showNotification('Error', 'Error emptying the trash');
return false;
}
},
/**
* Descargar un archivo
* @param {string} fileId - ID del archivo
* @param {string} fileName - Nombre del archivo
* Download a file
* @param {string} fileId - File ID
* @param {string} fileName - File name
*/
async downloadFile(fileId, fileName) {
try {
@@ -716,23 +716,23 @@ const fileOps = {
document.body.removeChild(link);
URL.revokeObjectURL(url);
} else {
window.ui.showNotification('Error', 'Error al descargar el archivo');
window.ui.showNotification('Error', 'Error downloading the file');
}
} catch (error) {
console.error('Error downloading file:', error);
window.ui.showNotification('Error', 'Error al descargar el archivo');
window.ui.showNotification('Error', 'Error downloading the file');
}
},
/**
* Descargar una carpeta como ZIP
* @param {string} folderId - ID de la carpeta
* @param {string} folderName - Nombre de la carpeta
* Download a folder as ZIP
* @param {string} folderId - Folder ID
* @param {string} folderName - Folder name
*/
async downloadFolder(folderId, folderName) {
try {
// Show notification to user
window.ui.showNotification('Preparando descarga', 'Preparando la carpeta para descargar...');
window.ui.showNotification('Preparing download', 'Preparing the folder for download...');
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
headers: getAuthHeaders()
@@ -748,11 +748,11 @@ const fileOps = {
document.body.removeChild(link);
URL.revokeObjectURL(url);
} else {
window.ui.showNotification('Error', 'Error al descargar la carpeta');
window.ui.showNotification('Error', 'Error downloading the folder');
}
} catch (error) {
console.error('Error downloading folder:', error);
window.ui.showNotification('Error', 'Error al descargar la carpeta');
window.ui.showNotification('Error', 'Error downloading the folder');
}
}
};
+5 -5
View File
@@ -214,11 +214,11 @@ const fileSharing = {
copyLinkToClipboard(url) {
try {
navigator.clipboard.writeText(url);
window.ui.showNotification('Enlace copiado', 'Enlace copiado al portapapeles');
window.ui.showNotification('Link copied', 'Link copied to clipboard');
return true;
} catch (error) {
console.error('Error copying to clipboard:', error);
window.ui.showNotification('Error', 'No se pudo copiar el enlace');
window.ui.showNotification('Error', 'Could not copy link');
return false;
}
},
@@ -229,7 +229,7 @@ const fileSharing = {
* @returns {string} - Formatted date string
*/
formatExpirationDate(dateString) {
if (!dateString) return 'Sin vencimiento';
if (!dateString) return 'No expiration';
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
@@ -252,11 +252,11 @@ const fileSharing = {
// Simulate network delay
//await new Promise(resolve => setTimeout(resolve, 800));
window.ui.showNotification('Notificación enviada', `Se envió notificación a ${recipientEmail}`);
window.ui.showNotification('Notification sent', `Notification sent to ${recipientEmail}`);
return true;
} catch (error) {
console.error('Error sending share notification:', error);
window.ui.showNotification('Error', 'No se pudo enviar la notificación');
window.ui.showNotification('Error', 'Could not send notification');
return false;
}
},
+11 -11
View File
@@ -110,10 +110,10 @@ const recent = {
filesGrid.innerHTML = '';
filesListView.innerHTML = `
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.size">Tamaño</div>
<div data-i18n="files.last_accessed">Último acceso</div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
<div data-i18n="files.last_accessed">Last accessed</div>
</div>
`;
@@ -126,8 +126,8 @@ const recent = {
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<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_hint') : 'Los archivos que abras aparecerán aquí'}</p>
<p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No recent files'}</p>
<p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
`;
filesGrid.appendChild(emptyState);
return;
@@ -143,7 +143,7 @@ const recent = {
} catch (error) {
console.error('Error displaying recent files:', error);
window.ui.showNotification('Error', 'Error al cargar archivos recientes');
window.ui.showNotification('Error', 'Error loading recent files');
}
},
@@ -157,17 +157,17 @@ const recent = {
// Determine icon and type
let iconClass = 'fas fa-file';
let iconSpecialClass = '';
let typeLabel = 'Documento';
let typeLabel = 'Document';
if (file.mime_type) {
if (file.mime_type.startsWith('image/')) {
iconClass = 'fas fa-file-image';
iconSpecialClass = 'image-icon';
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen';
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
} else if (file.mime_type.startsWith('text/')) {
iconClass = 'fas fa-file-alt';
iconSpecialClass = 'text-icon';
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto';
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
} else if (file.mime_type.startsWith('video/')) {
iconClass = 'fas fa-file-video';
iconSpecialClass = 'video-icon';
@@ -204,7 +204,7 @@ const recent = {
<i class="${iconClass}"></i>
</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
+12 -12
View File
@@ -52,7 +52,7 @@ const search = {
}
} catch (error) {
console.error('Error performing search:', error);
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
window.ui.showNotification('Error', 'Error performing search');
return { files: [], folders: [], total_count: 0 };
}
},
@@ -91,7 +91,7 @@ const search = {
}
} catch (error) {
console.error('Error performing advanced search:', error);
window.ui.showNotification('Error', 'Error al realizar la búsqueda avanzada');
window.ui.showNotification('Error', 'Error performing advanced search');
return { files: [], folders: [], total_count: 0 };
}
},
@@ -109,10 +109,10 @@ const search = {
filesGrid.innerHTML = '';
filesListView.innerHTML = `
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.size">Tamaño</div>
<div data-i18n="files.modified">Modificado</div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modified</div>
</div>
`;
@@ -120,9 +120,9 @@ const search = {
const searchHeader = document.createElement('div');
searchHeader.className = 'search-results-header';
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">
<i class="fas fa-times"></i> Limpiar búsqueda
<i class="fas fa-times"></i> Clear search
</button>
`;
filesGrid.appendChild(searchHeader);
@@ -147,7 +147,7 @@ const search = {
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<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);
return;
@@ -178,15 +178,15 @@ const search = {
});
if (response.ok) {
window.ui.showNotification('Caché limpiada', 'Caché de búsqueda limpiada correctamente');
window.ui.showNotification('Cache cleared', 'Search cache cleared successfully');
return true;
} else {
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
window.ui.showNotification('Error', 'Error clearing search cache');
return false;
}
} catch (error) {
console.error('Error clearing search cache:', error);
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
window.ui.showNotification('Error', 'Error clearing search cache');
return false;
}
}
+66 -66
View File
@@ -16,24 +16,24 @@ const ui = {
folderMenu.id = 'folder-context-menu';
folderMenu.innerHTML = `
<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 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 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 class="context-menu-separator"></div>
<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 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 class="context-menu-separator"></div>
<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>
`;
document.body.appendChild(folderMenu);
@@ -46,28 +46,28 @@ const ui = {
fileMenu.id = 'file-context-menu';
fileMenu.innerHTML = `
<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 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 class="context-menu-separator"></div>
<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 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 class="context-menu-separator"></div>
<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 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 class="context-menu-separator"></div>
<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>
`;
document.body.appendChild(fileMenu);
@@ -82,14 +82,14 @@ const ui = {
<div class="rename-dialog-content">
<div class="rename-dialog-header">
<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 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 class="rename-dialog-buttons">
<button class="btn btn-secondary" id="rename-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
<button class="btn btn-primary" id="rename-confirm-btn" data-i18n="actions.rename">Renombrar</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">Rename</button>
</div>
</div>
`;
@@ -105,19 +105,19 @@ const ui = {
<div class="rename-dialog-content">
<div class="rename-dialog-header">
<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 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 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 class="rename-dialog-buttons">
<button class="btn btn-secondary" id="move-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
<button class="btn btn-primary" id="move-confirm-btn" data-i18n="actions.move_to">Mover</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">Move</button>
</div>
</div>
`;
@@ -133,67 +133,67 @@ const ui = {
<div class="share-dialog-content">
<div class="share-dialog-header">
<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 class="shared-item-info">
<strong>Elemento:</strong> <span id="shared-item-name"></span>
<strong>Item:</strong> <span id="shared-item-name"></span>
</div>
<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>
<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">
<label for="share-password" data-i18n="dialogs.password">Contraseña (opcional):</label>
<input type="password" id="share-password" placeholder="Proteger con contraseña">
<label for="share-password" data-i18n="dialogs.password">Password (optional):</label>
<input type="password" id="share-password" placeholder="Protect with password">
</div>
<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">
</div>
<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-option">
<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 class="permission-option">
<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 class="permission-option">
<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 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">
<input type="text" id="generated-share-url" readonly>
<div class="share-link-actions">
<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 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>
</div>
</div>
</div>
<div class="share-dialog-buttons">
<button class="btn btn-secondary" id="share-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
<button class="btn btn-primary" id="share-confirm-btn" data-i18n="actions.share">Compartir</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">Share</button>
</div>
</div>
`;
@@ -228,24 +228,24 @@ const ui = {
<div class="share-dialog-content">
<div class="share-dialog-header">
<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>
<p><strong>URL:</strong> <span id="notification-share-url"></span></p>
<div class="form-group">
<label for="notification-email" data-i18n="dialogs.recipient">Destinatario:</label>
<input type="email" id="notification-email" placeholder="Correo electrónico">
<label for="notification-email" data-i18n="dialogs.recipient">Recipient:</label>
<input type="email" id="notification-email" placeholder="Email address">
</div>
<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>
</div>
<div class="share-dialog-buttons">
<button class="btn btn-secondary" id="notification-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
<button class="btn btn-primary" id="notification-send-btn" data-i18n="actions.send">Enviar</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">Send</button>
</div>
</div>
`;
@@ -402,7 +402,7 @@ const ui = {
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
// For searching, we might have a custom breadcrumb text
if (folderName && folderName.startsWith('Búsqueda:')) {
if (folderName && folderName.startsWith('Search:')) {
// We're in search mode - don't add click handler
breadcrumb.appendChild(homeItem);
return;
@@ -421,7 +421,7 @@ const ui = {
breadcrumb.appendChild(homeItem);
// If we have a subfolder, add it to the breadcrumb
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Búsqueda:')) {
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Search:')) {
const separator = document.createElement('span');
separator.className = 'breadcrumb-separator';
separator.textContent = '>';
@@ -633,14 +633,14 @@ const ui = {
* @param {Object} folder - Folder object
*/
addFolderToView(folder) {
// Verificar si la carpeta ya existe en la vista para evitar duplicados
// Check if the folder already exists in the view to avoid duplicates
if (document.querySelector(`.file-card[data-folder-id="${folder.id}"]`) ||
document.querySelector(`.file-item[data-folder-id="${folder.id}"]`)) {
console.log(`Carpeta ${folder.name} (${folder.id}) ya existe en la vista, no duplicando`);
console.log(`Folder ${folder.name} (${folder.id}) already exists in the view, not duplicating`);
return;
}
console.log(`Añadiendo carpeta a la vista: ${folder.name} (${folder.id})`);
console.log(`Adding folder to the view: ${folder.name} (${folder.id})`);
// Grid view element
const folderGridElement = document.createElement('div');
@@ -655,7 +655,7 @@ const ui = {
<i class="fas fa-folder"></i>
</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
@@ -746,7 +746,7 @@ const ui = {
if (id) {
if (isFolder) {
if (id === folder.id) {
alert("No puedes mover una carpeta a sí misma");
alert("You cannot move a folder to itself");
return;
}
await fileOps.moveFolder(id, folder.id);
@@ -758,7 +758,7 @@ const ui = {
document.getElementById('files-grid').appendChild(folderGridElement);
// List view element - Mejorado
// List view element - Improved
const folderListElement = document.createElement('div');
folderListElement.className = 'file-item';
folderListElement.dataset.folderId = folder.id;
@@ -788,7 +788,7 @@ const ui = {
});
}
// Mejorado: Estructura y clases para la vista de lista
// Improved: Structure and classes for list view
folderListElement.innerHTML = `
<div class="name-cell">
<div class="file-icon folder-icon">
@@ -796,7 +796,7 @@ const ui = {
</div>
<span>${folder.name}</span>
</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="date-cell">${formattedDate}</div>
`;
@@ -844,7 +844,7 @@ const ui = {
if (id) {
if (isFolder) {
if (id === folder.id) {
alert("No puedes mover una carpeta a sí misma");
alert("You cannot move a folder to itself");
return;
}
await fileOps.moveFolder(id, folder.id);
@@ -862,29 +862,29 @@ const ui = {
* @param {Object} file - File object
*/
addFileToView(file) {
// Verificar si el archivo ya existe en la vista para evitar duplicados
// Check if the file already exists in the view to avoid duplicates
if (document.querySelector(`.file-card[data-file-id="${file.id}"]`) ||
document.querySelector(`.file-item[data-file-id="${file.id}"]`)) {
console.log(`Archivo ${file.name} (${file.id}) ya existe en la vista, no duplicando`);
console.log(`File ${file.name} (${file.id}) already exists in the view, not duplicating`);
return;
}
console.log(`Añadiendo archivo a la vista: ${file.name} (${file.id})`);
console.log(`Adding file to the view: ${file.name} (${file.id})`);
// Determine icon and type
let iconClass = 'fas fa-file';
let iconSpecialClass = '';
let typeLabel = 'Documento';
let typeLabel = 'Document';
if (file.mime_type) {
if (file.mime_type.startsWith('image/')) {
iconClass = 'fas fa-file-image';
iconSpecialClass = 'image-icon';
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen';
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
} else if (file.mime_type.startsWith('text/')) {
iconClass = 'fas fa-file-alt';
iconSpecialClass = 'text-icon';
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto';
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
} else if (file.mime_type.startsWith('video/')) {
iconClass = 'fas fa-file-video';
iconSpecialClass = 'video-icon';
@@ -916,7 +916,7 @@ const ui = {
<i class="${iconClass}"></i>
</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;
@@ -1007,7 +1007,7 @@ const ui = {
document.getElementById('files-grid').appendChild(fileGridElement);
// List view element - Mejorado con clases específicas y diseño mejorado
// List view element - Improved with specific classes and enhanced layout
const fileListElement = document.createElement('div');
fileListElement.className = 'file-item';
fileListElement.dataset.fileId = file.id;
@@ -1238,9 +1238,9 @@ window.initRubberBandSelection = initRubberBandSelection;
* @returns {Promise<boolean>} true if confirmed, false if cancelled
*/
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Eliminar');
const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancelar');
const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirmar acción');
const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Delete');
const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancel');
const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirm action');
return new Promise((resolve) => {
// Remove any previous confirm dialog
+34 -34
View File
@@ -62,46 +62,46 @@
<div class="auth-logo-text">OxiCloud</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>
<form class="auth-form" id="login-form">
<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
type="text"
id="login-username"
class="auth-input"
data-i18n-placeholder="auth.username_placeholder"
placeholder="Ingresa tu nombre de usuario"
placeholder="Enter your username"
required
>
</div>
<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
type="password"
id="login-password"
class="auth-input"
data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa tu contraseña"
placeholder="Enter your password"
required
>
</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>
<div class="auth-toggle">
<span data-i18n="auth.no_account">¿No tienes cuenta?</span>
<span class="auth-toggle-link" id="show-register" data-i18n="auth.register">Regístrate</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">Sign up</span>
</div>
<div class="auth-toggle">
<span data-i18n="auth.admin_setup">¿Primera vez?</span>
<span class="auth-toggle-link" id="show-admin-setup" data-i18n="auth.setup">Configurar administrador</span>
<span data-i18n="auth.admin_setup">First time?</span>
<span class="auth-toggle-link" id="show-admin-setup" data-i18n="auth.setup">Set up administrator</span>
</div>
</div>
@@ -115,20 +115,20 @@
<div class="auth-logo-text">OxiCloud</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-success" id="register-success"></div>
<form class="auth-form" id="register-form">
<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
type="text"
id="register-username"
class="auth-input"
data-i18n-placeholder="auth.username_placeholder"
placeholder="Ingresa un nombre de usuario"
placeholder="Enter a username"
required
minlength="3"
maxlength="32"
@@ -142,42 +142,42 @@
id="register-email"
class="auth-input"
data-i18n-placeholder="auth.email_placeholder"
placeholder="Ingresa tu email"
placeholder="Enter your email"
required
>
</div>
<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
type="password"
id="register-password"
class="auth-input"
data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa una contraseña segura"
placeholder="Enter a secure password"
required
minlength="8"
>
</div>
<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
type="password"
id="register-password-confirm"
class="auth-input"
data-i18n-placeholder="auth.confirm_password_placeholder"
placeholder="Confirma tu contraseña"
placeholder="Confirm your password"
required
>
</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>
<div class="auth-toggle">
<span data-i18n="auth.have_account">¿Ya tienes cuenta?</span>
<span class="auth-toggle-link" id="show-login" data-i18n="auth.login">Iniciar sesión</span>
<span data-i18n="auth.have_account">Already have an account?</span>
<span class="auth-toggle-link" id="show-login" data-i18n="auth.login">Log in</span>
</div>
</div>
@@ -191,7 +191,7 @@
<div class="auth-logo-text">OxiCloud</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-step">
@@ -200,11 +200,11 @@
</div>
<div class="setup-step">
<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 class="setup-step">
<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>
@@ -213,7 +213,7 @@
<form class="auth-form" id="admin-setup-form">
<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
type="text"
id="admin-username"
@@ -224,48 +224,48 @@
</div>
<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
type="email"
id="admin-email"
class="auth-input"
data-i18n-placeholder="auth.email_placeholder"
placeholder="Ingresa el email del administrador"
placeholder="Enter the administrator email"
required
>
</div>
<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
type="password"
id="admin-password"
class="auth-input"
data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa una contraseña segura"
placeholder="Enter a secure password"
required
minlength="8"
>
</div>
<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
type="password"
id="admin-password-confirm"
class="auth-input"
data-i18n-placeholder="auth.confirm_password_placeholder"
placeholder="Confirma la contraseña"
placeholder="Confirm the password"
required
>
</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>
<div class="auth-toggle">
<span data-i18n="auth.back_to_login">¿Ya está configurado?</span>
<span class="auth-toggle-link" id="back-to-login" data-i18n="auth.login">Iniciar sesión</span>
<span data-i18n="auth.back_to_login">Already configured?</span>
<span class="auth-toggle-link" id="back-to-login" data-i18n="auth.login">Log in</span>
</div>
</div>
</div>
+10 -10
View File
@@ -8,17 +8,17 @@
<body>
<h1>OxiCloud Test Page</h1>
<div id="result"></div>
<button id="createBtn">Crear carpeta nueva</button>
<button id="listBtn">Listar carpetas</button>
<button id="createBtn">Create new folder</button>
<button id="listBtn">List folders</button>
<script>
// Función para mostrar resultados
// Function to show results
function showResult(data) {
document.getElementById('result').innerHTML =
'<pre>' + JSON.stringify(data, null, 2) + '</pre>';
}
// Función para crear carpeta
// Function to create folder
async function createFolder() {
try {
const response = await fetch('/api/folders', {
@@ -33,34 +33,34 @@
});
const data = await response.json();
console.log('Respuesta de creación de carpeta:', data);
console.log('Folder creation response:', data);
showResult(data);
return data;
} catch (error) {
console.error('Error al crear carpeta:', error);
console.error('Error creating folder:', error);
showResult({error: error.message});
return null;
}
}
// Función para listar carpetas
// Function to list folders
async function listFolders() {
try {
const response = await fetch('/api/folders');
const data = await response.json();
console.log('Listado de carpetas:', data);
console.log('Folder listing:', data);
showResult(data);
return data;
} catch (error) {
console.error('Error al listar carpetas:', error);
console.error('Error listing folders:', error);
showResult({error: error.message});
return [];
}
}
// Asignar eventos a botones
// Assign events to buttons
document.getElementById('createBtn').addEventListener('click', createFolder);
document.getElementById('listBtn').addEventListener('click', listFolders);
</script>
+10 -2
View File
@@ -1,11 +1,19 @@
{
"path_to_id": {
"/Mi Carpeta - testuser999": "7c1afffd-867f-4649-a0f6-4cc8a9979af4",
"/Mi Carpeta - test": "edfc23e0-d9f9-4cc7-b1dc-6369568b19e6",
"/Mi Carpeta - testuser": "304384d6-9a7e-4b22-a659-287957e8b667"
"/Mi Carpeta - user1": "b775c641-a698-458b-a561-42ae93073c74",
"/Mi Carpeta - testadmin": "02aed53b-f194-4e28-8680-2cc81a3584b3",
"/Mi Carpeta - testuser": "304384d6-9a7e-4b22-a659-287957e8b667",
"/Mi Carpeta - admin": "562e90aa-a3fc-4672-a0fd-9187fef5d4f1"
},
"id_to_path": {
"562e90aa-a3fc-4672-a0fd-9187fef5d4f1": "/Mi Carpeta - admin",
"7c1afffd-867f-4649-a0f6-4cc8a9979af4": "/Mi Carpeta - testuser999",
"02aed53b-f194-4e28-8680-2cc81a3584b3": "/Mi Carpeta - testadmin",
"304384d6-9a7e-4b22-a659-287957e8b667": "/Mi Carpeta - testuser",
"b775c641-a698-458b-a561-42ae93073c74": "/Mi Carpeta - user1",
"edfc23e0-d9f9-4cc7-b1dc-6369568b19e6": "/Mi Carpeta - test"
},
"version": 2
"version": 6
}