adding several features

This commit is contained in:
DioCrafts
2025-03-19 00:44:27 +01:00
parent 7157762540
commit d9bbd575d2
66 changed files with 12137 additions and 1055 deletions
@@ -0,0 +1,410 @@
use std::sync::Arc;
use axum::{
extract::{State, Json},
response::IntoResponse,
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use crate::application::services::batch_operations::{
BatchOperationService, BatchResult, BatchStats
};
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
#[derive(Clone)]
pub struct BatchHandlerState {
pub batch_service: Arc<BatchOperationService>,
}
/// DTO para las solicitudes de operaciones en lote de archivos
#[derive(Debug, Deserialize)]
pub struct BatchFileOperationRequest {
/// IDs de los archivos a procesar
pub file_ids: Vec<String>,
/// ID de la carpeta destino (opcional)
#[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>,
}
/// DTO para las solicitudes de operaciones en lote de carpetas
#[derive(Debug, Deserialize)]
pub struct BatchFolderOperationRequest {
/// IDs de las carpetas a procesar
pub folder_ids: Vec<String>,
/// Si la operación debe ser recursiva
#[serde(default)]
pub recursive: bool,
/// ID de la carpeta destino (opcional)
#[serde(skip_serializing_if = "Option::is_none")]
#[allow(dead_code)]
pub target_folder_id: Option<String>,
}
/// DTO para las solicitudes de creación en lote de carpetas
#[derive(Debug, Deserialize)]
pub struct BatchCreateFoldersRequest {
/// Detalles de las carpetas a crear
pub folders: Vec<CreateFolderDetail>,
}
/// Detalle para creación de una carpeta
#[derive(Debug, Deserialize)]
pub struct CreateFolderDetail {
/// Nombre de la carpeta
pub name: String,
/// ID de la carpeta padre (opcional)
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}
/// DTO para los resultados de operaciones en lote
#[derive(Debug, Serialize)]
pub struct BatchOperationResponse<T> {
/// Entidades procesadas exitosamente
pub successful: Vec<T>,
/// Operaciones fallidas con sus mensajes de error
pub failed: Vec<FailedOperation>,
/// Estadísticas de la operación
pub stats: BatchOperationStats,
}
/// Operación fallida en un lote
#[derive(Debug, Serialize)]
pub struct FailedOperation {
/// Identificador de la entidad que falló
pub id: String,
/// Mensaje de error
pub error: String,
}
/// Estadísticas de una operación por lotes
#[derive(Debug, Serialize)]
pub struct BatchOperationStats {
/// Número total de operaciones
pub total: usize,
/// Número de operaciones exitosas
pub successful: usize,
/// Número de operaciones fallidas
pub failed: usize,
/// Tiempo total de ejecución en milisegundos
pub execution_time_ms: u128,
}
/// Convierte BatchStats del dominio a DTO
impl From<BatchStats> for BatchOperationStats {
fn from(stats: BatchStats) -> Self {
Self {
total: stats.total,
successful: stats.successful,
failed: stats.failed,
execution_time_ms: stats.execution_time_ms,
}
}
}
/// Convierte BatchResult<T> del dominio a DTO
impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U>
where
U: From<T>,
{
fn from(result: BatchResult<T>) -> Self {
let successful = result.successful.into_iter().map(U::from).collect();
let failed = result.failed.into_iter()
.map(|(id, error)| FailedOperation { id, error })
.collect();
Self {
successful,
failed,
stats: result.stats.into(),
}
}
}
/// Handler para mover múltiples archivos en lote
pub async fn move_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
}))
).into_response());
}
// Ejecutar operación de lote
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
let response: BatchOperationResponse<FileDto> = result.into();
// Determinar código de estado basado en los resultados
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
} else {
StatusCode::BAD_REQUEST // Todas fallaron
}
} else {
StatusCode::OK // Todas exitosas
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para copiar múltiples archivos en lote
pub async fn copy_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
}))
).into_response());
}
// Ejecutar operación de lote
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
let response: BatchOperationResponse<FileDto> = result.into();
// Determinar código de estado basado en los resultados
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
} else {
StatusCode::BAD_REQUEST // Todas fallaron
}
} else {
StatusCode::OK // Todas exitosas
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para eliminar múltiples archivos en lote
pub async fn delete_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
}))
).into_response());
}
// Ejecutar operación de lote
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
let response = BatchOperationResponse {
successful: result.successful,
failed: result.failed.into_iter()
.map(|(id, error)| FailedOperation { id, error })
.collect(),
stats: result.stats.into(),
};
// Determinar código de estado basado en los resultados
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
} else {
StatusCode::BAD_REQUEST // Todas fallaron
}
} else {
StatusCode::OK // Todas exitosas
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para eliminar múltiples carpetas en lote
pub async fn delete_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No folder IDs provided"
}))
).into_response());
}
// Ejecutar operación de lote
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
let response = BatchOperationResponse {
successful: result.successful,
failed: result.failed.into_iter()
.map(|(id, error)| FailedOperation { id, error })
.collect(),
stats: result.stats.into(),
};
// Determinar código de estado basado en los resultados
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
} else {
StatusCode::BAD_REQUEST // Todas fallaron
}
} else {
StatusCode::OK // Todas exitosas
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para crear múltiples carpetas en lote
pub async fn create_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchCreateFoldersRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
if request.folders.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No folders provided"
}))
).into_response());
}
// Transformar el formato para el servicio
let folders = request.folders
.into_iter()
.map(|detail| (detail.name, detail.parent_id))
.collect();
// Ejecutar operación de lote
let result = state.batch_service
.create_folders(folders)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
let response: BatchOperationResponse<FolderDto> = result.into();
// Determinar código de estado basado en los resultados
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
} else {
StatusCode::BAD_REQUEST // Todas fallaron
}
} else {
StatusCode::CREATED // Todas exitosas
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para obtener múltiples archivos en lote
pub async fn get_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
}))
).into_response());
}
// Ejecutar operación de lote
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
let response: BatchOperationResponse<FileDto> = result.into();
// Determinar código de estado basado en los resultados
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
} else {
StatusCode::BAD_REQUEST // Todas fallaron
}
} else {
StatusCode::OK // Todas exitosas
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para obtener múltiples carpetas en lote
pub async fn get_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No folder IDs provided"
}))
).into_response());
}
// Ejecutar operación de lote
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
let response: BatchOperationResponse<FolderDto> = result.into();
// Determinar código de estado basado en los resultados
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
} else {
StatusCode::BAD_REQUEST // Todas fallaron
}
} else {
StatusCode::OK // Todas exitosas
};
Ok((status_code, Json(response)).into_response())
}
+271 -24
View File
@@ -1,20 +1,55 @@
use std::sync::Arc;
use axum::{
extract::{Path, State, Multipart},
http::{StatusCode, header},
extract::{Path, State, Multipart, Query},
http::{StatusCode, header, HeaderName, HeaderValue, Response},
response::IntoResponse,
Json,
};
use serde::Deserialize;
use std::collections::HashMap;
use futures::Stream;
use std::task::{Context, Poll};
use std::pin::Pin;
use crate::application::services::file_service::FileService;
use crate::domain::repositories::file_repository::FileRepositoryError;
use crate::application::services::file_service::{FileService, FileServiceError};
use crate::infrastructure::services::compression_service::{
CompressionService, GzipCompressionService, CompressionLevel
};
type AppState = Arc<FileService>;
/// Handler for file-related API endpoints
pub struct FileHandler;
// Simpler approach to make streams Unpin - use Pin<Box<dyn Stream>> directly
struct BoxedStream<T> {
inner: Pin<Box<dyn Stream<Item = T> + Send + 'static>>,
}
impl<T> Stream for BoxedStream<T> {
type Item = T;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// Accessing the field directly is safe because BoxedStream is not a structural pinning type
unsafe { self.get_unchecked_mut().inner.as_mut().poll_next(cx) }
}
}
// This is safe because BoxedStream's inner field is already Pin<Box<dyn Stream>>
impl<T> Unpin for BoxedStream<T> {}
impl<T> BoxedStream<T> {
#[allow(dead_code)]
fn new<S>(stream: S) -> Self
where
S: Stream<Item = T> + Send + 'static,
{
BoxedStream {
inner: Box::pin(stream),
}
}
}
impl FileHandler {
/// Uploads a file
pub async fn upload_file(
@@ -49,8 +84,8 @@ impl FileHandler {
Ok(file) => (StatusCode::CREATED, Json(file)).into_response(),
Err(err) => {
let status = match &err {
FileRepositoryError::AlreadyExists(_) => StatusCode::CONFLICT,
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
FileServiceError::Conflict(_) => StatusCode::CONFLICT,
FileServiceError::NotFound(_) => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -66,28 +101,240 @@ impl FileHandler {
}
}
/// Downloads a file
/// Downloads a file with optional compression
pub async fn download_file(
State(service): State<AppState>,
Path(id): Path<String>,
Query(params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
// Get file info and content
let file_result = service.get_file(&id).await;
let content_result = service.get_file_content(&id).await;
// Initialize compression service
let compression_service = GzipCompressionService::new();
match (file_result, content_result) {
(Ok(file), Ok(content)) => {
// Create response with proper headers
let headers = [
(header::CONTENT_TYPE, file.mime_type),
(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", file.name)),
];
// Check if compression is explicitly requested or rejected
let compression_param = params.get("compress").map(|v| v.as_str());
let force_compress = compression_param == Some("true") || compression_param == Some("1");
let force_no_compress = compression_param == Some("false") || compression_param == Some("0");
// Determine compression level from query params
let compression_level = match params.get("compression_level").map(|v| v.as_str()) {
Some("none") => CompressionLevel::None,
Some("fast") => CompressionLevel::Fast,
Some("best") => CompressionLevel::Best,
_ => CompressionLevel::Default, // Default or unrecognized
};
// Get file info first to check it exists and get metadata
match service.get_file(&id).await {
Ok(file) => {
// Determine if we should compress based on file type and size
let should_compress = if force_no_compress {
false
} else if force_compress {
true
} else {
compression_service.should_compress(&file.mime_type, file.size)
};
(StatusCode::OK, headers, content).into_response()
// Log compression decision for debugging
tracing::debug!(
"Download file: name={}, size={}KB, mime={}, compress={}",
file.name, file.size / 1024, file.mime_type, should_compress
);
// For large files, use streaming response with potential compression
if file.size > 10 * 1024 * 1024 { // 10MB threshold for streaming
match service.get_file_content(&id).await {
Ok(content) => {
// Create base headers
let mut headers = HashMap::new();
headers.insert(
header::CONTENT_DISPOSITION.to_string(),
format!("attachment; filename=\"{}\"", file.name)
);
if should_compress {
// Add content-encoding header for compressed response
headers.insert(header::CONTENT_ENCODING.to_string(), "gzip".to_string());
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
headers.insert(header::VARY.to_string(), "Accept-Encoding".to_string());
// Compress the content
match compression_service.compress_data(&content, compression_level).await {
Ok(compressed_content) => {
tracing::debug!(
"Compressed file: {} from {}KB to {}KB (ratio: {:.2})",
file.name,
content.len() / 1024,
compressed_content.len() / 1024,
content.len() as f64 / compressed_content.len() as f64
);
// Build a custom response with headers and body
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(compressed_content))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
);
}
response
},
Err(e) => {
tracing::warn!("Compression failed, sending uncompressed: {}", e);
// Fall back to uncompressed
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
// Build a custom response with headers and body
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(content))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
);
}
response
}
}
} else {
// No compression, return as-is
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
// Build a custom response with headers and body
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(content))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
);
}
response
}
},
Err(err) => {
tracing::error!("Error getting file content: {}", err);
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": format!("Error reading file: {}", err)
}))).into_response()
}
}
} else {
// For smaller files, load entirely but still potentially compress
match service.get_file_content(&id).await {
Ok(content) => {
// Create base headers
let mut headers = HashMap::new();
headers.insert(
header::CONTENT_DISPOSITION.to_string(),
format!("attachment; filename=\"{}\"", file.name)
);
if should_compress {
// Add content-encoding header for compressed response
headers.insert(header::CONTENT_ENCODING.to_string(), "gzip".to_string());
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
headers.insert(header::VARY.to_string(), "Accept-Encoding".to_string());
// Compress the content
match compression_service.compress_data(&content, compression_level).await {
Ok(compressed_content) => {
tracing::debug!(
"Compressed file: {} from {}KB to {}KB (ratio: {:.2})",
file.name,
content.len() / 1024,
compressed_content.len() / 1024,
content.len() as f64 / compressed_content.len() as f64
);
// Build a custom response with headers and body
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(compressed_content))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
);
}
response
},
Err(e) => {
tracing::warn!("Compression failed, sending uncompressed: {}", e);
// Fall back to uncompressed
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
// Build a custom response with headers and body
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(content))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
);
}
response
}
}
} else {
// No compression, return as-is
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
// Build a custom response with headers and body
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(content))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
);
}
response
}
},
Err(err) => {
tracing::error!("Error getting file content: {}", err);
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": format!("Error reading file: {}", err)
}))).into_response()
}
}
}
},
(Err(err), _) | (_, Err(err)) => {
Err(err) => {
let status = match &err {
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
FileServiceError::NotFound(_) => StatusCode::NOT_FOUND,
FileServiceError::AccessError(_) => StatusCode::SERVICE_UNAVAILABLE,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -110,7 +357,7 @@ impl FileHandler {
},
Err(err) => {
let status = match &err {
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
FileServiceError::NotFound(_) => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -131,7 +378,7 @@ impl FileHandler {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => {
let status = match &err {
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
FileServiceError::NotFound(_) => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -169,11 +416,11 @@ impl FileHandler {
},
Err(err) => {
let status = match &err {
FileRepositoryError::NotFound(_) => {
FileServiceError::NotFound(_) => {
tracing::error!("Error al mover archivo - no encontrado: {}", err);
StatusCode::NOT_FOUND
},
FileRepositoryError::AlreadyExists(_) => {
FileServiceError::Conflict(_) => {
tracing::error!("Error al mover archivo - ya existe: {}", err);
StatusCode::CONFLICT
},
+43 -17
View File
@@ -1,6 +1,6 @@
use std::sync::Arc;
use axum::{
extract::{Path, State},
extract::{Path, State, Query},
http::StatusCode,
response::IntoResponse,
Json,
@@ -8,7 +8,9 @@ use axum::{
use crate::application::services::folder_service::FolderService;
use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto};
use crate::domain::repositories::folder_repository::FolderRepositoryError;
use crate::application::dtos::pagination::PaginationRequestDto;
use crate::common::errors::ErrorKind;
use crate::application::ports::inbound::FolderUseCase;
type AppState = Arc<FolderService>;
@@ -24,9 +26,9 @@ impl FolderHandler {
match service.create_folder(dto).await {
Ok(folder) => (StatusCode::CREATED, Json(folder)).into_response(),
Err(err) => {
let status = match &err {
FolderRepositoryError::AlreadyExists(_) => StatusCode::CONFLICT,
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
let status = match err.kind {
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -43,8 +45,8 @@ impl FolderHandler {
match service.get_folder(&id).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => {
let status = match &err {
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -66,8 +68,32 @@ impl FolderHandler {
(StatusCode::OK, Json(folders)).into_response()
},
Err(err) => {
let status = match &err {
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
// Return a JSON error response
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
}
}
}
/// Lists folders with pagination support
pub async fn list_folders_paginated(
State(service): State<AppState>,
Query(pagination): Query<PaginationRequestDto>,
parent_id: Option<&str>,
) -> impl IntoResponse {
match service.list_folders_paginated(parent_id, &pagination).await {
Ok(paginated_result) => {
(StatusCode::OK, Json(paginated_result)).into_response()
},
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -88,9 +114,9 @@ impl FolderHandler {
match service.rename_folder(&id, dto).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => {
let status = match &err {
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
FolderRepositoryError::AlreadyExists(_) => StatusCode::CONFLICT,
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -111,9 +137,9 @@ impl FolderHandler {
match service.move_folder(&id, dto).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => {
let status = match &err {
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
FolderRepositoryError::AlreadyExists(_) => StatusCode::CONFLICT,
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -130,8 +156,8 @@ impl FolderHandler {
match service.delete_folder(&id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => {
let status = match &err {
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
+4
View File
@@ -1,4 +1,8 @@
pub mod file_handler;
pub mod folder_handler;
pub mod i18n_handler;
pub mod batch_handler;
/// Tipo de resultado para controladores de API
pub type ApiResult<T> = Result<T, (axum::http::StatusCode, String)>;
+75 -3
View File
@@ -2,16 +2,27 @@ use std::sync::Arc;
use axum::{
routing::{get, post, put, delete},
Router,
extract::State,
extract::{State, Query, Path},
};
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tower_http::{
compression::CompressionLayer,
trace::TraceLayer,
};
use crate::interfaces::middleware::cache::{HttpCache, start_cache_cleanup_task};
use crate::application::services::folder_service::FolderService;
use crate::application::services::file_service::FileService;
use crate::application::services::i18n_application_service::I18nApplicationService;
use crate::application::services::batch_operations::BatchOperationService;
use crate::interfaces::api::handlers::folder_handler::FolderHandler;
use crate::interfaces::api::handlers::file_handler::FileHandler;
use crate::interfaces::api::handlers::i18n_handler::I18nHandler;
use crate::interfaces::api::handlers::batch_handler::{
self, BatchHandlerState
};
use crate::application::dtos::pagination::PaginationRequestDto;
/// Creates API routes for the application
pub fn create_api_routes(
@@ -19,13 +30,57 @@ pub fn create_api_routes(
file_service: Arc<FileService>,
i18n_service: Option<Arc<I18nApplicationService>>,
) -> Router {
// Inicializar el servicio de operaciones por lotes
let batch_service = Arc::new(BatchOperationService::default(
file_service.clone(),
folder_service.clone()
));
// Crear estado para el manejador de operaciones por lotes
let batch_handler_state = BatchHandlerState {
batch_service: batch_service.clone(),
};
// Implement HTTP Cache
let http_cache = HttpCache::new();
// Define TTL values for different resource types (in seconds)
let _folders_ttl = 300; // 5 minutes
let _files_list_ttl = 300; // 5 minutes
let _i18n_ttl = 3600; // 1 hour
// Start the cleanup task for HTTP cache
start_cache_cleanup_task(http_cache.clone());
let folders_router = Router::new()
.route("/", post(FolderHandler::create_folder))
.route("/", get(|State(service): State<Arc<FolderService>>| async move {
// No parent ID means list root folders
FolderHandler::list_folders(State(service), None).await
}))
.route("/paginated", get(|
State(service): State<Arc<FolderService>>,
pagination: Query<PaginationRequestDto>
| async move {
// Paginación para carpetas raíz (sin parent)
FolderHandler::list_folders_paginated(State(service), pagination, None).await
}))
.route("/{id}", get(FolderHandler::get_folder))
.route("/{id}/contents", get(|
State(service): State<Arc<FolderService>>,
Path(id): Path<String>
| async move {
// Listar contenido de una carpeta por su ID
FolderHandler::list_folders(State(service), Some(&id)).await
}))
.route("/{id}/contents/paginated", get(|
State(service): State<Arc<FolderService>>,
Path(id): Path<String>,
pagination: Query<PaginationRequestDto>
| async move {
// Listar contenido paginado de una carpeta por su ID
FolderHandler::list_folders_paginated(State(service), pagination, Some(&id)).await
}))
.route("/{id}/rename", put(FolderHandler::rename_folder))
.route("/{id}/move", put(FolderHandler::move_folder))
.route("/{id}", delete(FolderHandler::delete_folder))
@@ -47,10 +102,24 @@ pub fn create_api_routes(
.route("/{id}/move", put(FileHandler::move_file))
.with_state(file_service);
// Crear rutas para operaciones por lotes
let batch_router = Router::new()
// Operaciones de archivos
.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
.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))
.with_state(batch_handler_state);
// Create a router without the i18n routes
let mut router = Router::new()
.nest("/folders", folders_router)
.nest("/files", files_router);
.nest("/files", files_router)
.nest("/batch", batch_router);
// Add i18n routes if the service is provided
if let Some(i18n_service) = i18n_service {
@@ -68,7 +137,10 @@ pub fn create_api_routes(
router = router.nest("/i18n", i18n_router);
}
// Apply compression and tracing layers
router
.layer(CompressionLayer::new())
.layer(TraceLayer::new_for_http())
// HTTP caching is disabled temporarily due to compatibility issues
// .layer(HttpCacheLayer::new(http_cache.clone()).with_max_age(folders_ttl))
}