chore: translate all Spanish comments and log messages to English

This commit is contained in:
Dionisio
2026-02-12 09:41:25 +01:00
parent bc169de8f6
commit d31a413e57
89 changed files with 2480 additions and 2482 deletions
@@ -68,13 +68,13 @@ impl AuthApplicationService {
}
}
/// Configura el servicio de carpetas, necesario para crear carpetas personales
/// Configures the folder service, needed to create personal folders
pub fn with_folder_service(mut self, folder_service: Arc<dyn FolderUseCase>) -> Self {
self.folder_service = Some(folder_service);
self
}
/// Configura el servicio OIDC
/// Configures the OIDC service
pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self {
{
let mut state = self.oidc.write().unwrap();
@@ -123,12 +123,12 @@ impl AuthApplicationService {
}
pub async fn register(&self, dto: RegisterDto) -> Result<UserDto, DomainError> {
// Verificar usuario duplicado
// Check for duplicate user
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
return Err(DomainError::new(
ErrorKind::AlreadyExists,
"User",
format!("El usuario '{}' ya existe", dto.username)
format!("User '{}' already exists", dto.username)
));
}
@@ -136,62 +136,62 @@ impl AuthApplicationService {
return Err(DomainError::new(
ErrorKind::AlreadyExists,
"User",
format!("El email '{}' ya está registrado", dto.email)
format!("Email '{}' is already registered", dto.email)
));
}
// Verificar si el usuario quiere crear un admin
// Check if the user wants to create an admin
let is_admin_request = dto.username.to_lowercase() == "admin" ||
(dto.role.is_some() && dto.role.as_ref().unwrap().to_lowercase() == "admin");
// Si está intentando crear un admin, verificar si ya existen admins en el sistema
// If trying to create an admin, check if admins already exist in the system
if is_admin_request {
match self.count_admin_users().await {
Ok(admin_count) => {
// Si ya hay admins en el sistema y no estamos en instalación limpia,
// no permitimos crear otro admin desde el registro
// If there are already admins in the system and this is not a clean install,
// we do not allow creating another admin from registration
if admin_count > 0 {
// Verificar si es una instalación limpia (solo el admin predeterminado)
// Check if this is a clean install (only the default admin)
match self.count_all_users().await {
Ok(user_count) => {
// Si hay más de 2 usuarios (admin + test), no es instalación limpia
// If there are more than 2 users (admin + test), it is not a clean install
if user_count > 2 {
tracing::warn!("Intento de crear admin adicional rechazado: ya existe al menos un admin");
tracing::warn!("Attempt to create additional admin rejected: at least one admin already exists");
return Err(DomainError::new(
ErrorKind::AccessDenied,
"User",
"No se permite crear usuarios admin adicionales desde la página de registro"
"Creating additional admin users from the registration page is not allowed"
));
}
// En caso contrario, es instalación limpia y se permite el primer admin
tracing::info!("Permitiendo creación de admin en instalación limpia");
// Otherwise, it is a clean install and the first admin is allowed
tracing::info!("Allowing admin creation on clean install");
},
Err(e) => {
tracing::error!("Error al contar usuarios: {}", e);
// Por seguridad, si no podemos verificar, rechazamos la creación de admin
tracing::error!("Error counting users: {}", e);
// For security, if we cannot verify, we reject admin creation
return Err(DomainError::new(
ErrorKind::AccessDenied,
"User",
"No se permite crear usuarios admin adicionales"
"Creating additional admin users is not allowed"
));
}
}
}
},
Err(e) => {
tracing::error!("Error al contar usuarios admin: {}", e);
// Por seguridad, si no podemos verificar, rechazamos la creación de admin
tracing::error!("Error counting admin users: {}", e);
// For security, if we cannot verify, we reject admin creation
return Err(DomainError::new(
ErrorKind::AccessDenied,
"User",
"No se permite crear usuarios admin adicionales"
"Creating additional admin users is not allowed"
));
}
}
}
// Determinar rol y cuota según el tipo de usuario
// Si se proporciona un rol explícito de "admin", usar rol de administrador
// Determine role and quota based on user type
// If an explicit "admin" role is provided, use the administrator role
let role = if let Some(role_str) = &dto.role {
if role_str.to_lowercase() == "admin" {
UserRole::Admin
@@ -199,7 +199,7 @@ impl AuthApplicationService {
UserRole::User
}
} else {
// Caso especial: si el nombre es "admin", asignar rol de admin aunque no se especifique
// Special case: if the username is "admin", assign admin role even if not specified
if dto.username.to_lowercase() == "admin" {
UserRole::Admin
} else {
@@ -207,26 +207,26 @@ impl AuthApplicationService {
}
};
// Cuota según el rol: 100GB para admin, 1GB para usuarios normales
// Quota based on role: 100GB for admin, 1GB for regular users
let quota = if role == UserRole::Admin {
107374182400 // 100GB para admin
107374182400 // 100GB for admin
} else {
1024 * 1024 * 1024 // 1GB para usuarios normales
1024 * 1024 * 1024 // 1GB for regular users
};
// Validar longitud de password antes de hashear
// Validate password length before hashing
if dto.password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Password debe tener al menos 8 caracteres"
"Password must be at least 8 characters long"
));
}
// Hashear el password usando el servicio de infraestructura
// Hash the password using the infrastructure service
let password_hash = self.password_hasher.hash_password(&dto.password)?;
// Crear usuario con el hash pre-generado
// Create user with the pre-generated hash
let user = User::new(
dto.username.clone(),
dto.email,
@@ -236,13 +236,13 @@ impl AuthApplicationService {
).map_err(|e| DomainError::new(
ErrorKind::InvalidInput,
"User",
format!("Error al crear usuario: {}", e)
format!("Error creating user: {}", e)
))?;
// Guardar usuario
// Save user
let created_user = self.user_storage.create_user(user).await?;
// Crear carpeta personal para el usuario
// Create personal folder for the user
if let Some(folder_service) = &self.folder_service {
let folder_name = format!("Mi Carpeta - {}", dto.username);
@@ -252,20 +252,20 @@ impl AuthApplicationService {
}).await {
Ok(folder) => {
tracing::info!(
"Carpeta personal creada para el usuario {}: {} (ID: {})",
"Personal folder created for user {}: {} (ID: {})",
created_user.id(),
folder.name,
folder.id
);
// Aquí se podría guardar la asociación de la carpeta al usuario
// por ejemplo, en una tabla de relación carpeta-usuario
// Here we could save the folder-to-user association,
// for example, in a folder-user relationship table
},
Err(e) => {
// No fallamos el registro por un error en la creación de la carpeta
// pero lo registramos para investigación
// We don't fail registration due to a folder creation error,
// but we log it for investigation
tracing::error!(
"No se pudo crear la carpeta personal para el usuario {}: {}",
"Could not create personal folder for user {}: {}",
created_user.id(),
e
);
@@ -273,67 +273,67 @@ impl AuthApplicationService {
}
} else {
tracing::warn!(
"No se configuró el servicio de carpetas, no se puede crear carpeta personal para el usuario: {}",
"Folder service not configured, cannot create personal folder for user: {}",
created_user.id()
);
}
tracing::info!("Usuario registrado: {}", created_user.id());
tracing::info!("User registered: {}", created_user.id());
Ok(UserDto::from(created_user))
}
pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> {
// Buscar usuario
// Find user
let mut user = self.user_storage
.get_user_by_username(&dto.username)
.await
.map_err(|_| DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Credenciales inválidas"
"Invalid credentials"
))?;
// Verificar si usuario está activo
// Check if user is active
if !user.is_active() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Cuenta desactivada"
"Account deactivated"
));
}
// Verificar contraseña usando el hasher inyectado
// Verify password using the injected hasher
let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Credenciales inválidas"
"Invalid credentials"
));
}
// Actualizar último login
// Update last login
user.register_login();
self.user_storage.update_user(user.clone()).await?;
// Generar tokens usando el servicio de tokens inyectado
// Generate tokens using the injected token service
let access_token = self.token_service.generate_access_token(&user)?;
let refresh_token = self.token_service.generate_refresh_token();
// Guardar sesión
// Save session
let session = Session::new(
user.id().to_string(),
refresh_token.clone(),
None, // IP (se puede añadir desde la capa HTTP)
None, // User-Agent (se puede añadir desde la capa HTTP)
None, // IP (can be added from the HTTP layer)
None, // User-Agent (can be added from the HTTP layer)
self.token_service.refresh_token_expiry_days(),
);
self.session_storage.create_session(session).await?;
// Respuesta de autenticación
// Authentication response
Ok(AuthResponseDto {
user: UserDto::from(user),
access_token,
@@ -344,43 +344,43 @@ impl AuthApplicationService {
}
pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result<AuthResponseDto, DomainError> {
// Obtener sesión válida
// Get valid session
let session = self.session_storage
.get_session_by_refresh_token(&dto.refresh_token)
.await?;
// Verificar si la sesión está expirada o revocada
// Check if the session is expired or revoked
if session.is_expired() || session.is_revoked() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Sesión expirada o inválida"
"Session expired or invalid"
));
}
// Obtener usuario
// Get user
let user = self.user_storage
.get_user_by_id(session.user_id())
.await?;
// Verificar si usuario está activo
// Check if user is active
if !user.is_active() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Cuenta desactivada"
"Account deactivated"
));
}
// Revocar sesión actual
// Revoke current session
self.session_storage.revoke_session(session.id()).await?;
// Generar nuevos tokens
// Generate new tokens
let access_token = self.token_service.generate_access_token(&user)?;
let new_refresh_token = self.token_service.generate_refresh_token();
// Crear nueva sesión
// Create new session
let new_session = Session::new(
user.id().to_string(),
new_refresh_token.clone(),
@@ -401,67 +401,67 @@ impl AuthApplicationService {
}
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> {
// Obtener sesión
// Get session
let session = match self.session_storage.get_session_by_refresh_token(refresh_token).await {
Ok(s) => s,
// Si la sesión no existe, consideramos el logout como exitoso
// If the session doesn't exist, we consider the logout successful
Err(_) => return Ok(()),
};
// Verificar que la sesión pertenece al usuario
// Verify that the session belongs to the user
if session.user_id() != user_id {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"La sesión no pertenece al usuario"
"The session does not belong to the user"
));
}
// Revocar sesión
// Revoke session
self.session_storage.revoke_session(session.id()).await?;
Ok(())
}
pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
// Revocar todas las sesiones del usuario
// Revoke all user sessions
let revoked_count = self.session_storage.revoke_all_user_sessions(user_id).await?;
Ok(revoked_count)
}
pub async fn change_password(&self, user_id: &str, dto: ChangePasswordDto) -> Result<(), DomainError> {
// Obtener usuario
// Get user
let mut user = self.user_storage.get_user_by_id(user_id).await?;
// Verificar contraseña actual usando el hasher inyectado
// Verify current password using the injected hasher
let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Contraseña actual incorrecta"
"Current password is incorrect"
));
}
// Validar nueva contraseña
// Validate new password
if dto.new_password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Password debe tener al menos 8 caracteres"
"Password must be at least 8 characters long"
));
}
// Hashear nueva contraseña y actualizar usuario
// Hash new password and update user
let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
user.update_password_hash(new_hash);
// Guardar usuario actualizado
// Save updated user
self.user_storage.update_user(user).await?;
// Opcional: revocar todas las sesiones para forzar re-login con nueva contraseña
// Optional: revoke all sessions to force re-login with new password
self.session_storage.revoke_all_user_sessions(user_id).await?;
Ok(())
@@ -492,7 +492,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al contar usuarios administradores: {}", e)
format!("Error counting admin users: {}", e)
))?;
Ok(admin_users.len() as i64)
@@ -506,7 +506,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al contar usuarios: {}", e)
format!("Error counting users: {}", e)
))?;
Ok(all_users.len() as i64)
@@ -523,7 +523,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al eliminar usuario admin predeterminado: {}", e)
format!("Error deleting default admin user: {}", e)
))
},
Err(_) => {
@@ -545,7 +545,7 @@ impl AuthApplicationService {
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error al eliminar usuario admin predeterminado: {}", e)
format!("Error deleting default admin user: {}", e)
))?;
// 3. Create new admin user with the provided credentials but admin role
@@ -564,7 +564,7 @@ impl AuthApplicationService {
).map_err(|e| DomainError::new(
ErrorKind::InvalidInput,
"User",
format!("Error al crear usuario admin: {}", e)
format!("Error creating admin user: {}", e)
))?;
// 4. Save the new admin user
@@ -580,7 +580,7 @@ impl AuthApplicationService {
}).await {
Ok(folder) => {
tracing::info!(
"Carpeta personal creada para el admin {}: {} (ID: {})",
"Personal folder created for admin {}: {} (ID: {})",
created_user.id(),
folder.name,
folder.id
@@ -588,7 +588,7 @@ impl AuthApplicationService {
},
Err(e) => {
tracing::error!(
"No se pudo crear la carpeta personal para el admin {}: {}",
"Could not create personal folder for admin {}: {}",
created_user.id(),
e
);
@@ -596,7 +596,7 @@ impl AuthApplicationService {
}
}
tracing::info!("Admin personalizado creado: {}", created_user.id());
tracing::info!("Custom admin created: {}", created_user.id());
Ok(UserDto::from(created_user))
}
+118 -118
View File
@@ -12,52 +12,52 @@ use crate::application::ports::inbound::FolderUseCase;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
/// Errores específicos para operaciones por lotes
/// Specific errors for batch operations
#[derive(Debug, Error)]
pub enum BatchOperationError {
#[error("Error de dominio: {0}")]
#[error("Domain error: {0}")]
Domain(#[from] DomainError),
#[error("Operación cancelada: {0}")]
#[error("Operation cancelled: {0}")]
Cancelled(String),
#[error("Límite de concurrencia excedido: {0}")]
#[error("Concurrency limit exceeded: {0}")]
ConcurrencyLimit(String),
#[error("Error en operación del lote: {0} ({1} de {2} completadas)")]
#[error("Batch operation error: {0} ({1} of {2} completed)")]
PartialFailure(String, usize, usize),
#[error("Error interno: {0}")]
#[error("Internal error: {0}")]
Internal(String),
}
/// Resultado de una operación por lotes con estadísticas
/// Result of a batch operation with statistics
#[derive(Debug, Clone)]
pub struct BatchResult<T> {
/// Resultados exitosos
/// Successful results
pub successful: Vec<T>,
/// Operaciones fallidas con sus errores
/// Failed operations with their errors
pub failed: Vec<(String, String)>,
/// Estadísticas de la operación
/// Operation statistics
pub stats: BatchStats,
}
/// Estadísticas de una operación por lotes
/// Statistics of a batch operation
#[derive(Debug, Clone, Default)]
pub struct BatchStats {
/// Número total de operaciones
/// Total number of operations
pub total: usize,
/// Número de operaciones exitosas
/// Number of successful operations
pub successful: usize,
/// Número de operaciones fallidas
/// Number of failed operations
pub failed: usize,
/// Tiempo total de ejecución en milisegundos
/// Total execution time in milliseconds
pub execution_time_ms: u128,
/// Concurrencia máxima alcanzada
/// Maximum concurrency reached
pub max_concurrency: usize,
}
/// Servicio de operaciones por lotes
/// Batch operations service
pub struct BatchOperationService {
file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>,
@@ -67,14 +67,14 @@ pub struct BatchOperationService {
}
impl BatchOperationService {
/// Crea una nueva instancia del servicio de operaciones por lotes
/// Creates a new instance of the batch operations service
pub fn new(
file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>,
folder_service: Arc<FolderService>,
config: AppConfig
) -> Self {
// Limitar la concurrencia basada en la configuración
// Limit concurrency based on configuration
let max_concurrency = config.concurrency.max_concurrent_files;
Self {
@@ -86,7 +86,7 @@ impl BatchOperationService {
}
}
/// Crea una nueva instancia con la configuración por defecto
/// Creates a new instance with default configuration
pub fn default(
file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>,
@@ -95,16 +95,16 @@ impl BatchOperationService {
Self::new(file_retrieval, file_management, folder_service, AppConfig::default())
}
/// Copia múltiples archivos en paralelo
/// Copies multiple files in parallel
pub async fn copy_files(
&self,
file_ids: Vec<String>,
target_folder_id: Option<String>,
) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Iniciando copia en lote de {} archivos", file_ids.len());
info!("Starting batch copy of {} files", file_ids.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -114,30 +114,30 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await;
// Liberar el permiso explícitamente (también se libera al hacer drop)
// Release the permit explicitly (also released on drop)
drop(permit);
// Devolver el resultado junto con el ID para identificar éxitos/fallos
// Return the result along with the ID to identify successes/failures
(file_id, copy_result)
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(file) => {
@@ -151,13 +151,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Copia en lote completada: {}/{} exitosas en {}ms",
"Batch copy completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -166,16 +166,16 @@ impl BatchOperationService {
Ok(result)
}
/// Mueve múltiples archivos en paralelo
/// Moves multiple files in parallel
pub async fn move_files(
&self,
file_ids: Vec<String>,
target_folder_id: Option<String>,
) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Iniciando movimiento en lote de {} archivos", file_ids.len());
info!("Starting batch move of {} files", file_ids.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -185,30 +185,30 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let move_result = mgmt.move_file(&file_id, target_folder.clone()).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID para identificar éxitos/fallos
// Return the result along with the ID to identify successes/failures
(file_id, move_result)
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(file) => {
@@ -222,13 +222,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Movimiento en lote completado: {}/{} exitosas en {}ms",
"Batch move completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -237,15 +237,15 @@ impl BatchOperationService {
Ok(result)
}
/// Elimina múltiples archivos en paralelo
/// Deletes multiple files in parallel
pub async fn delete_files(
&self,
file_ids: Vec<String>,
) -> Result<BatchResult<String>, BatchOperationError> {
info!("Iniciando eliminación en lote de {} archivos", file_ids.len());
info!("Starting batch deletion of {} files", file_ids.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -255,30 +255,30 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let semaphore = self.semaphore.clone();
let id_clone = file_id.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let delete_result = mgmt.delete_file(&file_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID
// Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone))
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(id) => {
@@ -292,13 +292,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Eliminación en lote completada: {}/{} exitosas en {}ms",
"Batch deletion completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -307,15 +307,15 @@ impl BatchOperationService {
Ok(result)
}
/// Carga múltiples archivos en paralelo (datos en memoria)
/// Loads multiple files in parallel (data in memory)
pub async fn get_multiple_files(
&self,
file_ids: Vec<String>,
) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Iniciando carga en lote de {} archivos", file_ids.len());
info!("Starting batch load of {} files", file_ids.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -325,29 +325,29 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada archivo
// Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| {
let retrieval = self.file_retrieval.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let get_result = retrieval.get_file(&file_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID
// Return the result along with the ID
(file_id, get_result)
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (file_id, operation_result) in operation_results {
match operation_result {
Ok(file) => {
@@ -361,13 +361,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Carga en lote completada: {}/{} exitosas en {}ms",
"Batch load completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -376,16 +376,16 @@ impl BatchOperationService {
Ok(result)
}
/// Elimina múltiples carpetas en paralelo
/// Deletes multiple folders in parallel
pub async fn delete_folders(
&self,
folder_ids: Vec<String>,
_recursive: bool,
) -> Result<BatchResult<String>, BatchOperationError> {
info!("Iniciando eliminación en lote de {} carpetas", folder_ids.len());
info!("Starting batch deletion of {} folders", folder_ids.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -395,32 +395,32 @@ impl BatchOperationService {
},
};
// Definir la operación a realizar para cada carpeta
// Define the operation to perform for each folder
let operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone();
let id_clone = folder_id.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
// For both recursive and non-recursive, use the standard delete_folder method
// since FolderUseCase only has a single delete_folder method
let delete_result = folder_service.delete_folder(&folder_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado junto con el ID
// Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone))
}
});
// Ejecutar todas las operaciones en paralelo con control de concurrencia
// Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (folder_id, operation_result) in operation_results {
match operation_result {
Ok(id) => {
@@ -434,13 +434,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Eliminación en lote de carpetas completada: {}/{} exitosas en {}ms",
"Batch folder deletion completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -449,7 +449,7 @@ impl BatchOperationService {
Ok(result)
}
/// Operación genérica de lote para cualquier tipo de función asíncrona
/// Generic batch operation for any type of async function
pub async fn generic_batch_operation<T, F, Fut>(
&self,
items: Vec<T>,
@@ -460,10 +460,10 @@ impl BatchOperationService {
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<T, DomainError>> + Send + 'static,
{
info!("Iniciando operación genérica en lote con {} items", items.len());
info!("Starting generic batch operation with {} items", items.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -473,25 +473,25 @@ impl BatchOperationService {
},
};
// Convertir cada item a una tarea
// Convert each item to a task
let tasks = items.iter().map(|item| {
let item_clone = item.clone();
let op = operation.clone();
let semaphore = self.semaphore.clone();
async move {
// La función proporcionada debe manejar la adquisición del semáforo
// The provided function must handle semaphore acquisition
let op_result = op(item_clone.clone(), semaphore).await;
// Devolver el resultado junto con el item original para identificación
// Return the result along with the original item for identification
(item_clone, op_result)
}
});
// Ejecutar todas las tareas en paralelo
// Execute all tasks in parallel
let operation_results = join_all(tasks).await;
// Procesar resultados
// Process results
for (item, operation_result) in operation_results {
match operation_result {
Ok(result_item) => {
@@ -499,20 +499,20 @@ impl BatchOperationService {
result.stats.successful += 1;
}
Err(e) => {
// Convertir el item a string para el reporte de error
// Convert item to string for error reporting
result.failed.push((format!("{:?}", item), e.to_string()));
result.stats.failed += 1;
}
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Operación genérica en lote completada: {}/{} exitosas en {}ms",
"Generic batch operation completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -521,15 +521,15 @@ impl BatchOperationService {
Ok(result)
}
/// Crear múltiples carpetas en paralelo
/// Create multiple folders in parallel
pub async fn create_folders(
&self,
folders: Vec<(String, Option<String>)>, // (nombre, padre_id)
folders: Vec<(String, Option<String>)>, // (name, parent_id)
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
info!("Iniciando creación en lote de {} carpetas", folders.len());
info!("Starting batch creation of {} folders", folders.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -539,13 +539,13 @@ impl BatchOperationService {
},
};
// Definir la operación para cada carpeta
// Define the operation for each folder
let operations = folders.into_iter().map(|(name, parent_id)| {
let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let dto = crate::application::dtos::folder_dto::CreateFolderDto {
@@ -554,19 +554,19 @@ impl BatchOperationService {
};
let create_result = folder_service.create_folder(dto).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado con un identificador para los errores
// Return the result with an identifier for errors
let id = format!("{}:{}", name, parent_id.unwrap_or_default());
(id, create_result)
}
});
// Ejecutar todas las operaciones en paralelo
// Execute all operations in parallel
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (id, operation_result) in operation_results {
match operation_result {
Ok(folder) => {
@@ -580,13 +580,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Creación en lote de carpetas completada: {}/{} exitosas en {}ms",
"Batch folder creation completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -595,15 +595,15 @@ impl BatchOperationService {
Ok(result)
}
/// Obtener metadatos de múltiples carpetas en paralelo
/// Get metadata of multiple folders in parallel
pub async fn get_multiple_folders(
&self,
folder_ids: Vec<String>,
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
info!("Iniciando carga en lote de {} carpetas", folder_ids.len());
info!("Starting batch load of {} folders", folder_ids.len());
let start_time = std::time::Instant::now();
// Crear estructura para el resultado
// Create result structure
let mut result = BatchResult {
successful: Vec::new(),
failed: Vec::new(),
@@ -613,29 +613,29 @@ impl BatchOperationService {
},
};
// Definir la operación para cada carpeta
// Define the operation for each folder
let operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone();
async move {
// Adquirir permiso del semáforo
// Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap();
let get_result = folder_service.get_folder(&folder_id).await;
// Liberar el permiso explícitamente
// Release the permit explicitly
drop(permit);
// Devolver el resultado con su ID
// Return the result with its ID
(folder_id, get_result)
}
});
// Ejecutar todas las operaciones en paralelo
// Execute all operations in parallel
let operation_results = join_all(operations).await;
// Procesar los resultados
// Process the results
for (folder_id, operation_result) in operation_results {
match operation_result {
Ok(folder) => {
@@ -649,13 +649,13 @@ impl BatchOperationService {
}
}
// Completar estadísticas
// Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
.min(result.stats.total);
info!(
"Carga en lote de carpetas completada: {}/{} exitosas en {}ms",
"Batch folder load completed: {}/{} successful in {}ms",
result.stats.successful,
result.stats.total,
result.stats.execution_time_ms
@@ -673,7 +673,7 @@ mod tests {
#[tokio::test]
async fn test_generic_batch_operation() {
// Crear el servicio de batch with stubs
// Create the batch service with stubs
let batch_service = BatchOperationService::new(
Arc::new(StubFileRetrievalUseCase),
Arc::new(StubFileManagementUseCase),
@@ -683,35 +683,35 @@ mod tests {
AppConfig::default()
);
// Definir una operación genérica de prueba
// Define a generic test operation
let operation = |item: i32, semaphore: Arc<Semaphore>| async move {
// Adquirir y liberar el semáforo
// Acquire and release the semaphore
let _permit = semaphore.acquire().await.unwrap();
if item % 2 == 0 {
// Simular éxito para números pares
// Simulate success for even numbers
Ok(item * 2)
} else {
// Simular error para números impares
// Simulate error for odd numbers
Err(DomainError::validation_error("Odd number not allowed"))
}
};
// Ejecutar la operación de batch
// Execute the batch operation
let items = vec![1, 2, 3, 4, 5];
let result = batch_service.generic_batch_operation(items, operation).await.unwrap();
// Verificar los resultados
// Verify the results
assert_eq!(result.stats.total, 5);
assert_eq!(result.stats.successful, 2);
assert_eq!(result.stats.failed, 3);
// Los números pares deberían estar en los éxitos, duplicados
// Even numbers should be in successes, doubled
assert!(result.successful.contains(&4)); // 2*2
assert!(result.successful.contains(&8)); // 4*2
// Los impares deberían estar en los fallos
// Odd numbers should be in failures
assert_eq!(result.failed.len(), 3);
}
}
@@ -16,7 +16,7 @@ const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
/// Threshold above which mmap is used instead of streaming (100 MB).
const MMAP_THRESHOLD: u64 = 100 * 1024 * 1024;
/// Servicio para operaciones de recuperación de archivos
/// Service for file retrieval operations
///
/// Implements a multi-tier download strategy:
/// - Tier 0: Write-behind cache (just-uploaded files still in RAM)
@@ -29,7 +29,7 @@ fn extract_username_from_path(path: &str) -> Option<String> {
Some(parts[1].trim().to_string())
}
/// Servicio para operaciones de subida de archivos
/// Service for file upload operations
///
/// Encapsulates the three-tier upload strategy:
/// 1. **Write-Behind** (<256 KB): store in RAM, respond instantly, flush async.
@@ -78,7 +78,7 @@ impl FileUploadService {
}
}
/// Configura el servicio de uso de almacenamiento
/// Configures the storage usage service
pub fn with_storage_usage_service(
mut self,
storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>,
@@ -6,14 +6,14 @@ use crate::application::services::file_retrieval_service::FileRetrievalService;
use crate::application::services::file_management_service::FileManagementService;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
/// Factory para crear implementaciones de casos de uso de archivos
/// Factory for creating file use case implementations
pub struct AppFileUseCaseFactory {
file_read_repository: Arc<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort>,
}
impl AppFileUseCaseFactory {
/// Crea una nueva factory para casos de uso de archivos
/// Creates a new factory for file use cases
pub fn new(
file_read_repository: Arc<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort>
+43 -43
View File
@@ -7,13 +7,13 @@ use crate::application::ports::outbound::FolderStoragePort;
use crate::application::transactions::storage_transaction::StorageTransaction;
use crate::common::errors::{DomainError, ErrorKind};
/// Implementación del caso de uso para operaciones de carpetas
/// Implementation of the use case for folder operations
pub struct FolderService {
folder_storage: Arc<dyn FolderStoragePort>,
}
impl FolderService {
/// Crea un nuevo servicio de carpetas
/// Creates a new folder service
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
Self { folder_storage }
}
@@ -72,9 +72,9 @@ impl FolderService {
#[async_trait]
impl FolderUseCase for FolderService {
/// Crea una nueva carpeta
/// Creates a new folder
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
// Validación de entrada
// Input validation
if dto.name.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -83,7 +83,7 @@ impl FolderUseCase for FolderService {
));
}
// Si se proporciona un parent_id, verificar que existe
// If a parent_id is provided, verify it exists
if let Some(parent_id) = &dto.parent_id {
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists {
@@ -91,16 +91,16 @@ impl FolderUseCase for FolderService {
}
}
// Crear la carpeta
// Create the folder
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?;
// Convertir a DTO
// Convert to DTO
Ok(FolderDto::from(folder))
}
/// Obtiene una carpeta por su ID
/// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
let folder = self.folder_storage.get_folder(id)
.await
@@ -109,9 +109,9 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Obtiene una carpeta por su ruta
/// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
// Convertir la ruta de string a StoragePath
// Convert the string path to StoragePath
let storage_path = StoragePath::from_string(path);
let folder = self.folder_storage.get_folder_by_path(&storage_path)
@@ -121,39 +121,39 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Lista carpetas dentro de una carpeta padre
/// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
let folders = self.folder_storage.list_folders(parent_id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?;
// Convertir a DTOs
// Convert to DTOs
Ok(folders.into_iter().map(FolderDto::from).collect())
}
/// Lista carpetas con paginación
/// Lists folders with pagination
async fn list_folders_paginated(
&self,
parent_id: Option<&str>,
pagination: &crate::application::dtos::pagination::PaginationRequestDto
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
// Validar y ajustar la paginación
// Validate and adjust pagination
let pagination = pagination.validate_and_adjust();
// Obtener carpetas paginadas y conteo total
// Get paginated folders and total count
let (folders, total_items) = self.folder_storage.list_folders_paginated(
parent_id,
pagination.offset(),
pagination.limit(),
true // Siempre incluir total para mejor UX
true // Always include total for better UX
)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?;
// El total es necesario para calcular la paginación
// The total is needed to calculate pagination
let total = total_items.unwrap_or(folders.len());
// Convertir a PaginatedResponseDto
// Convert to PaginatedResponseDto
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
folders.into_iter().map(FolderDto::from).collect(),
pagination.page,
@@ -164,9 +164,9 @@ impl FolderUseCase for FolderService {
Ok(response)
}
/// Renombra una carpeta
/// Renames a folder
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> {
// Validación de entrada
// Input validation
if dto.name.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -175,15 +175,15 @@ impl FolderUseCase for FolderService {
));
}
// Verificar que la carpeta existe
// Verify the folder exists
let existing_folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?;
// Crear transacción para renombrar
// Create transaction for renaming
let mut transaction = StorageTransaction::new("rename_folder");
// Operación principal: renombrar carpeta
// Main operation: rename folder
// Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string();
@@ -200,7 +200,7 @@ impl FolderUseCase for FolderService {
let id_clone = id.to_string();
async move {
// En caso de fallo, restaurar el nombre original
// In case of failure, restore the original name
storage.rename_folder(&id_clone, original_name).await
.map(|_| ())
.map_err(|e| DomainError::new(
@@ -211,13 +211,13 @@ impl FolderUseCase for FolderService {
}
};
// Añadir a la transacción
// Add to the transaction
transaction.add_operation(rename_op, rollback_op);
// Ejecutar transacción
// Execute transaction
transaction.commit().await?;
// Obtener la carpeta renombrada
// Get the renamed folder
let folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?;
@@ -225,16 +225,16 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Mueve una carpeta a un nuevo padre
/// Moves a folder to a new parent
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
// Verificar que la carpeta origen existe
// Verify the source folder exists
let source_folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?;
// Si se especifica un parent_id, verificar que existe
// If a parent_id is specified, verify it exists
if let Some(parent_id) = &dto.parent_id {
// Verificar que no estamos intentando mover la carpeta a sí misma o a uno de sus descendientes
// Verify we are not trying to move the folder into itself or one of its descendants
if parent_id == id {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -243,19 +243,19 @@ impl FolderUseCase for FolderService {
));
}
// Verificar que el destino existe
// Verify the destination exists
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists {
return Err(DomainError::not_found("Folder", parent_id));
}
// TODO: Idealmente deberíamos verificar toda la jerarquía para evitar ciclos
// TODO: Ideally we should verify the entire hierarchy to prevent cycles
}
// Crear transacción para mover
// Create transaction for moving
let mut transaction = StorageTransaction::new("move_folder");
// Operación principal: mover carpeta
// Main operation: move folder
// Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string();
@@ -275,7 +275,7 @@ impl FolderUseCase for FolderService {
let id_clone = id.to_string();
async move {
// En caso de fallo, restaurar la ubicación original
// In case of failure, restore the original location
storage.move_folder(&id_clone, original_parent_id.as_deref()).await
.map(|_| ())
.map_err(|e| DomainError::new(
@@ -286,13 +286,13 @@ impl FolderUseCase for FolderService {
}
};
// Añadir a la transacción
// Add to the transaction
transaction.add_operation(move_op, rollback_op);
// Ejecutar transacción
// Execute transaction
transaction.commit().await?;
// Obtener la carpeta movida
// Get the moved folder
let folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?;
@@ -300,16 +300,16 @@ impl FolderUseCase for FolderService {
Ok(FolderDto::from(folder))
}
/// Elimina una carpeta
/// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
// Verificar que la carpeta existe
// Verify the folder exists
let _folder = self.folder_storage.get_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?;
// En una implementación real, podríamos verificar permisos, dependencias, etc.
// In a real implementation, we could verify permissions, dependencies, etc.
// Eliminar la carpeta
// Delete the folder
self.folder_storage.delete_folder(id)
.await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
+17 -17
View File
@@ -5,17 +5,17 @@ use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::application::ports::recent_ports::{RecentItemsUseCase, RecentItemsRepositoryPort};
use crate::application::dtos::recent_dto::RecentItemDto;
/// Implementación del caso de uso para gestionar elementos recientes.
/// Implementation of the use case for managing recent items.
///
/// Depende de `RecentItemsRepositoryPort` (outbound port) en lugar
/// de acceder directamente a `PgPool`, siguiendo la arquitectura hexagonal.
/// Depends on `RecentItemsRepositoryPort` (outbound port) instead
/// of accessing `PgPool` directly, following the hexagonal architecture.
pub struct RecentService {
repo: Arc<dyn RecentItemsRepositoryPort>,
max_recent_items: i32,
}
impl RecentService {
/// Crear un nuevo servicio de elementos recientes
/// Create a new recent items service
pub fn new(repo: Arc<dyn RecentItemsRepositoryPort>, max_recent_items: i32) -> Self {
Self {
repo,
@@ -26,51 +26,51 @@ impl RecentService {
#[async_trait]
impl RecentItemsUseCase for RecentService {
/// Obtener elementos recientes de un usuario
/// Get recent items for a user
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>> {
info!("Obteniendo elementos recientes para usuario: {}", user_id);
info!("Getting recent items for user: {}", user_id);
let limit_value = limit.unwrap_or(self.max_recent_items).min(self.max_recent_items);
let items = self.repo.get_recent_items(user_id, limit_value).await?;
info!("Recuperados {} elementos recientes para usuario {}", items.len(), user_id);
info!("Retrieved {} recent items for user {}", items.len(), user_id);
Ok(items)
}
/// Registrar acceso a un elemento
/// Record access to an item
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
info!("Registrando acceso a {} '{}' para usuario {}", item_type, item_id, user_id);
info!("Recording access to {} '{}' for user {}", item_type, item_id, user_id);
if item_type != "file" && item_type != "folder" {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"RecentItems",
"El tipo de elemento debe ser 'file' o 'folder'",
"Item type must be 'file' or 'folder'",
));
}
self.repo.upsert_access(user_id, item_id, item_type).await?;
self.repo.prune(user_id, self.max_recent_items).await?;
info!("Registrado correctamente acceso a {} '{}' para usuario {}", item_type, item_id, user_id);
info!("Successfully recorded access to {} '{}' for user {}", item_type, item_id, user_id);
Ok(())
}
/// Eliminar un elemento de recientes
/// Remove an item from recent
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
info!("Eliminando {} '{}' de recientes para usuario {}", item_type, item_id, user_id);
info!("Removing {} '{}' from recent for user {}", item_type, item_id, user_id);
let removed = self.repo.remove_item(user_id, item_id, item_type).await?;
info!(
"{} {} '{}' de recientes para usuario {}",
if removed { "Eliminado correctamente" } else { "No se encontró" },
if removed { "Successfully removed" } else { "Not found" },
item_type, item_id, user_id
);
Ok(removed)
}
/// Limpiar todos los elementos recientes
/// Clear all recent items
async fn clear_recent_items(&self, user_id: &str) -> Result<()> {
info!("Limpiando todos los elementos recientes para usuario {}", user_id);
info!("Clearing all recent items for user {}", user_id);
self.repo.clear_all(user_id).await?;
info!("Limpiados todos los elementos recientes para usuario {}", user_id);
info!("Cleared all recent items for user {}", user_id);
Ok(())
}
}
+93 -93
View File
@@ -14,57 +14,57 @@ use crate::application::ports::outbound::FolderStoragePort;
use crate::application::ports::storage_ports::FileReadPort;
/**
* Implementación del servicio de búsqueda para archivos y carpetas.
* Search service implementation for files and folders.
*
* Este servicio implementa la funcionalidad de búsqueda avanzada que permite
* a los usuarios encontrar archivos y carpetas basados en diversos criterios
* como nombre, tipo, fecha y tamaño. También incluye una caché para mejorar
* el rendimiento de búsquedas repetidas.
* This service implements the advanced search functionality that allows
* users to find files and folders based on various criteria
* such as name, type, date and size. It also includes a cache to improve
* the performance of repeated searches.
*/
pub struct SearchService {
/// Repositorio para operaciones con archivos
/// Repository for file operations
file_repository: Arc<dyn FileReadPort>,
/// Repositorio para operaciones con carpetas
/// Repository for folder operations
folder_repository: Arc<dyn FolderStoragePort>,
/// Caché de resultados de búsqueda con tiempo de expiración
/// Search results cache with expiration time
search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
/// Duración de validez de la caché en segundos
/// Cache validity duration in seconds
cache_ttl: u64,
/// Tamaño máximo de la caché (número de resultados almacenados)
/// Maximum cache size (number of stored results)
max_cache_size: usize,
}
/// Clave para la caché de búsqueda
/// Key for the search cache
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct SearchCacheKey {
/// Representación serializada de los criterios de búsqueda
/// Serialized representation of the search criteria
criteria_hash: String,
/// ID del usuario (para aislar búsquedas entre usuarios)
/// User ID (to isolate searches between users)
user_id: String,
}
/// Resultado de búsqueda en caché con tiempo de expiración
/// Cached search result with expiration time
struct CachedSearchResult {
/// Resultados de la búsqueda
/// Search results
results: SearchResultsDto,
/// Momento en que se creó la entrada de caché
/// Time when the cache entry was created
timestamp: Instant,
}
impl SearchService {
/**
* Crea una nueva instancia del servicio de búsqueda.
* Creates a new instance of the search service.
*
* @param file_repository Repositorio para operaciones con archivos
* @param folder_repository Repositorio para operaciones con carpetas
* @param cache_ttl Tiempo de vida de la caché en segundos (0 para desactivar)
* @param max_cache_size Tamaño máximo de la caché
* @param file_repository Repository for file operations
* @param folder_repository Repository for folder operations
* @param cache_ttl Cache time-to-live in seconds (0 to disable)
* @param max_cache_size Maximum cache size
*/
pub fn new(
file_repository: Arc<dyn FileReadPort>,
@@ -80,7 +80,7 @@ impl SearchService {
max_cache_size,
};
// Iniciar tarea de limpieza de caché si TTL > 0
// Start cache cleanup task if TTL > 0
if cache_ttl > 0 {
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl);
}
@@ -89,10 +89,10 @@ impl SearchService {
}
/**
* Inicia una tarea asíncrona para limpiar entradas expiradas de la caché.
* Starts an asynchronous task to clean up expired cache entries.
*
* @param cache_ref Referencia a la caché compartida
* @param ttl_seconds TTL en segundos
* @param cache_ref Reference to the shared cache
* @param ttl_seconds TTL in seconds
*/
fn start_cache_cleanup_task(
cache_ref: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
@@ -105,18 +105,18 @@ impl SearchService {
loop {
time::sleep(cleanup_interval).await;
// Obtener lock y limpiar entradas expiradas
// Acquire lock and clean up expired entries
if let Ok(mut cache) = cache_ref.lock() {
let now = Instant::now();
// Identificar entradas expiradas
// Identify expired entries
let expired_keys: Vec<SearchCacheKey> = cache
.iter()
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
.map(|(key, _)| key.clone())
.collect();
// Eliminar entradas expiradas
// Remove expired entries
for key in expired_keys {
cache.remove(&key);
}
@@ -126,14 +126,14 @@ impl SearchService {
}
/**
* Crea una clave de caché a partir de los criterios de búsqueda.
* Creates a cache key from the search criteria.
*
* @param criteria Criterios de búsqueda
* @param user_id ID del usuario (para aislar caché entre usuarios)
* @return Clave para la caché
* @param criteria Search criteria
* @param user_id User ID (to isolate cache between users)
* @return Cache key
*/
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey {
// Serializar criterios para generar un hash
// Serialize criteria to generate a hash
let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
SearchCacheKey {
@@ -143,13 +143,13 @@ impl SearchService {
}
/**
* Intenta obtener resultados de la caché.
* Attempts to retrieve results from the cache.
*
* @param key Clave de caché
* @return Opcionalmente, los resultados si existen y no han expirado
* @param key Cache key
* @return Optionally, the results if they exist and have not expired
*/
fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> {
// Si TTL es 0, la caché está desactivada
// If TTL is 0, the cache is disabled
if self.cache_ttl == 0 {
return None;
}
@@ -159,7 +159,7 @@ impl SearchService {
let now = Instant::now();
let ttl = Duration::from_secs(self.cache_ttl);
// Comprobar si la entrada ha expirado
// Check if the entry has expired
if now.duration_since(cached_result.timestamp) < ttl {
return Some(cached_result.results.clone());
}
@@ -170,19 +170,19 @@ impl SearchService {
}
/**
* Almacena resultados en la caché.
* Stores results in the cache.
*
* @param key Clave de caché
* @param results Resultados a almacenar
* @param key Cache key
* @param results Results to store
*/
fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) {
// Si TTL es 0, la caché está desactivada
// If TTL is 0, the cache is disabled
if self.cache_ttl == 0 {
return;
}
if let Ok(mut cache) = self.search_cache.lock() {
// Si la caché está llena, eliminar la entrada más antigua
// If the cache is full, remove the oldest entry
if cache.len() >= self.max_cache_size {
if let Some((oldest_key, _)) = cache
.iter()
@@ -192,7 +192,7 @@ impl SearchService {
}
}
// Almacenar el nuevo resultado
// Store the new result
cache.insert(key, CachedSearchResult {
results,
timestamp: Instant::now(),
@@ -201,35 +201,35 @@ impl SearchService {
}
/**
* Filtra archivos según los criterios de búsqueda.
* Filters files according to the search criteria.
*
* @param files Lista de archivos a filtrar
* @param criteria Criterios de búsqueda
* @return Archivos que cumplen con los criterios
* @param files List of files to filter
* @param criteria Search criteria
* @return Files that match the criteria
*/
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
files.into_iter()
.filter(|file| {
// Filtrar por nombre
// Filter by name
if let Some(name_query) = &criteria.name_contains {
if !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false;
}
}
// Filtrar por tipo de archivo (extensión)
// Filter by file type (extension)
if let Some(file_types) = &criteria.file_types {
if let Some(extension) = file.name.split('.').last() {
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) {
return false;
}
} else {
// No tiene extensión
// Has no extension
return false;
}
}
// Filtrar por fecha de creación
// Filter by creation date
if let Some(created_after) = criteria.created_after {
if file.created_at < created_after {
return false;
@@ -242,7 +242,7 @@ impl SearchService {
}
}
// Filtrar por fecha de modificación
// Filter by modification date
if let Some(modified_after) = criteria.modified_after {
if file.modified_at < modified_after {
return false;
@@ -255,7 +255,7 @@ impl SearchService {
}
}
// Filtrar por tamaño
// Filter by size
if let Some(min_size) = criteria.min_size {
if file.size < min_size {
return false;
@@ -274,23 +274,23 @@ impl SearchService {
}
/**
* Filtra carpetas según los criterios de búsqueda.
* Filters folders according to the search criteria.
*
* @param folders Lista de carpetas a filtrar
* @param criteria Criterios de búsqueda
* @return Carpetas que cumplen con los criterios
* @param folders List of folders to filter
* @param criteria Search criteria
* @return Folders that match the criteria
*/
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
folders.into_iter()
.filter(|folder| {
// Filtrar por nombre
// Filter by name
if let Some(name_query) = &criteria.name_contains {
if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false;
}
}
// Filtrar por fecha de creación
// Filter by creation date
if let Some(created_after) = criteria.created_after {
if folder.created_at < created_after {
return false;
@@ -303,7 +303,7 @@ impl SearchService {
}
}
// Filtrar por fecha de modificación
// Filter by modification date
if let Some(modified_after) = criteria.modified_after {
if folder.modified_at < modified_after {
return false;
@@ -322,12 +322,12 @@ impl SearchService {
}
/**
* Implementación de la búsqueda recursiva a través de carpetas.
* Implementation of recursive search through folders.
*
* @param current_folder_id ID de la carpeta actual
* @param criteria Criterios de búsqueda
* @param found_files Archivos encontrados hasta ahora
* @param found_folders Carpetas encontradas hasta ahora
* @param current_folder_id ID of the current folder
* @param criteria Search criteria
* @param found_files Files found so far
* @param found_folders Folders found so far
*/
async fn search_recursive(
&self,
@@ -337,31 +337,31 @@ impl SearchService {
found_folders: &mut Vec<FolderDto>,
) -> Result<()> {
Box::pin(async move {
// Listar archivos en la carpeta actual
// List files in the current folder
let files = self.file_repository.list_files(current_folder_id).await?;
// Filtrar archivos según criterios y agregarlos a los resultados
// Filter files according to criteria and add them to the results
let filtered_files = self.filter_files(
files.into_iter().map(FileDto::from).collect(),
criteria
);
found_files.extend(filtered_files);
// Si la búsqueda es recursiva, procesar subcarpetas
// If the search is recursive, process subfolders
if criteria.recursive {
// Listar subcarpetas
// List subfolders
let folders = self.folder_repository.list_folders(current_folder_id).await?;
// Filtrar carpetas según criterios y agregarlas a los resultados
// Filter folders according to criteria and add them to the results
let filtered_folders: Vec<FolderDto> = self.filter_folders(
folders.into_iter().map(FolderDto::from).collect(),
criteria
);
// Añadir las carpetas filtradas a los resultados
// Add filtered folders to the results
found_folders.extend(filtered_folders.iter().cloned());
// Buscar recursivamente en cada subcarpeta
// Search recursively in each subfolder
for folder in filtered_folders {
self.search_recursive(
Some(&folder.id),
@@ -380,26 +380,26 @@ impl SearchService {
#[async_trait]
impl SearchUseCase for SearchService {
/**
* Realiza una búsqueda basada en los criterios especificados.
* Performs a search based on the specified criteria.
*
* @param criteria Criterios de búsqueda
* @return Resultados de la búsqueda
* @param criteria Search criteria
* @return Search results
*/
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
// TODO: Obtener ID de usuario del contexto de autenticación
// TODO: Get user ID from the authentication context
let user_id = "default-user";
let cache_key = self.create_cache_key(&criteria, user_id);
// Intentar obtener resultados de la caché
// Try to get results from the cache
if let Some(cached_results) = self.get_from_cache(&cache_key) {
return Ok(cached_results);
}
// Inicializar colecciones para resultados
// Initialize collections for results
let mut found_files: Vec<FileDto> = Vec::new();
let mut found_folders: Vec<FolderDto> = Vec::new();
// Realizar búsqueda en la carpeta especificada o en la raíz
// Perform search in the specified folder or at the root
self.search_recursive(
criteria.folder_id.as_deref(),
&criteria,
@@ -407,29 +407,29 @@ impl SearchUseCase for SearchService {
&mut found_folders,
).await?;
// Aplicar paginación
// Apply pagination
let total_count = found_files.len() + found_folders.len();
// Ordenar por relevancia o fecha según criterios
// Por defecto, ordenamos por fecha de modificación (más reciente primero)
// Sort by relevance or date according to criteria
// By default, sort by modification date (most recent first)
found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
found_folders.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
// Aplicar límite y offset para paginación
// Apply limit and offset for pagination
let start_idx = criteria.offset.min(total_count);
let end_idx = (criteria.offset + criteria.limit).min(total_count);
let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx)
.map(|i| {
if i < found_folders.len() {
(true, i) // Es una carpeta
(true, i) // It's a folder
} else {
(false, i - found_folders.len()) // Es un archivo
(false, i - found_folders.len()) // It's a file
}
})
.collect();
// Extraer elementos paginados
// Extract paginated items
let mut paginated_folders = Vec::new();
let mut paginated_files = Vec::new();
@@ -445,7 +445,7 @@ impl SearchUseCase for SearchService {
}
}
// Crear objeto de resultados
// Create results object
let search_results = SearchResultsDto::new(
paginated_files,
paginated_folders,
@@ -454,16 +454,16 @@ impl SearchUseCase for SearchService {
Some(total_count),
);
// Almacenar en caché
// Store in cache
self.store_in_cache(cache_key, search_results.clone());
Ok(search_results)
}
/**
* Limpia la caché de resultados de búsqueda.
* Clears the search results cache.
*
* @return Resultado indicando éxito
* @return Result indicating success
*/
async fn clear_search_cache(&self) -> Result<()> {
if let Ok(mut cache) = self.search_cache.lock() {
@@ -473,9 +473,9 @@ impl SearchUseCase for SearchService {
}
}
// Implementar el caso de uso de prueba (stub)
// Implement the test use case (stub)
impl SearchService {
/// Crea una versión stub del servicio para pruebas
/// Creates a stub version of the service for testing
pub fn new_stub() -> impl SearchUseCase {
struct SearchServiceStub;
+38 -38
View File
@@ -80,7 +80,7 @@ impl ShareService {
}
}
/// Verifica que el elemento a compartir existe
/// Verifies that the item to share exists
async fn verify_item_exists(
&self,
item_id: &str,
@@ -89,13 +89,13 @@ impl ShareService {
match item_type {
ShareItemType::File => {
self.file_repository
.get_file(item_id) // Usando el método correcto del trait FileStoragePort
.get_file(item_id) // Using the correct method from the FileStoragePort trait
.await
.map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?;
}
ShareItemType::Folder => {
self.folder_repository
.get_folder(item_id) // Usando el método correcto del trait FolderStoragePort
.get_folder(item_id) // Using the correct method from the FolderStoragePort trait
.await
.map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?;
}
@@ -103,7 +103,7 @@ impl ShareService {
Ok(())
}
/// Hash de contraseña usando Argon2id (resistente a timing attacks y GPU attacks)
/// Password hash using Argon2id (resistant to timing attacks and GPU attacks)
fn hash_password(&self, password: &str) -> String {
use argon2::{Argon2, PasswordHasher};
use argon2::password_hash::SaltString;
@@ -126,20 +126,20 @@ impl ShareUseCase for ShareService {
user_id: &str,
dto: CreateShareDto,
) -> Result<ShareDto, DomainError> {
// Convertir el tipo de elemento
// Convert the item type
let item_type = ShareItemType::try_from(dto.item_type.as_str())
.map_err(|e| ShareServiceError::InvalidItemType(e.to_string()))?;
// Verificar que el elemento existe
// Verify that the item exists
self.verify_item_exists(&dto.item_id, &item_type).await?;
// Convertir el DTO de permisos si existe
// Convert the permissions DTO if it exists
let permissions = dto.permissions.map(|p| p.to_entity());
// Hash de contraseña si existe
// Hash the password if provided
let password_hash = dto.password.map(|p| self.hash_password(&p));
// Crear la entidad Share
// Create the Share entity
let share = Share::new(
dto.item_id.clone(),
item_type,
@@ -150,48 +150,48 @@ impl ShareUseCase for ShareService {
)
.map_err(|e| ShareServiceError::Validation(e.to_string()))?;
// Guardar en el repositorio
// Save to the repository
let saved_share = self
.share_repository
.save_share(&share)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&saved_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
async fn get_shared_link(&self, id: &str) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido por su ID
// Find the shared link by its ID
let share = self
.share_repository
.find_share_by_id(id)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido por su token
// Find the shared link by its token
let share = self
.share_repository
.find_share_by_token(token)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
@@ -200,17 +200,17 @@ impl ShareUseCase for ShareService {
item_id: &str,
item_type: &ShareItemType,
) -> Result<Vec<ShareDto>, DomainError> {
// Buscar todos los enlaces compartidos para el elemento
// Find all shared links for the item
let shares = self
.share_repository
.find_shares_by_item(item_id, item_type)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Filtrar los enlaces expirados
// Filter out expired links
let active_shares: Vec<Share> = shares.into_iter().filter(|s| !s.is_expired()).collect();
// Convertir las entidades a DTOs para la respuesta
// Convert the entities to DTOs for the response
let share_dtos = active_shares
.iter()
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
@@ -224,14 +224,14 @@ impl ShareUseCase for ShareService {
id: &str,
dto: UpdateShareDto,
) -> Result<ShareDto, DomainError> {
// Buscar el enlace compartido existente
// Find the existing shared link
let mut share = self
.share_repository
.find_share_by_id(id)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
// Actualizar permisos si se proporcionan
// Update permissions if provided
if let Some(permissions_dto) = dto.permissions {
let permissions = SharePermissions::new(
permissions_dto.read,
@@ -241,7 +241,7 @@ impl ShareUseCase for ShareService {
share = share.with_permissions(permissions);
}
// Actualizar contraseña si se proporciona
// Update password if provided
if let Some(password) = dto.password {
let password_hash = if password.is_empty() {
None
@@ -251,24 +251,24 @@ impl ShareUseCase for ShareService {
share = share.with_password(password_hash);
}
// Actualizar fecha de expiración si se proporciona
// Update expiration date if provided
if dto.expires_at.is_some() {
share = share.with_expiration(dto.expires_at);
}
// Guardar los cambios
// Save the changes
let updated_share = self
.share_repository
.update_share(&share)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convertir la entidad a DTO para la respuesta
// Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&updated_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
}
async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> {
// Eliminar el enlace compartido
// Delete the shared link
self.share_repository
.delete_share(id)
.await
@@ -283,23 +283,23 @@ impl ShareUseCase for ShareService {
page: usize,
per_page: usize,
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
// Calcular offset para paginación
// Calculate offset for pagination
let offset = (page - 1) * per_page;
// Buscar los enlaces compartidos del usuario
// Find the user's shared links
let (shares, total) = self
.share_repository
.find_shares_by_user(user_id, offset, per_page)
.await
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convertir las entidades a DTOs
// Convert the entities to DTOs
let share_dtos: Vec<ShareDto> = shares
.iter()
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
.collect();
// Crear el resultado paginado
// Create the paginated result
let paginated = PaginatedResponseDto::new(
share_dtos,
page,
@@ -315,19 +315,19 @@ impl ShareUseCase for ShareService {
token: &str,
password: &str,
) -> Result<bool, DomainError> {
// Buscar el enlace compartido por su token
// Find the shared link by its token
let share = self
.share_repository
.find_share_by_token(token)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Verificar la contraseña usando el port de infraestructura
// Verify the password using the infrastructure port
match share.password_hash() {
Some(hash) => {
self.password_hasher.verify_password(password, hash)
@@ -337,22 +337,22 @@ impl ShareUseCase for ShareService {
}
async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError> {
// Buscar el enlace compartido por su token
// Find the shared link by its token
let share = self
.share_repository
.find_share_by_token(token)
.await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
// Verificar si ha expirado
// Check if it has expired
if share.is_expired() {
return Err(ShareServiceError::Expired.into());
}
// Incrementar el contador de accesos
// Increment the access counter
let updated_share = share.increment_access_count();
// Guardar los cambios
// Save the changes
self.share_repository
.update_share(&updated_share)
.await
+31 -31
View File
@@ -7,69 +7,69 @@ use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath;
use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort};
/// Errores específicos del mediador de almacenamiento
/// Storage mediator specific errors
#[derive(Debug, Error)]
pub enum StorageMediatorError {
#[error("Entidad no encontrada: {0}")]
#[error("Entity not found: {0}")]
NotFound(String),
#[error("Entidad ya existe: {0}")]
#[error("Entity already exists: {0}")]
AlreadyExists(String),
#[error("Ruta inválida: {0}")]
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Error de acceso: {0}")]
#[error("Access error: {0}")]
AccessError(String),
#[error("Error interno: {0}")]
#[error("Internal error: {0}")]
InternalError(String),
#[error("Error de dominio: {0}")]
#[error("Domain error: {0}")]
DomainError(#[from] crate::common::errors::DomainError),
}
/// Tipo de resultado para las operaciones del mediador
/// Result type for mediator operations
pub type StorageMediatorResult<T> = Result<T, StorageMediatorError>;
/// Interfaz del servicio mediador entre repositorios de archivos y carpetas
/// Interface for the mediator service between file and folder repositories
#[async_trait]
pub trait StorageMediator: Send + Sync + 'static {
/// Obtiene la ruta de una carpeta por su ID
/// Gets the path of a folder by its ID
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf>;
/// Obtiene la ruta de dominio de una carpeta por su ID
/// Gets the domain path of a folder by its ID
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>;
/// Obtiene todos los detalles de una carpeta por su ID
/// Gets all details of a folder by its ID
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>;
/// Verifica si existe un archivo en una ruta específica
/// Checks if a file exists at a specific path
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
/// Verifica si existe un archivo en una ruta de dominio específica
/// Checks if a file exists at a specific domain path
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
/// Verifica si existe una carpeta en una ruta específica
/// Checks if a folder exists at a specific path
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
/// Verifica si existe una carpeta en una ruta de dominio específica
/// Checks if a folder exists at a specific domain path
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
/// Resuelve una ruta relativa a absoluta (legacy)
/// Resolves a relative path to absolute (legacy)
fn resolve_path(&self, relative_path: &Path) -> PathBuf;
/// Resuelve una ruta de dominio a una ruta física absoluta
/// Resolves a domain path to an absolute physical path
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf;
/// Crea un directorio si no existe (legacy)
/// Creates a directory if it does not exist (legacy)
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
/// Crea un directorio si no existe
/// Creates a directory if it does not exist
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>;
}
/// Implementación concreta del mediador de almacenamiento
/// Concrete implementation of the storage mediator
pub struct FileSystemStorageMediator {
pub folder_storage_port: Arc<dyn FolderStoragePort>,
pub path_service: Arc<dyn StoragePort>,
@@ -189,7 +189,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_path(path);
// Verificar si existe como archivo (no como directorio)
// Check if it exists as a file (not as a directory)
let exists = abs_path.exists() && abs_path.is_file();
Ok(exists)
@@ -198,7 +198,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path);
// Verificar si existe como archivo (no como directorio)
// Check if it exists as a file (not as a directory)
let exists = abs_path.exists() && abs_path.is_file();
Ok(exists)
@@ -207,7 +207,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_path(path);
// Verificar si existe como directorio
// Check if it exists as a directory
let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists)
@@ -216,7 +216,7 @@ impl StorageMediator for FileSystemStorageMediator {
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path);
// Verificar si existe como directorio
// Check if it exists as a directory
let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists)
@@ -236,13 +236,13 @@ impl StorageMediator for FileSystemStorageMediator {
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
let abs_path = self.resolve_path(path);
// Crear directorios si no existen
// Create directories if they don't exist
if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
} else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath(
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
format!("Path exists but is not a directory: {}", abs_path.display())
));
}
@@ -252,13 +252,13 @@ impl StorageMediator for FileSystemStorageMediator {
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
let abs_path = self.resolve_storage_path(storage_path);
// Crear directorios si no existen
// Create directories if they don't exist
if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
} else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath(
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
format!("Path exists but is not a directory: {}", abs_path.display())
));
}
+4 -14
View File
@@ -303,17 +303,7 @@ impl TrashUseCase for TrashService {
}
};
// Obtener el elemento de la papelera
info!("Retrieving trash item from repository: ID={}", trash_id);
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await;
match item_result {
Ok(Some(item)) => {
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}",
trash_id, item.item_type(), item.original_id());
// Restore based on type
match item.item_type() {
// Get the trash item
TrashedItemType::File => {
// Restore the file to its original location
let file_id = item.original_id().to_string();
@@ -428,7 +418,7 @@ impl TrashUseCase for TrashService {
}
};
// Obtener el elemento de la papelera
// Get the trash item
info!("Retrieving trash item from repository: ID={}", trash_id);
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await;
@@ -440,7 +430,7 @@ impl TrashUseCase for TrashService {
// Permanently delete based on type
match item.item_type() {
TrashedItemType::File => {
// Eliminar el archivo permanentemente
// Permanently delete the file
let file_id = item.original_id().to_string();
info!("Permanently deleting file: {}", file_id);
@@ -466,7 +456,7 @@ impl TrashUseCase for TrashService {
}
},
TrashedItemType::Folder => {
// Eliminar la carpeta permanentemente
// Permanently delete the folder
let folder_id = item.original_id().to_string();
info!("Permanently deleting folder: {}", folder_id);