chore: translate all Spanish comments and log messages to English
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<()>;
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user