chore: translate all Spanish comments and log messages to English
This commit is contained in:
@@ -15,7 +15,7 @@ use crate::application::dtos::user_dto::{
|
||||
use crate::interfaces::errors::AppError;
|
||||
|
||||
pub fn auth_routes() -> Router<Arc<AppState>> {
|
||||
// Rutas que NO requieren autenticación
|
||||
// Routes that do NOT require authentication
|
||||
let public_routes = Router::new()
|
||||
.route("/register", post(register))
|
||||
.route("/login", post(login))
|
||||
@@ -27,14 +27,14 @@ pub fn auth_routes() -> Router<Arc<AppState>> {
|
||||
.route("/oidc/callback", get(oidc_callback))
|
||||
.route("/oidc/exchange", post(oidc_exchange));
|
||||
|
||||
// Rutas que SÍ requieren autenticación - usamos route_layer para aplicar middleware
|
||||
// El middleware usará el state que se pase con .with_state() desde main.rs
|
||||
// Routes that DO require authentication - we use route_layer to apply middleware
|
||||
// The middleware will use the state passed with .with_state() from main.rs
|
||||
let protected_routes = Router::new()
|
||||
.route("/me", get(get_current_user))
|
||||
.route("/change-password", put(change_password))
|
||||
.route("/logout", post(logout));
|
||||
|
||||
// Combinar rutas públicas y protegidas
|
||||
// Combine public and protected routes
|
||||
public_routes.merge(protected_routes)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ async fn register(
|
||||
},
|
||||
None => {
|
||||
tracing::error!("Auth service not configured");
|
||||
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
|
||||
return Err(AppError::internal_error("Authentication service not configured"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,7 +153,7 @@ async fn login(
|
||||
},
|
||||
None => {
|
||||
tracing::error!("Auth service not configured");
|
||||
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
|
||||
return Err(AppError::internal_error("Authentication service not configured"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,7 +174,7 @@ async fn login(
|
||||
// Ensure the response has the expected fields
|
||||
if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() {
|
||||
tracing::error!("Login response contains empty tokens for user: {}", dto.username);
|
||||
return Err(AppError::internal_error("Error generando tokens de autenticación"));
|
||||
return Err(AppError::internal_error("Error generating authentication tokens"));
|
||||
}
|
||||
|
||||
Ok((StatusCode::OK, Json(auth_response)))
|
||||
@@ -198,7 +198,7 @@ async fn refresh_token(
|
||||
|
||||
// Normal process for real tokens
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||
|
||||
let auth_response = auth_service.auth_application_service.refresh_token(dto).await?;
|
||||
|
||||
@@ -214,37 +214,37 @@ async fn get_current_user(
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
// Normal process for all users
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||
|
||||
// Extraer y validar el token directamente
|
||||
// Extract and validate the token directly
|
||||
let token = headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
|
||||
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
|
||||
|
||||
// Validar el token y obtener claims
|
||||
// Validate the token and get claims
|
||||
let claims = auth_service.token_service.validate_token(token)
|
||||
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
|
||||
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
|
||||
|
||||
let user_id = claims.sub;
|
||||
|
||||
// Primero, actualizar las estadísticas de uso de almacenamiento
|
||||
// IMPORTANTE: Esperamos el cálculo para devolver datos actualizados
|
||||
// First, update the storage usage statistics
|
||||
// IMPORTANT: We await the calculation to return updated data
|
||||
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
|
||||
// Calcular storage de forma síncrona (esperamos el resultado)
|
||||
// Calculate storage synchronously (we await the result)
|
||||
match storage_usage_service.update_user_storage_usage(&user_id).await {
|
||||
Ok(usage) => {
|
||||
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
|
||||
},
|
||||
Err(e) => {
|
||||
// Solo log de warning, no fallar la petición completa
|
||||
// Only log a warning, don't fail the entire request
|
||||
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ahora obtener los datos del usuario CON el almacenamiento actualizado
|
||||
// Now get the user data WITH the updated storage
|
||||
let user = auth_service.auth_application_service.get_user_by_id(&user_id).await?;
|
||||
|
||||
Ok((StatusCode::OK, Json(user)))
|
||||
@@ -256,18 +256,18 @@ async fn change_password(
|
||||
Json(dto): Json<ChangePasswordDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||
|
||||
// Extraer y validar el token directamente
|
||||
// Extract and validate the token directly
|
||||
let token = headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
|
||||
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
|
||||
|
||||
// Validar el token y obtener claims
|
||||
// Validate the token and get claims
|
||||
let claims = auth_service.token_service.validate_token(token)
|
||||
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
|
||||
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
|
||||
|
||||
auth_service.auth_application_service.change_password(&claims.sub, dto).await?;
|
||||
|
||||
@@ -279,18 +279,18 @@ async fn logout(
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||
|
||||
// Extraer y validar el token directamente
|
||||
// Extract and validate the token directly
|
||||
let token = headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
|
||||
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
|
||||
|
||||
// Validar el token y obtener claims
|
||||
// Validate the token and get claims
|
||||
let claims = auth_service.token_service.validate_token(token)
|
||||
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
|
||||
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
|
||||
|
||||
// Use access token for logout (we don't have refresh token in headers)
|
||||
auth_service.auth_application_service.logout(&claims.sub, token).await?;
|
||||
@@ -314,7 +314,7 @@ async fn get_system_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||
|
||||
// Count admin users to determine if system is initialized
|
||||
let admin_count = auth_service.auth_application_service.count_admin_users().await
|
||||
|
||||
@@ -13,86 +13,86 @@ use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::FolderDto;
|
||||
use crate::interfaces::api::handlers::ApiResult;
|
||||
|
||||
/// Estado compartido para el handler de batch
|
||||
/// Shared state for the batch handler
|
||||
#[derive(Clone)]
|
||||
pub struct BatchHandlerState {
|
||||
pub batch_service: Arc<BatchOperationService>,
|
||||
}
|
||||
|
||||
/// DTO para las solicitudes de operaciones en lote de archivos
|
||||
/// DTO for batch file operation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BatchFileOperationRequest {
|
||||
/// IDs de los archivos a procesar
|
||||
/// IDs of the files to process
|
||||
pub file_ids: Vec<String>,
|
||||
/// ID de la carpeta destino (opcional)
|
||||
/// Target folder ID (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_folder_id: Option<String>,
|
||||
}
|
||||
|
||||
/// DTO para las solicitudes de operaciones en lote de carpetas
|
||||
/// DTO for batch folder operation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BatchFolderOperationRequest {
|
||||
/// IDs de las carpetas a procesar
|
||||
/// IDs of the folders to process
|
||||
pub folder_ids: Vec<String>,
|
||||
/// Si la operación debe ser recursiva
|
||||
/// Whether the operation should be recursive
|
||||
#[serde(default)]
|
||||
pub recursive: bool,
|
||||
/// ID de la carpeta destino (opcional)
|
||||
/// Target folder ID (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_folder_id: Option<String>,
|
||||
}
|
||||
|
||||
/// DTO para las solicitudes de creación en lote de carpetas
|
||||
/// DTO for batch folder creation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BatchCreateFoldersRequest {
|
||||
/// Detalles de las carpetas a crear
|
||||
/// Details of the folders to create
|
||||
pub folders: Vec<CreateFolderDetail>,
|
||||
}
|
||||
|
||||
/// Detalle para creación de una carpeta
|
||||
/// Detail for folder creation
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateFolderDetail {
|
||||
/// Nombre de la carpeta
|
||||
/// Folder name
|
||||
pub name: String,
|
||||
/// ID de la carpeta padre (opcional)
|
||||
/// Parent folder ID (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<String>,
|
||||
}
|
||||
|
||||
/// DTO para los resultados de operaciones en lote
|
||||
/// DTO for batch operation results
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BatchOperationResponse<T> {
|
||||
/// Entidades procesadas exitosamente
|
||||
/// Successfully processed entities
|
||||
pub successful: Vec<T>,
|
||||
/// Operaciones fallidas con sus mensajes de error
|
||||
/// Failed operations with their error messages
|
||||
pub failed: Vec<FailedOperation>,
|
||||
/// Estadísticas de la operación
|
||||
/// Operation statistics
|
||||
pub stats: BatchOperationStats,
|
||||
}
|
||||
|
||||
/// Operación fallida en un lote
|
||||
/// Failed operation in a batch
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FailedOperation {
|
||||
/// Identificador de la entidad que falló
|
||||
/// Identifier of the entity that failed
|
||||
pub id: String,
|
||||
/// Mensaje de error
|
||||
/// Error message
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// Estadísticas de una operación por lotes
|
||||
/// Statistics for a batch operation
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BatchOperationStats {
|
||||
/// Número total de operaciones
|
||||
/// Total number of operations
|
||||
pub total: usize,
|
||||
/// Número de operaciones exitosas
|
||||
/// Number of successful operations
|
||||
pub successful: usize,
|
||||
/// Número de operaciones fallidas
|
||||
/// Number of failed operations
|
||||
pub failed: usize,
|
||||
/// Tiempo total de ejecución en milisegundos
|
||||
/// Total execution time in milliseconds
|
||||
pub execution_time_ms: u128,
|
||||
}
|
||||
|
||||
/// Convierte BatchStats del dominio a DTO
|
||||
/// Converts domain BatchStats to DTO
|
||||
impl From<BatchStats> for BatchOperationStats {
|
||||
fn from(stats: BatchStats) -> Self {
|
||||
Self {
|
||||
@@ -104,7 +104,7 @@ impl From<BatchStats> for BatchOperationStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte BatchResult<T> del dominio a DTO
|
||||
/// Converts domain BatchResult<T> to DTO
|
||||
impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U>
|
||||
where
|
||||
U: From<T>,
|
||||
@@ -124,12 +124,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler para mover múltiples archivos en lote
|
||||
/// Handler for moving multiple files in batch
|
||||
pub async fn move_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
Json(request): Json<BatchFileOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verificar que hay archivos para procesar
|
||||
// Verify there are files to process
|
||||
if request.file_ids.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -139,35 +139,35 @@ pub async fn move_files_batch(
|
||||
).into_response());
|
||||
}
|
||||
|
||||
// Ejecutar operación de lote
|
||||
// Execute batch operation
|
||||
let result = state.batch_service
|
||||
.move_files(request.file_ids, request.target_folder_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Convertir resultado a DTO
|
||||
// Convert result to DTO
|
||||
let response: BatchOperationResponse<FileDto> = result.into();
|
||||
|
||||
// Determinar código de estado basado en los resultados
|
||||
// Determine status code based on results
|
||||
let status_code = if response.stats.failed > 0 {
|
||||
if response.stats.successful > 0 {
|
||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
||||
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST // Todas fallaron
|
||||
StatusCode::BAD_REQUEST // All failed
|
||||
}
|
||||
} else {
|
||||
StatusCode::OK // Todas exitosas
|
||||
StatusCode::OK // All successful
|
||||
};
|
||||
|
||||
Ok((status_code, Json(response)).into_response())
|
||||
}
|
||||
|
||||
/// Handler para copiar múltiples archivos en lote
|
||||
/// Handler for copying multiple files in batch
|
||||
pub async fn copy_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
Json(request): Json<BatchFileOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verificar que hay archivos para procesar
|
||||
// Verify there are files to process
|
||||
if request.file_ids.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -177,35 +177,35 @@ pub async fn copy_files_batch(
|
||||
).into_response());
|
||||
}
|
||||
|
||||
// Ejecutar operación de lote
|
||||
// Execute batch operation
|
||||
let result = state.batch_service
|
||||
.copy_files(request.file_ids, request.target_folder_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Convertir resultado a DTO
|
||||
// Convert result to DTO
|
||||
let response: BatchOperationResponse<FileDto> = result.into();
|
||||
|
||||
// Determinar código de estado basado en los resultados
|
||||
// Determine status code based on results
|
||||
let status_code = if response.stats.failed > 0 {
|
||||
if response.stats.successful > 0 {
|
||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
||||
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST // Todas fallaron
|
||||
StatusCode::BAD_REQUEST // All failed
|
||||
}
|
||||
} else {
|
||||
StatusCode::OK // Todas exitosas
|
||||
StatusCode::OK // All successful
|
||||
};
|
||||
|
||||
Ok((status_code, Json(response)).into_response())
|
||||
}
|
||||
|
||||
/// Handler para eliminar múltiples archivos en lote
|
||||
/// Handler for deleting multiple files in batch
|
||||
pub async fn delete_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
Json(request): Json<BatchFileOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verificar que hay archivos para procesar
|
||||
// Verify there are files to process
|
||||
if request.file_ids.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -215,13 +215,13 @@ pub async fn delete_files_batch(
|
||||
).into_response());
|
||||
}
|
||||
|
||||
// Ejecutar operación de lote
|
||||
// Execute batch operation
|
||||
let result = state.batch_service
|
||||
.delete_files(request.file_ids)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Crear respuesta personalizada para IDs de string
|
||||
// Create custom response for string IDs
|
||||
let response = BatchOperationResponse {
|
||||
successful: result.successful,
|
||||
failed: result.failed.into_iter()
|
||||
@@ -230,26 +230,26 @@ pub async fn delete_files_batch(
|
||||
stats: result.stats.into(),
|
||||
};
|
||||
|
||||
// Determinar código de estado basado en los resultados
|
||||
// Determine status code based on results
|
||||
let status_code = if response.stats.failed > 0 {
|
||||
if response.stats.successful > 0 {
|
||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
||||
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST // Todas fallaron
|
||||
StatusCode::BAD_REQUEST // All failed
|
||||
}
|
||||
} else {
|
||||
StatusCode::OK // Todas exitosas
|
||||
StatusCode::OK // All successful
|
||||
};
|
||||
|
||||
Ok((status_code, Json(response)).into_response())
|
||||
}
|
||||
|
||||
/// Handler para eliminar múltiples carpetas en lote
|
||||
/// Handler for deleting multiple folders in batch
|
||||
pub async fn delete_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
Json(request): Json<BatchFolderOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verificar que hay carpetas para procesar
|
||||
// Verify there are folders to process
|
||||
if request.folder_ids.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -259,13 +259,13 @@ pub async fn delete_folders_batch(
|
||||
).into_response());
|
||||
}
|
||||
|
||||
// Ejecutar operación de lote
|
||||
// Execute batch operation
|
||||
let result = state.batch_service
|
||||
.delete_folders(request.folder_ids, request.recursive)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Crear respuesta personalizada para IDs de string
|
||||
// Create custom response for string IDs
|
||||
let response = BatchOperationResponse {
|
||||
successful: result.successful,
|
||||
failed: result.failed.into_iter()
|
||||
@@ -274,26 +274,26 @@ pub async fn delete_folders_batch(
|
||||
stats: result.stats.into(),
|
||||
};
|
||||
|
||||
// Determinar código de estado basado en los resultados
|
||||
// Determine status code based on results
|
||||
let status_code = if response.stats.failed > 0 {
|
||||
if response.stats.successful > 0 {
|
||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
||||
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST // Todas fallaron
|
||||
StatusCode::BAD_REQUEST // All failed
|
||||
}
|
||||
} else {
|
||||
StatusCode::OK // Todas exitosas
|
||||
StatusCode::OK // All successful
|
||||
};
|
||||
|
||||
Ok((status_code, Json(response)).into_response())
|
||||
}
|
||||
|
||||
/// Handler para crear múltiples carpetas en lote
|
||||
/// Handler for creating multiple folders in batch
|
||||
pub async fn create_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
Json(request): Json<BatchCreateFoldersRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verificar que hay carpetas para procesar
|
||||
// Verify there are folders to process
|
||||
if request.folders.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -303,41 +303,41 @@ pub async fn create_folders_batch(
|
||||
).into_response());
|
||||
}
|
||||
|
||||
// Transformar el formato para el servicio
|
||||
// Transform the format for the service
|
||||
let folders = request.folders
|
||||
.into_iter()
|
||||
.map(|detail| (detail.name, detail.parent_id))
|
||||
.collect();
|
||||
|
||||
// Ejecutar operación de lote
|
||||
// Execute batch operation
|
||||
let result = state.batch_service
|
||||
.create_folders(folders)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Convertir resultado a DTO
|
||||
// Convert result to DTO
|
||||
let response: BatchOperationResponse<FolderDto> = result.into();
|
||||
|
||||
// Determinar código de estado basado en los resultados
|
||||
// Determine status code based on results
|
||||
let status_code = if response.stats.failed > 0 {
|
||||
if response.stats.successful > 0 {
|
||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
||||
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST // Todas fallaron
|
||||
StatusCode::BAD_REQUEST // All failed
|
||||
}
|
||||
} else {
|
||||
StatusCode::CREATED // Todas exitosas
|
||||
StatusCode::CREATED // All successful
|
||||
};
|
||||
|
||||
Ok((status_code, Json(response)).into_response())
|
||||
}
|
||||
|
||||
/// Handler para obtener múltiples archivos en lote
|
||||
/// Handler for getting multiple files in batch
|
||||
pub async fn get_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
Json(request): Json<BatchFileOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verificar que hay archivos para procesar
|
||||
// Verify there are files to process
|
||||
if request.file_ids.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -347,35 +347,35 @@ pub async fn get_files_batch(
|
||||
).into_response());
|
||||
}
|
||||
|
||||
// Ejecutar operación de lote
|
||||
// Execute batch operation
|
||||
let result = state.batch_service
|
||||
.get_multiple_files(request.file_ids)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Convertir resultado a DTO
|
||||
// Convert result to DTO
|
||||
let response: BatchOperationResponse<FileDto> = result.into();
|
||||
|
||||
// Determinar código de estado basado en los resultados
|
||||
// Determine status code based on results
|
||||
let status_code = if response.stats.failed > 0 {
|
||||
if response.stats.successful > 0 {
|
||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
||||
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST // Todas fallaron
|
||||
StatusCode::BAD_REQUEST // All failed
|
||||
}
|
||||
} else {
|
||||
StatusCode::OK // Todas exitosas
|
||||
StatusCode::OK // All successful
|
||||
};
|
||||
|
||||
Ok((status_code, Json(response)).into_response())
|
||||
}
|
||||
|
||||
/// Handler para obtener múltiples carpetas en lote
|
||||
/// Handler for getting multiple folders in batch
|
||||
pub async fn get_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
Json(request): Json<BatchFolderOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verificar que hay carpetas para procesar
|
||||
// Verify there are folders to process
|
||||
if request.folder_ids.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -385,24 +385,24 @@ pub async fn get_folders_batch(
|
||||
).into_response());
|
||||
}
|
||||
|
||||
// Ejecutar operación de lote
|
||||
// Execute batch operation
|
||||
let result = state.batch_service
|
||||
.get_multiple_folders(request.folder_ids)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Convertir resultado a DTO
|
||||
// Convert result to DTO
|
||||
let response: BatchOperationResponse<FolderDto> = result.into();
|
||||
|
||||
// Determinar código de estado basado en los resultados
|
||||
// Determine status code based on results
|
||||
let status_code = if response.stats.failed > 0 {
|
||||
if response.stats.successful > 0 {
|
||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
||||
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST // Todas fallaron
|
||||
StatusCode::BAD_REQUEST // All failed
|
||||
}
|
||||
} else {
|
||||
StatusCode::OK // Todas exitosas
|
||||
StatusCode::OK // All successful
|
||||
};
|
||||
|
||||
Ok((status_code, Json(response)).into_response())
|
||||
|
||||
@@ -11,14 +11,14 @@ use tracing::{error, info};
|
||||
use crate::application::ports::recent_ports::RecentItemsUseCase;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Parámetros de consulta para obtener elementos recientes
|
||||
/// Query parameters for getting recent items
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetRecentParams {
|
||||
#[serde(default)]
|
||||
limit: Option<i32>,
|
||||
}
|
||||
|
||||
/// Obtener elementos recientes del usuario
|
||||
/// Get user's recent items
|
||||
pub async fn get_recent_items(
|
||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -28,22 +28,22 @@ pub async fn get_recent_items(
|
||||
|
||||
match recent_service.get_recent_items(user_id, params.limit).await {
|
||||
Ok(items) => {
|
||||
info!("Recuperados {} elementos recientes para usuario", items.len());
|
||||
info!("Retrieved {} recent items for user", items.len());
|
||||
(StatusCode::OK, Json(items)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error al recuperar elementos recientes: {}", err);
|
||||
error!("Error retrieving recent items: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": format!("Fallo al recuperar elementos recientes: {}", err)
|
||||
"error": format!("Failed to retrieve recent items: {}", err)
|
||||
}))
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registrar acceso a un elemento
|
||||
/// Record access to an item
|
||||
pub async fn record_item_access(
|
||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -51,39 +51,39 @@ pub async fn record_item_access(
|
||||
) -> impl IntoResponse {
|
||||
let user_id = &auth_user.id;
|
||||
|
||||
// Validar tipo de elemento
|
||||
// Validate item type
|
||||
if item_type != "file" && item_type != "folder" {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({
|
||||
"error": "El tipo de elemento debe ser 'file' o 'folder'"
|
||||
"error": "Item type must be 'file' or 'folder'"
|
||||
}))
|
||||
).into_response();
|
||||
}
|
||||
|
||||
match recent_service.record_item_access(user_id, &item_id, &item_type).await {
|
||||
Ok(_) => {
|
||||
info!("Registrado acceso a {} '{}' en recientes", item_type, item_id);
|
||||
info!("Recorded access to {} '{}' in recents", item_type, item_id);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"message": "Acceso registrado correctamente"
|
||||
"message": "Access recorded successfully"
|
||||
}))
|
||||
).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error al registrar acceso en recientes: {}", err);
|
||||
error!("Error recording access in recents: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": format!("Fallo al registrar acceso: {}", err)
|
||||
"error": format!("Failed to record access: {}", err)
|
||||
}))
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Eliminar un elemento de recientes
|
||||
/// Remove an item from recents
|
||||
pub async fn remove_from_recent(
|
||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -94,36 +94,36 @@ pub async fn remove_from_recent(
|
||||
match recent_service.remove_from_recent(user_id, &item_id, &item_type).await {
|
||||
Ok(removed) => {
|
||||
if removed {
|
||||
info!("Eliminado {} '{}' de recientes", item_type, item_id);
|
||||
info!("Removed {} '{}' from recents", item_type, item_id);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"message": "Elemento eliminado de recientes"
|
||||
"message": "Item removed from recents"
|
||||
}))
|
||||
).into_response()
|
||||
} else {
|
||||
info!("Elemento {} '{}' no estaba en recientes", item_type, item_id);
|
||||
info!("Item {} '{}' was not in recents", item_type, item_id);
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({
|
||||
"message": "Elemento no estaba en recientes"
|
||||
"message": "Item was not in recents"
|
||||
}))
|
||||
).into_response()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error al eliminar de recientes: {}", err);
|
||||
error!("Error removing from recents: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": format!("Fallo al eliminar de recientes: {}", err)
|
||||
"error": format!("Failed to remove from recents: {}", err)
|
||||
}))
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Limpiar todos los elementos recientes
|
||||
/// Clear all recent items
|
||||
pub async fn clear_recent_items(
|
||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -132,20 +132,20 @@ pub async fn clear_recent_items(
|
||||
|
||||
match recent_service.clear_recent_items(user_id).await {
|
||||
Ok(_) => {
|
||||
info!("Limpiados todos los elementos recientes para usuario");
|
||||
info!("Cleared all recent items for user");
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"message": "Elementos recientes limpiados correctamente"
|
||||
"message": "Recent items cleared successfully"
|
||||
}))
|
||||
).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error al limpiar elementos recientes: {}", err);
|
||||
error!("Error clearing recent items: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": format!("Fallo al limpiar elementos recientes: {}", err)
|
||||
"error": format!("Failed to clear recent items: {}", err)
|
||||
}))
|
||||
).into_response()
|
||||
}
|
||||
|
||||
@@ -10,34 +10,34 @@ use crate::application::dtos::search_dto::SearchCriteriaDto;
|
||||
use crate::common::di::AppState;
|
||||
|
||||
/**
|
||||
* Manejador para las operaciones de búsqueda a través de la API.
|
||||
* Handler for search operations through the API.
|
||||
*
|
||||
* Este manejador expone endpoints relacionados con la funcionalidad de búsqueda,
|
||||
* permitiendo a los usuarios buscar archivos y carpetas usando diversos criterios.
|
||||
* This handler exposes endpoints related to search functionality,
|
||||
* allowing users to search for files and folders using various criteria.
|
||||
*/
|
||||
pub struct SearchHandler;
|
||||
|
||||
impl SearchHandler {
|
||||
/**
|
||||
* Realiza una búsqueda basada en los criterios proporcionados como parámetros de consulta.
|
||||
* Performs a search based on the criteria provided as query parameters.
|
||||
*
|
||||
* Este endpoint permite búsquedas simples directamente con parámetros URL.
|
||||
* This endpoint allows simple searches directly with URL parameters.
|
||||
*
|
||||
* @param state Estado de la aplicación con servicios
|
||||
* @param query_params Parámetros de búsqueda como query string
|
||||
* @return Respuesta HTTP con los resultados de la búsqueda
|
||||
* @param state Application state with services
|
||||
* @param query_params Search parameters as query string
|
||||
* @return HTTP response with the search results
|
||||
*/
|
||||
pub async fn search_files_get(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<SearchParams>,
|
||||
) -> impl IntoResponse {
|
||||
info!("API: Búsqueda de archivos con parámetros: {:?}", params);
|
||||
info!("API: File search with parameters: {:?}", params);
|
||||
|
||||
// Extraer el servicio de búsqueda o devolver error si no está disponible
|
||||
// Extract the search service or return error if not available
|
||||
let search_service = match &state.applications.search_service {
|
||||
Some(service) => service,
|
||||
None => {
|
||||
error!("Servicio de búsqueda no disponible");
|
||||
error!("Search service not available");
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({
|
||||
@@ -47,7 +47,7 @@ impl SearchHandler {
|
||||
}
|
||||
};
|
||||
|
||||
// Convertir parámetros de búsqueda a DTO
|
||||
// Convert search parameters to DTO
|
||||
let search_criteria = SearchCriteriaDto {
|
||||
name_contains: params.query,
|
||||
file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()),
|
||||
@@ -63,15 +63,15 @@ impl SearchHandler {
|
||||
offset: params.offset.unwrap_or(0),
|
||||
};
|
||||
|
||||
// Realizar la búsqueda
|
||||
// Perform the search
|
||||
match search_service.search(search_criteria).await {
|
||||
Ok(results) => {
|
||||
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
|
||||
info!("Search completed, {} files and {} folders found",
|
||||
results.files.len(), results.folders.len());
|
||||
(StatusCode::OK, Json(results)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error en búsqueda: {}", err);
|
||||
error!("Search error: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
@@ -83,26 +83,26 @@ impl SearchHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza una búsqueda avanzada basada en un objeto de criterios JSON completo.
|
||||
* Performs an advanced search based on a complete JSON criteria object.
|
||||
*
|
||||
* Este endpoint permite búsquedas más complejas con todos los criterios posibles
|
||||
* proporcionados en el cuerpo de la solicitud.
|
||||
* This endpoint allows more complex searches with all possible criteria
|
||||
* provided in the request body.
|
||||
*
|
||||
* @param state Estado de la aplicación con servicios
|
||||
* @param criteria Criterios de búsqueda completos
|
||||
* @return Respuesta HTTP con los resultados de la búsqueda
|
||||
* @param state Application state with services
|
||||
* @param criteria Complete search criteria
|
||||
* @return HTTP response with the search results
|
||||
*/
|
||||
pub async fn search_files_post(
|
||||
State(state): State<AppState>,
|
||||
Json(criteria): Json<SearchCriteriaDto>,
|
||||
) -> impl IntoResponse {
|
||||
info!("API: Búsqueda avanzada de archivos");
|
||||
info!("API: Advanced file search");
|
||||
|
||||
// Extraer el servicio de búsqueda o devolver error si no está disponible
|
||||
// Extract the search service or return error if not available
|
||||
let search_service = match &state.applications.search_service {
|
||||
Some(service) => service,
|
||||
None => {
|
||||
error!("Servicio de búsqueda no disponible");
|
||||
error!("Search service not available");
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({
|
||||
@@ -112,15 +112,15 @@ impl SearchHandler {
|
||||
}
|
||||
};
|
||||
|
||||
// Realizar la búsqueda
|
||||
// Perform the search
|
||||
match search_service.search(criteria).await {
|
||||
Ok(results) => {
|
||||
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
|
||||
info!("Search completed, {} files and {} folders found",
|
||||
results.files.len(), results.folders.len());
|
||||
(StatusCode::OK, Json(results)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error en búsqueda: {}", err);
|
||||
error!("Search error: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
@@ -132,24 +132,24 @@ impl SearchHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la caché de resultados de búsqueda.
|
||||
* Clears the search results cache.
|
||||
*
|
||||
* Este endpoint es útil para forzar búsquedas frescas después de cambios
|
||||
* significativos en el sistema de archivos.
|
||||
* This endpoint is useful for forcing fresh searches after significant
|
||||
* changes in the file system.
|
||||
*
|
||||
* @param state Estado de la aplicación con servicios
|
||||
* @return Respuesta HTTP indicando éxito o error
|
||||
* @param state Application state with services
|
||||
* @return HTTP response indicating success or error
|
||||
*/
|
||||
pub async fn clear_search_cache(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
info!("API: Limpiando caché de búsqueda");
|
||||
info!("API: Clearing search cache");
|
||||
|
||||
// Extraer el servicio de búsqueda o devolver error si no está disponible
|
||||
// Extract the search service or return error if not available
|
||||
let search_service = match &state.applications.search_service {
|
||||
Some(service) => service,
|
||||
None => {
|
||||
error!("Servicio de búsqueda no disponible");
|
||||
error!("Search service not available");
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({
|
||||
@@ -159,10 +159,10 @@ impl SearchHandler {
|
||||
}
|
||||
};
|
||||
|
||||
// Limpiar la caché
|
||||
// Clear the cache
|
||||
match search_service.clear_search_cache().await {
|
||||
Ok(_) => {
|
||||
info!("Caché de búsqueda limpiada correctamente");
|
||||
info!("Search cache cleared successfully");
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
@@ -171,7 +171,7 @@ impl SearchHandler {
|
||||
).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error al limpiar caché de búsqueda: {}", err);
|
||||
error!("Error clearing search cache: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
@@ -183,43 +183,43 @@ impl SearchHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parámetros de búsqueda para el endpoint GET
|
||||
/// Search parameters for the GET endpoint
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct SearchParams {
|
||||
/// Texto a buscar en nombres de archivos y carpetas
|
||||
/// Text to search for in file and folder names
|
||||
pub query: Option<String>,
|
||||
|
||||
/// Filtro por tipos de archivo (extensiones separadas por comas)
|
||||
/// Filter by file types (comma-separated extensions)
|
||||
#[serde(rename = "type")]
|
||||
pub type_filter: Option<String>,
|
||||
|
||||
/// Filtrar elementos creados después de esta fecha (timestamp)
|
||||
/// Filter items created after this date (timestamp)
|
||||
pub created_after: Option<u64>,
|
||||
|
||||
/// Filtrar elementos creados antes de esta fecha (timestamp)
|
||||
/// Filter items created before this date (timestamp)
|
||||
pub created_before: Option<u64>,
|
||||
|
||||
/// Filtrar elementos modificados después de esta fecha (timestamp)
|
||||
/// Filter items modified after this date (timestamp)
|
||||
pub modified_after: Option<u64>,
|
||||
|
||||
/// Filtrar elementos modificados antes de esta fecha (timestamp)
|
||||
/// Filter items modified before this date (timestamp)
|
||||
pub modified_before: Option<u64>,
|
||||
|
||||
/// Tamaño mínimo en bytes
|
||||
/// Minimum size in bytes
|
||||
pub min_size: Option<u64>,
|
||||
|
||||
/// Tamaño máximo en bytes
|
||||
/// Maximum size in bytes
|
||||
pub max_size: Option<u64>,
|
||||
|
||||
/// ID de carpeta para limitar la búsqueda
|
||||
/// Folder ID to limit the search scope
|
||||
pub folder_id: Option<String>,
|
||||
|
||||
/// Búsqueda recursiva en subcarpetas
|
||||
/// Recursive search in subfolders
|
||||
pub recursive: Option<bool>,
|
||||
|
||||
/// Límite de resultados para paginación
|
||||
/// Result limit for pagination
|
||||
pub limit: Option<usize>,
|
||||
|
||||
/// Desplazamiento para paginación
|
||||
/// Offset for pagination
|
||||
pub offset: Option<usize>,
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use tracing::{debug, error, warn, instrument};
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Obtiene todos los elementos en la papelera para el usuario actual
|
||||
/// Gets all items in the trash for the current user
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_trash_items(
|
||||
State(state): State<AppState>,
|
||||
@@ -19,7 +19,7 @@ pub async fn get_trash_items(
|
||||
// privilege escalation attacks.
|
||||
let effective_user = auth_user.id.clone();
|
||||
|
||||
debug!("Solicitud para listar elementos en papelera para usuario {}", effective_user);
|
||||
debug!("Request to list trash items for user {}", effective_user);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
@@ -34,11 +34,11 @@ pub async fn get_trash_items(
|
||||
|
||||
match result {
|
||||
Ok(items) => {
|
||||
debug!("Encontrados {} elementos en la papelera", items.len());
|
||||
debug!("Found {} items in trash", items.len());
|
||||
(StatusCode::OK, Json(json!(items)))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error al obtener elementos de la papelera: {:?}", e);
|
||||
error!("Error retrieving trash items: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": format!("Error retrieving trash items: {}", e)
|
||||
})))
|
||||
@@ -46,14 +46,14 @@ pub async fn get_trash_items(
|
||||
}
|
||||
}
|
||||
|
||||
/// Mueve un elemento (archivo o carpeta) a la papelera (función genérica, no usada directamente en rutas)
|
||||
/// Moves an item (file or folder) to the trash (generic function, not used directly in routes)
|
||||
#[instrument(skip_all)]
|
||||
pub async fn move_to_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path((item_type, item_id)): Path<(String, String)>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
debug!("Solicitud para mover a papelera: tipo={}, id={}, usuario={}",
|
||||
debug!("Request to move to trash: type={}, id={}, user={}",
|
||||
item_type, item_id, auth_user.id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
@@ -68,14 +68,14 @@ pub async fn move_to_trash(
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
debug!("Elemento movido a papelera con éxito");
|
||||
debug!("Item moved to trash successfully");
|
||||
(StatusCode::OK, Json(json!({
|
||||
"success": true,
|
||||
"message": "Item moved to trash successfully"
|
||||
})))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error al mover elemento a papelera: {:?}", e);
|
||||
error!("Error moving item to trash: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": format!("Error moving item to trash: {}", e)
|
||||
})))
|
||||
@@ -83,14 +83,14 @@ pub async fn move_to_trash(
|
||||
}
|
||||
}
|
||||
|
||||
/// Mueve un archivo a la papelera
|
||||
/// Moves a file to the trash
|
||||
#[instrument(skip_all)]
|
||||
pub async fn move_file_to_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(item_id): Path<String>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
debug!("Solicitud para mover archivo a papelera: id={}, usuario={}",
|
||||
debug!("Request to move file to trash: id={}, user={}",
|
||||
item_id, auth_user.id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
@@ -102,19 +102,19 @@ pub async fn move_file_to_trash(
|
||||
}
|
||||
};
|
||||
|
||||
// Especificar que es un archivo
|
||||
// Specify that it is a file
|
||||
let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
debug!("Archivo movido a papelera con éxito");
|
||||
debug!("File moved to trash successfully");
|
||||
(StatusCode::OK, Json(json!({
|
||||
"success": true,
|
||||
"message": "File moved to trash successfully"
|
||||
})))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error al mover archivo a papelera: {:?}", e);
|
||||
error!("Error moving file to trash: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": format!("Error moving file to trash: {}", e)
|
||||
})))
|
||||
@@ -122,14 +122,14 @@ pub async fn move_file_to_trash(
|
||||
}
|
||||
}
|
||||
|
||||
/// Mueve una carpeta a la papelera
|
||||
/// Moves a folder to the trash
|
||||
#[instrument(skip_all)]
|
||||
pub async fn move_folder_to_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(item_id): Path<String>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
debug!("Solicitud para mover carpeta a papelera: id={}, usuario={}",
|
||||
debug!("Request to move folder to trash: id={}, user={}",
|
||||
item_id, auth_user.id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
@@ -141,19 +141,19 @@ pub async fn move_folder_to_trash(
|
||||
}
|
||||
};
|
||||
|
||||
// Especificar que es una carpeta
|
||||
// Specify that it is a folder
|
||||
let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
debug!("Carpeta movida a papelera con éxito");
|
||||
debug!("Folder moved to trash successfully");
|
||||
(StatusCode::OK, Json(json!({
|
||||
"success": true,
|
||||
"message": "Folder moved to trash successfully"
|
||||
})))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error al mover carpeta a papelera: {:?}", e);
|
||||
error!("Error moving folder to trash: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": format!("Error moving folder to trash: {}", e)
|
||||
})))
|
||||
@@ -161,14 +161,14 @@ pub async fn move_folder_to_trash(
|
||||
}
|
||||
}
|
||||
|
||||
/// Restaura un elemento desde la papelera a su ubicación original
|
||||
/// Restores an item from the trash to its original location
|
||||
#[instrument(skip_all)]
|
||||
pub async fn restore_from_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(trash_id): Path<String>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
debug!("Solicitud para restaurar elemento {} de papelera", trash_id);
|
||||
debug!("Request to restore item {} from trash", trash_id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
@@ -182,7 +182,7 @@ pub async fn restore_from_trash(
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
debug!("Elemento restaurado con éxito");
|
||||
debug!("Item restored successfully");
|
||||
(StatusCode::OK, Json(json!({
|
||||
"success": true,
|
||||
"message": "Item restored successfully"
|
||||
@@ -199,7 +199,7 @@ pub async fn restore_from_trash(
|
||||
})));
|
||||
}
|
||||
|
||||
error!("Error al restaurar elemento de papelera: {:?}", e);
|
||||
error!("Error restoring item from trash: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": format!("Error restoring item from trash: {}", e)
|
||||
})))
|
||||
@@ -207,14 +207,14 @@ pub async fn restore_from_trash(
|
||||
}
|
||||
}
|
||||
|
||||
/// Elimina permanentemente un elemento de la papelera
|
||||
/// Permanently deletes an item from the trash
|
||||
#[instrument(skip_all)]
|
||||
pub async fn delete_permanently(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(trash_id): Path<String>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
debug!("Solicitud para eliminar permanentemente elemento {}", trash_id);
|
||||
debug!("Request to permanently delete item {}", trash_id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
@@ -228,7 +228,7 @@ pub async fn delete_permanently(
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
debug!("Elemento eliminado permanentemente");
|
||||
debug!("Item permanently deleted");
|
||||
(StatusCode::OK, Json(json!({
|
||||
"success": true,
|
||||
"message": "Item deleted permanently"
|
||||
@@ -245,7 +245,7 @@ pub async fn delete_permanently(
|
||||
})));
|
||||
}
|
||||
|
||||
error!("Error al eliminar permanentemente elemento: {:?}", e);
|
||||
error!("Error permanently deleting item: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": format!("Error deleting item permanently: {}", e)
|
||||
})))
|
||||
@@ -253,13 +253,13 @@ pub async fn delete_permanently(
|
||||
}
|
||||
}
|
||||
|
||||
/// Vacía la papelera completamente para el usuario actual
|
||||
/// Empties the trash completely for the current user
|
||||
#[instrument(skip_all)]
|
||||
pub async fn empty_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
debug!("Solicitud para vaciar papelera del usuario {}", auth_user.id);
|
||||
debug!("Request to empty trash for user {}", auth_user.id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
@@ -273,14 +273,14 @@ pub async fn empty_trash(
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
debug!("Papelera vaciada con éxito");
|
||||
debug!("Trash emptied successfully");
|
||||
(StatusCode::OK, Json(json!({
|
||||
"success": true,
|
||||
"message": "Trash emptied successfully"
|
||||
})))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error al vaciar papelera: {:?}", e);
|
||||
error!("Error emptying trash: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||
"error": format!("Error emptying trash: {}", e)
|
||||
})))
|
||||
|
||||
@@ -90,14 +90,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
||||
let favorites_service = app_state.favorites_service.clone();
|
||||
let recent_service = app_state.recent_service.clone();
|
||||
|
||||
// Inicializar el servicio de operaciones por lotes
|
||||
// Initialize the batch operations service
|
||||
let batch_service = Arc::new(BatchOperationService::default(
|
||||
file_retrieval_service.clone(),
|
||||
file_management_service.clone(),
|
||||
folder_service.clone()
|
||||
));
|
||||
|
||||
// Crear estado para el manejador de operaciones por lotes
|
||||
// Create state for the batch operations handler
|
||||
let batch_handler_state = BatchHandlerState {
|
||||
batch_service: batch_service.clone(),
|
||||
};
|
||||
@@ -154,14 +154,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
||||
// Merge the routers
|
||||
let files_router = basic_file_router.merge(file_operations_router);
|
||||
|
||||
// Crear rutas para operaciones por lotes
|
||||
// Create routes for batch operations
|
||||
let batch_router = Router::new()
|
||||
// Operaciones de archivos
|
||||
// File operations
|
||||
.route("/files/move", post(batch_handler::move_files_batch))
|
||||
.route("/files/copy", post(batch_handler::copy_files_batch))
|
||||
.route("/files/delete", post(batch_handler::delete_files_batch))
|
||||
.route("/files/get", post(batch_handler::get_files_batch))
|
||||
// Operaciones de carpetas
|
||||
// Folder operations
|
||||
.route("/folders/delete", post(batch_handler::delete_folders_batch))
|
||||
.route("/folders/create", post(batch_handler::create_folders_batch))
|
||||
.route("/folders/get", post(batch_handler::get_folders_batch))
|
||||
@@ -183,7 +183,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
};
|
||||
|
||||
// Implementaciones directas de handlers para compartir, sin depender de ShareHandler
|
||||
// Direct handler implementations for sharing, without depending on ShareHandler
|
||||
|
||||
// Create routes for shared resources management (requires auth)
|
||||
let share_router = if let Some(share_service) = share_service.clone() {
|
||||
|
||||
@@ -11,24 +11,24 @@ use crate::common::di::AppState;
|
||||
// Re-export CurrentUser from application layer for use in handlers
|
||||
pub use crate::application::dtos::user_dto::CurrentUser;
|
||||
|
||||
// Estructura para usar en extractores de Axum
|
||||
// Structure for use in Axum extractors
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthUser {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// Extractor reutilizable que obtiene el user_id del usuario autenticado.
|
||||
/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware.
|
||||
/// Reusable extractor that gets the user_id of the authenticated user.
|
||||
/// Automatically extracted from the `CurrentUser` inserted by the auth middleware.
|
||||
///
|
||||
/// Uso en handlers:
|
||||
/// Usage in handlers:
|
||||
/// ```ignore
|
||||
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CurrentUserId(pub String);
|
||||
|
||||
// Implementar FromRequestParts para AuthUser — permite usar `auth_user: AuthUser` en handlers
|
||||
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
|
||||
impl<S> FromRequestParts<S> for AuthUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
@@ -47,7 +47,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Implementar FromRequestParts para CurrentUserId — extractor ligero solo para el user_id
|
||||
// Implement FromRequestParts for CurrentUserId — lightweight extractor for user_id only
|
||||
impl<S> FromRequestParts<S> for CurrentUserId
|
||||
where
|
||||
S: Send + Sync,
|
||||
@@ -63,37 +63,37 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Error para las operaciones de autenticación
|
||||
// Error for authentication operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AuthError {
|
||||
#[error("Token no proporcionado")]
|
||||
#[error("Token not provided")]
|
||||
TokenNotProvided,
|
||||
|
||||
#[error("Token inválido: {0}")]
|
||||
#[error("Invalid token: {0}")]
|
||||
InvalidToken(String),
|
||||
|
||||
#[error("Token expirado")]
|
||||
#[error("Token expired")]
|
||||
TokenExpired,
|
||||
|
||||
#[error("Usuario no encontrado")]
|
||||
#[error("User not found")]
|
||||
UserNotFound,
|
||||
|
||||
#[error("Acceso denegado: {0}")]
|
||||
#[error("Access denied: {0}")]
|
||||
AccessDenied(String),
|
||||
|
||||
#[error("Servicio de autenticación no disponible")]
|
||||
#[error("Authentication service unavailable")]
|
||||
AuthServiceUnavailable,
|
||||
}
|
||||
|
||||
impl IntoResponse for AuthError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token no proporcionado".to_string()),
|
||||
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token not provided".to_string()),
|
||||
AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
|
||||
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()),
|
||||
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()),
|
||||
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired".to_string()),
|
||||
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "User not found".to_string()),
|
||||
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
|
||||
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Servicio de autenticación no disponible".to_string()),
|
||||
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Authentication service unavailable".to_string()),
|
||||
};
|
||||
|
||||
let body = axum::Json(serde_json::json!({
|
||||
@@ -104,24 +104,24 @@ impl IntoResponse for AuthError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware de autenticación seguro.
|
||||
/// Secure authentication middleware.
|
||||
///
|
||||
/// Valida el token JWT contra el servicio de autenticación configurado.
|
||||
/// No acepta bypasses, tokens mock, ni parámetros de URL para saltar validación.
|
||||
/// Validates the JWT token against the configured authentication service.
|
||||
/// Does not accept bypasses, mock tokens, or URL parameters to skip validation.
|
||||
pub async fn auth_middleware(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AuthError> {
|
||||
// Extraer el token Bearer del header Authorization
|
||||
// Extract the Bearer token from the Authorization header
|
||||
let token_str = headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.ok_or(AuthError::TokenNotProvided)?;
|
||||
|
||||
// Validar que el token no esté vacío
|
||||
// Validate that the token is not empty
|
||||
let token_str = token_str.trim();
|
||||
if token_str.is_empty() {
|
||||
return Err(AuthError::TokenNotProvided);
|
||||
@@ -129,7 +129,7 @@ pub async fn auth_middleware(
|
||||
|
||||
tracing::debug!("Processing authentication token");
|
||||
|
||||
// Validar el token usando el servicio de autenticación
|
||||
// Validate the token using the authentication service
|
||||
if let Some(auth_service) = state.auth_service.as_ref() {
|
||||
let token_service = &auth_service.token_service;
|
||||
match token_service.validate_token(token_str) {
|
||||
@@ -146,25 +146,25 @@ pub async fn auth_middleware(
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!("Token validation failed: {}", e);
|
||||
return Err(AuthError::InvalidToken(format!("Token inválido: {}", e)));
|
||||
return Err(AuthError::InvalidToken(format!("Invalid token: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si no hay servicio de autenticación disponible, denegar acceso
|
||||
// If no authentication service is available, deny access
|
||||
tracing::error!("Auth middleware invoked but auth service is not configured");
|
||||
Err(AuthError::AuthServiceUnavailable)
|
||||
}
|
||||
|
||||
/// Middleware para verificar que el usuario autenticado tiene rol de administrador.
|
||||
/// Middleware to verify that the authenticated user has an admin role.
|
||||
///
|
||||
/// Debe aplicarse DESPUÉS del auth_middleware, ya que depende de que
|
||||
/// `CurrentUser` esté presente en las extensiones de la request.
|
||||
/// Must be applied AFTER auth_middleware, as it depends on
|
||||
/// `CurrentUser` being present in the request extensions.
|
||||
pub async fn require_admin(
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// Obtener el CurrentUser insertado por auth_middleware
|
||||
// Get the CurrentUser inserted by auth_middleware
|
||||
if let Some(current_user) = request.extensions().get::<CurrentUser>() {
|
||||
if current_user.role == "admin" {
|
||||
tracing::debug!("Admin access granted for user: {}", current_user.username);
|
||||
@@ -175,7 +175,7 @@ pub async fn require_admin(
|
||||
tracing::warn!("Admin check failed: no authenticated user in request");
|
||||
}
|
||||
|
||||
// Acceso denegado
|
||||
let error = AuthError::AccessDenied("Se requiere rol de administrador".to_string());
|
||||
// Access denied
|
||||
let error = AuthError::AccessDenied("Admin role required".to_string());
|
||||
error.into_response()
|
||||
}
|
||||
@@ -17,39 +17,39 @@ use std::future::Future;
|
||||
use bytes::Bytes;
|
||||
use tracing::{debug, info};
|
||||
|
||||
const MAX_CACHE_ENTRIES: usize = 1000; // Máximo número de entradas en caché
|
||||
const DEFAULT_MAX_AGE: u64 = 60; // Tiempo de vida por defecto en segundos
|
||||
const MAX_CACHE_ENTRIES: usize = 1000; // Maximum number of cache entries
|
||||
const DEFAULT_MAX_AGE: u64 = 60; // Default time-to-live in seconds
|
||||
|
||||
// Definición de tipos para mayor claridad
|
||||
// Type definitions for clarity
|
||||
type CacheKey = String;
|
||||
type EntityTag = String;
|
||||
|
||||
/// Un valor almacenado en caché
|
||||
/// A cached value
|
||||
#[derive(Clone)]
|
||||
struct CacheEntry {
|
||||
/// El ETag calculado para este valor
|
||||
/// The ETag calculated for this value
|
||||
etag: EntityTag,
|
||||
/// Los datos serializados en bytes
|
||||
/// The serialized data in bytes
|
||||
data: Option<Bytes>,
|
||||
/// Las cabeceras originales
|
||||
/// The original headers
|
||||
headers: HeaderMap,
|
||||
/// Timestamp de cuando fue almacenado
|
||||
/// Timestamp of when it was stored
|
||||
timestamp: SystemTime,
|
||||
/// Tiempo de vida en segundos
|
||||
/// Time-to-live in seconds
|
||||
max_age: u64,
|
||||
}
|
||||
|
||||
/// Cache para respuestas HTTP con soporte para ETag
|
||||
/// Cache for HTTP responses with ETag support
|
||||
#[derive(Clone)]
|
||||
pub struct HttpCache {
|
||||
/// Almacenamiento de entradas en caché
|
||||
/// Cache entry storage
|
||||
cache: Arc<Mutex<HashMap<CacheKey, CacheEntry>>>,
|
||||
/// Tiempo de vida por defecto para las entradas
|
||||
/// Default time-to-live for entries
|
||||
default_max_age: u64,
|
||||
}
|
||||
|
||||
impl HttpCache {
|
||||
/// Crea una nueva instancia del caché
|
||||
/// Creates a new cache instance
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
|
||||
@@ -57,7 +57,7 @@ impl HttpCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea una nueva instancia con un tiempo de vida especificado
|
||||
/// Creates a new instance with a specified time-to-live
|
||||
pub fn with_max_age(max_age: u64) -> Self {
|
||||
Self {
|
||||
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
|
||||
@@ -65,12 +65,12 @@ impl HttpCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene estadísticas del caché
|
||||
/// Gets cache statistics
|
||||
pub fn stats(&self) -> (usize, usize) {
|
||||
let lock = self.cache.lock().unwrap();
|
||||
let total = lock.len();
|
||||
|
||||
// Contar entradas válidas
|
||||
// Count valid entries
|
||||
let _now = SystemTime::now();
|
||||
let valid = lock.values().filter(|entry| {
|
||||
match entry.timestamp.elapsed() {
|
||||
@@ -82,12 +82,12 @@ impl HttpCache {
|
||||
(total, valid)
|
||||
}
|
||||
|
||||
/// Limpia entradas expiradas
|
||||
/// Cleans up expired entries
|
||||
pub fn cleanup(&self) -> usize {
|
||||
let mut lock = self.cache.lock().unwrap();
|
||||
let initial_count = lock.len();
|
||||
|
||||
// Eliminar entradas expiradas
|
||||
// Remove expired entries
|
||||
let _now = SystemTime::now();
|
||||
lock.retain(|_, entry| {
|
||||
match entry.timestamp.elapsed() {
|
||||
@@ -102,18 +102,18 @@ impl HttpCache {
|
||||
removed
|
||||
}
|
||||
|
||||
/// Establece una entrada en el caché
|
||||
/// Sets an entry in the cache
|
||||
fn set(&self, key: &str, etag: EntityTag, data: Option<Bytes>, headers: HeaderMap, max_age: Option<u64>) {
|
||||
let mut lock = self.cache.lock().unwrap();
|
||||
|
||||
// Aplicar política de eviction si el caché está lleno
|
||||
// Apply eviction policy if the cache is full
|
||||
if lock.len() >= MAX_CACHE_ENTRIES {
|
||||
debug!("Cache full, removing oldest entries");
|
||||
// Eliminar el 10% de las entradas más antiguas
|
||||
// Remove the oldest 10% of entries
|
||||
self.evict_oldest(&mut lock, MAX_CACHE_ENTRIES / 10);
|
||||
}
|
||||
|
||||
// Almacenar la nueva entrada
|
||||
// Store the new entry
|
||||
lock.insert(key.to_string(), CacheEntry {
|
||||
etag,
|
||||
data,
|
||||
@@ -123,30 +123,30 @@ impl HttpCache {
|
||||
});
|
||||
}
|
||||
|
||||
/// Elimina las entradas más antiguas del caché
|
||||
/// Removes the oldest entries from the cache
|
||||
fn evict_oldest(&self, cache: &mut HashMap<CacheKey, CacheEntry>, count: usize) {
|
||||
// Ordenar por timestamp
|
||||
// Sort by timestamp
|
||||
let mut entries: Vec<(CacheKey, SystemTime)> = cache
|
||||
.iter()
|
||||
.map(|(key, entry)| (key.clone(), entry.timestamp))
|
||||
.collect();
|
||||
|
||||
// Ordenar por timestamp (más antiguo primero)
|
||||
// Sort by timestamp (oldest first)
|
||||
entries.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
|
||||
// Eliminar las entradas más antiguas
|
||||
// Remove the oldest entries
|
||||
for (key, _) in entries.iter().take(count) {
|
||||
cache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene una entrada del caché
|
||||
/// Gets an entry from the cache
|
||||
fn get(&self, key: &str) -> Option<CacheEntry> {
|
||||
let lock = self.cache.lock().unwrap();
|
||||
|
||||
// Buscar la entrada
|
||||
// Look up the entry
|
||||
if let Some(entry) = lock.get(key) {
|
||||
// Verificar si ha expirado
|
||||
// Check if it has expired
|
||||
match entry.timestamp.elapsed() {
|
||||
Ok(elapsed) if elapsed.as_secs() < entry.max_age => {
|
||||
// Entry is still valid
|
||||
@@ -162,9 +162,9 @@ impl HttpCache {
|
||||
None
|
||||
}
|
||||
|
||||
/// Genera un ETag simple para un bloque de bytes
|
||||
/// Generates a simple ETag for a block of bytes
|
||||
fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
|
||||
// Calcular hash
|
||||
// Calculate hash
|
||||
let mut hasher = DefaultHasher::new();
|
||||
bytes.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
@@ -173,7 +173,7 @@ impl HttpCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware de caché HTTP
|
||||
/// HTTP cache middleware
|
||||
pub async fn cache_middleware<T>(
|
||||
cache: HttpCache,
|
||||
cache_key: &str,
|
||||
@@ -184,65 +184,65 @@ pub async fn cache_middleware<T>(
|
||||
where
|
||||
T: Serialize
|
||||
{
|
||||
// Solo aplicar caché para solicitudes GET
|
||||
// Only apply cache for GET requests
|
||||
if req.method() != Method::GET {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
|
||||
// Verificar si la respuesta está en caché
|
||||
// Check if the response is cached
|
||||
let if_none_match = req.headers()
|
||||
.get("if-none-match")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
// Si hay una entrada en caché
|
||||
// If there is a cache entry
|
||||
if let Some(cache_entry) = cache.get(cache_key) {
|
||||
// Comprobar si el cliente ya tiene la versión actualizada
|
||||
// Check if the client already has the updated version
|
||||
if let Some(client_etag) = if_none_match {
|
||||
if client_etag == cache_entry.etag {
|
||||
// El cliente tiene la versión más reciente, enviar 304 Not Modified
|
||||
// The client has the most recent version, send 304 Not Modified
|
||||
debug!("Cache hit (304) for key: {}", cache_key);
|
||||
return Ok(create_not_modified_response(&cache_entry));
|
||||
}
|
||||
}
|
||||
|
||||
// El cliente necesita la versión actualizada
|
||||
// The client needs the updated version
|
||||
if let Some(data) = &cache_entry.data {
|
||||
debug!("Cache hit (200) for key: {}", cache_key);
|
||||
|
||||
// Crear respuesta con los datos en caché
|
||||
// Create response with cached data
|
||||
let mut response = Response::new(Body::from(data.clone()));
|
||||
|
||||
// Copiar cabeceras originales
|
||||
// Copy original headers
|
||||
for (key, value) in &cache_entry.headers {
|
||||
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
|
||||
response.headers_mut().insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Añadir cabeceras de caché
|
||||
// Add cache headers
|
||||
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
// No está en caché o ha expirado, continuar con el middleware
|
||||
// Not cached or expired, continue with the middleware
|
||||
debug!("Cache miss for key: {}", cache_key);
|
||||
let response = next.run(req).await;
|
||||
|
||||
// No cachear errores
|
||||
// Don't cache errors
|
||||
if !response.status().is_success() {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
// Convertir la respuesta para calcular el ETag
|
||||
// Convert the response to calculate the ETag
|
||||
let (parts, _body) = response.into_parts();
|
||||
let bytes = axum::body::to_bytes(_body, 1024 * 1024 * 10).await.unwrap_or_default();
|
||||
|
||||
// Calcular ETag
|
||||
// Calculate ETag
|
||||
let etag = cache.calculate_etag_for_bytes(&bytes);
|
||||
|
||||
// Guardar en caché
|
||||
// Save to cache
|
||||
cache.set(
|
||||
cache_key,
|
||||
etag.clone(),
|
||||
@@ -251,26 +251,26 @@ where
|
||||
max_age
|
||||
);
|
||||
|
||||
// Crear la respuesta con ETag
|
||||
// Create the response with ETag
|
||||
let mut response = Response::from_parts(parts, Body::from(bytes));
|
||||
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age));
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Crea una respuesta 304 Not Modified
|
||||
/// Creates a 304 Not Modified response
|
||||
fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::NOT_MODIFIED)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
// Copiar cabeceras de caché
|
||||
// Copy cache headers
|
||||
if let Some(cache_control) = entry.headers.get("cache-control") {
|
||||
response.headers_mut().insert("cache-control", cache_control.clone());
|
||||
}
|
||||
|
||||
// Añadir ETag
|
||||
// Add ETag
|
||||
response.headers_mut().insert(
|
||||
"etag",
|
||||
HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static(""))
|
||||
@@ -279,22 +279,22 @@ fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
|
||||
response
|
||||
}
|
||||
|
||||
/// Configura las cabeceras de caché para una respuesta
|
||||
/// Configures cache headers for a response
|
||||
fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
|
||||
// Añadir ETag
|
||||
// Add ETag
|
||||
response.headers_mut().insert(
|
||||
"etag",
|
||||
HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static(""))
|
||||
);
|
||||
|
||||
// Configurar Cache-Control
|
||||
// Configure Cache-Control
|
||||
let cache_control = format!("public, max-age={}", max_age);
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static(""))
|
||||
);
|
||||
|
||||
// Añadir cabecera Last-Modified
|
||||
// Add Last-Modified header
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
|
||||
response.headers_mut().insert(
|
||||
@@ -303,7 +303,7 @@ fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Layer para aplicar middleware de caché
|
||||
/// Layer for applying cache middleware
|
||||
#[derive(Clone)]
|
||||
pub struct HttpCacheLayer {
|
||||
cache: HttpCache,
|
||||
@@ -311,7 +311,7 @@ pub struct HttpCacheLayer {
|
||||
}
|
||||
|
||||
impl HttpCacheLayer {
|
||||
/// Crea una nueva capa de caché
|
||||
/// Creates a new cache layer
|
||||
pub fn new(cache: HttpCache) -> Self {
|
||||
Self {
|
||||
cache,
|
||||
@@ -319,7 +319,7 @@ impl HttpCacheLayer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Establece el tiempo de vida máximo
|
||||
/// Sets the maximum time-to-live
|
||||
pub fn with_max_age(mut self, max_age: u64) -> Self {
|
||||
self.max_age = Some(max_age);
|
||||
self
|
||||
@@ -338,7 +338,7 @@ impl<S> Layer<S> for HttpCacheLayer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Servicio que implementa la lógica de caché
|
||||
/// Service that implements cache logic
|
||||
#[derive(Clone)]
|
||||
pub struct HttpCacheService<S> {
|
||||
inner: S,
|
||||
@@ -365,10 +365,10 @@ where
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
// Generar clave de caché
|
||||
// Generate cache key
|
||||
let cache_key = req.uri().path().to_string();
|
||||
|
||||
// Solo aplicar caché para solicitudes GET
|
||||
// Only apply cache for GET requests
|
||||
if req.method() != Method::GET {
|
||||
let future = self.inner.call(req);
|
||||
return Box::pin(async move {
|
||||
@@ -377,42 +377,42 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener ETag del cliente
|
||||
// Get client ETag
|
||||
let if_none_match = req.headers()
|
||||
.get("if-none-match")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
// Verificar si hay una entrada en caché
|
||||
// Check if there is a cache entry
|
||||
let cache_clone = self.cache.clone();
|
||||
let max_age = self.max_age;
|
||||
let entry = cache_clone.get(&cache_key);
|
||||
|
||||
match entry {
|
||||
Some(cache_entry) if if_none_match == Some(&cache_entry.etag) => {
|
||||
// El cliente tiene la versión correcta, enviar 304
|
||||
// The client has the correct version, send 304
|
||||
debug!("Cache HIT (304): {}", cache_key);
|
||||
let response = create_not_modified_response(&cache_entry);
|
||||
return Box::pin(async move { Ok(response) });
|
||||
},
|
||||
Some(cache_entry) if cache_entry.data.is_some() => {
|
||||
// El cliente necesita la versión actualizada
|
||||
// The client needs the updated version
|
||||
debug!("Cache HIT (200): {}", cache_key);
|
||||
let mut response = Response::new(Body::from(cache_entry.data.clone().unwrap()));
|
||||
|
||||
// Copiar cabeceras originales
|
||||
// Copy original headers
|
||||
for (key, value) in &cache_entry.headers {
|
||||
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
|
||||
response.headers_mut().insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Añadir cabeceras de caché
|
||||
// Add cache headers
|
||||
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
|
||||
|
||||
return Box::pin(async move { Ok(response) });
|
||||
},
|
||||
_ => {
|
||||
// No está en caché o ha expirado
|
||||
// Not cached or expired
|
||||
debug!("Cache MISS: {}", cache_key);
|
||||
let future = self.inner.call(req);
|
||||
let cache_clone = self.cache.clone();
|
||||
@@ -423,19 +423,19 @@ where
|
||||
let response = future.await.map_err(|e| e.into())?;
|
||||
let response = response_map_body(response).await;
|
||||
|
||||
// No cachear errores
|
||||
// Don't cache errors
|
||||
if !response.status().is_success() {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
// Obtener el cuerpo y calcular ETag
|
||||
// Get the body and calculate ETag
|
||||
let (parts, body) = response.into_parts();
|
||||
let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?;
|
||||
|
||||
// Calcular ETag
|
||||
// Calculate ETag
|
||||
let etag = cache_clone.calculate_etag_for_bytes(&bytes);
|
||||
|
||||
// Guardar en caché
|
||||
// Save to cache
|
||||
cache_clone.set(
|
||||
&cache_key,
|
||||
etag.clone(),
|
||||
@@ -444,7 +444,7 @@ where
|
||||
max_age
|
||||
);
|
||||
|
||||
// Crear la respuesta con ETag
|
||||
// Create the response with ETag
|
||||
let mut response = Response::from_parts(parts, Body::from(bytes));
|
||||
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache_clone.default_max_age));
|
||||
|
||||
@@ -455,9 +455,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Función auxiliar para convertir cualquier cuerpo en Body preservando su contenido.
|
||||
// Anteriormente esta función descartaba el body con Body::empty(), causando
|
||||
// pérdida de datos en respuestas no cacheadas.
|
||||
// Helper function to convert any body into Body preserving its content.
|
||||
// Previously this function discarded the body with Body::empty(), causing
|
||||
// data loss in non-cached responses.
|
||||
async fn response_map_body<B>(response: Response<B>) -> Response<Body>
|
||||
where
|
||||
B: http_body::Body + Send + 'static,
|
||||
@@ -478,10 +478,10 @@ where
|
||||
Response::from_parts(parts, Body::from(collected))
|
||||
}
|
||||
|
||||
/// Inicia una tarea de limpieza periódica para el caché
|
||||
/// Starts a periodic cleanup task for the cache
|
||||
pub fn start_cache_cleanup_task(cache: HttpCache) {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Cada 5 minutos
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Every 5 minutes
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
@@ -516,10 +516,10 @@ mod tests {
|
||||
let etag2 = cache.calculate_etag_for_bytes(&data2);
|
||||
let etag3 = cache.calculate_etag_for_bytes(&data3);
|
||||
|
||||
// Mismos datos deben generar mismo ETag
|
||||
// Same data should generate the same ETag
|
||||
assert_eq!(etag1, etag2);
|
||||
|
||||
// Datos diferentes deben generar ETags diferentes
|
||||
// Different data should generate different ETags
|
||||
assert_ne!(etag1, etag3);
|
||||
}
|
||||
|
||||
@@ -527,19 +527,19 @@ mod tests {
|
||||
async fn test_cache_hit_miss() {
|
||||
let cache = HttpCache::new();
|
||||
|
||||
// Crear datos de prueba directamente como Bytes
|
||||
// Create test data directly as Bytes
|
||||
let bytes1 = Bytes::from(r#"{"id":1,"name":"Test"}"#);
|
||||
let headers1 = HeaderMap::new();
|
||||
|
||||
let etag1 = cache.calculate_etag_for_bytes(&bytes1);
|
||||
cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None);
|
||||
|
||||
// Verificar cache hit
|
||||
// Verify cache hit
|
||||
let entry = cache.get("test").unwrap();
|
||||
assert_eq!(entry.etag, etag1);
|
||||
assert_eq!(entry.data.unwrap(), bytes1);
|
||||
|
||||
// Verificar cache miss
|
||||
// Verify cache miss
|
||||
assert!(cache.get("nonexistent").is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user