Initial commit
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Path, State, Multipart},
|
||||
http::{StatusCode, header},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::application::services::file_service::FileService;
|
||||
use crate::domain::repositories::file_repository::FileRepositoryError;
|
||||
|
||||
type AppState = Arc<FileService>;
|
||||
|
||||
/// Handler for file-related API endpoints
|
||||
pub struct FileHandler;
|
||||
|
||||
impl FileHandler {
|
||||
/// Uploads a file
|
||||
pub async fn upload_file(
|
||||
State(service): State<AppState>,
|
||||
mut multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
// Extract file from multipart request
|
||||
let mut file_part = None;
|
||||
let mut folder_id = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
|
||||
if name == "file" {
|
||||
file_part = Some((
|
||||
field.file_name().unwrap_or("unnamed").to_string(),
|
||||
field.content_type().unwrap_or("application/octet-stream").to_string(),
|
||||
field.bytes().await.unwrap_or_default(),
|
||||
));
|
||||
} else if name == "folder_id" {
|
||||
let folder_id_value = field.text().await.unwrap_or_default();
|
||||
if !folder_id_value.is_empty() {
|
||||
folder_id = Some(folder_id_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if file was provided
|
||||
if let Some((filename, content_type, data)) = file_part {
|
||||
// Upload file from bytes
|
||||
match service.upload_file_from_bytes(filename, folder_id, content_type, data.to_vec()).await {
|
||||
Ok(file) => (StatusCode::CREATED, Json(file)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match &err {
|
||||
FileRepositoryError::AlreadyExists(_) => StatusCode::CONFLICT,
|
||||
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "No file provided"
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads a file
|
||||
pub async fn download_file(
|
||||
State(service): State<AppState>,
|
||||
Path(id): Path<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;
|
||||
|
||||
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)),
|
||||
];
|
||||
|
||||
(StatusCode::OK, headers, content).into_response()
|
||||
},
|
||||
(Err(err), _) | (_, Err(err)) => {
|
||||
let status = match &err {
|
||||
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists files, optionally filtered by folder ID
|
||||
pub async fn list_files(
|
||||
State(service): State<AppState>,
|
||||
folder_id: Option<&str>,
|
||||
) -> impl IntoResponse {
|
||||
match service.list_files(folder_id).await {
|
||||
Ok(files) => {
|
||||
// Always return an array even if empty
|
||||
(StatusCode::OK, Json(files)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
let status = match &err {
|
||||
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
// Return a JSON error response
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a file
|
||||
pub async fn delete_file(
|
||||
State(service): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match service.delete_file(&id).await {
|
||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => {
|
||||
let status = match &err {
|
||||
FileRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a file to a different folder
|
||||
pub async fn move_file(
|
||||
State(service): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<MoveFilePayload>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!("API request: Mover archivo con ID: {} a carpeta: {:?}", id, payload.folder_id);
|
||||
|
||||
// Primero verificar si el archivo existe
|
||||
match service.get_file(&id).await {
|
||||
Ok(file) => {
|
||||
tracing::info!("Archivo encontrado: {} (ID: {}), procediendo con la operación de mover", file.name, id);
|
||||
|
||||
// Para carpetas de destino, simplemente confiamos en que la
|
||||
// operación de mover verificará su existencia
|
||||
if let Some(folder_id) = &payload.folder_id {
|
||||
tracing::info!("Se intentará mover a carpeta: {}", folder_id);
|
||||
}
|
||||
|
||||
// Proceder con la operación de mover
|
||||
match service.move_file(&id, payload.folder_id).await {
|
||||
Ok(file) => {
|
||||
tracing::info!("Archivo movido exitosamente: {} (ID: {})", file.name, file.id);
|
||||
(StatusCode::OK, Json(file)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
let status = match &err {
|
||||
FileRepositoryError::NotFound(_) => {
|
||||
tracing::error!("Error al mover archivo - no encontrado: {}", err);
|
||||
StatusCode::NOT_FOUND
|
||||
},
|
||||
FileRepositoryError::AlreadyExists(_) => {
|
||||
tracing::error!("Error al mover archivo - ya existe: {}", err);
|
||||
StatusCode::CONFLICT
|
||||
},
|
||||
_ => {
|
||||
tracing::error!("Error al mover archivo: {}", err);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
};
|
||||
|
||||
(status, Json(serde_json::json!({
|
||||
"error": format!("Error al mover el archivo: {}", err.to_string()),
|
||||
"code": status.as_u16(),
|
||||
"details": format!("Error al mover archivo con ID: {} - {}", id, err)
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Error al encontrar archivo para mover - no existe: {} (ID: {})", err, id);
|
||||
(StatusCode::NOT_FOUND, Json(serde_json::json!({
|
||||
"error": format!("El archivo con ID: {} no existe", id),
|
||||
"code": StatusCode::NOT_FOUND.as_u16()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload for moving a file
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MoveFilePayload {
|
||||
/// Target folder ID (None means root)
|
||||
pub folder_id: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
|
||||
use crate::application::services::folder_service::FolderService;
|
||||
use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto};
|
||||
use crate::domain::repositories::folder_repository::FolderRepositoryError;
|
||||
|
||||
type AppState = Arc<FolderService>;
|
||||
|
||||
/// Handler for folder-related API endpoints
|
||||
pub struct FolderHandler;
|
||||
|
||||
impl FolderHandler {
|
||||
/// Creates a new folder
|
||||
pub async fn create_folder(
|
||||
State(service): State<AppState>,
|
||||
Json(dto): Json<CreateFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
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,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, err.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a folder by ID
|
||||
pub async fn get_folder(
|
||||
State(service): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
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,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, err.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists folders, optionally filtered by parent ID
|
||||
pub async fn list_folders(
|
||||
State(service): State<AppState>,
|
||||
parent_id: Option<&str>,
|
||||
) -> impl IntoResponse {
|
||||
// Parent ID is already a &str
|
||||
|
||||
match service.list_folders(parent_id).await {
|
||||
Ok(folders) => {
|
||||
// Always return an array even if empty
|
||||
(StatusCode::OK, Json(folders)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
let status = match &err {
|
||||
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
// Return a JSON error response
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renames a folder
|
||||
pub async fn rename_folder(
|
||||
State(service): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<RenameFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
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,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
// Return a proper JSON error response
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a folder to a new parent
|
||||
pub async fn move_folder(
|
||||
State(service): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<MoveFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
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,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, err.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a folder
|
||||
pub async fn delete_folder(
|
||||
State(service): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match service.delete_folder(&id).await {
|
||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => {
|
||||
let status = match &err {
|
||||
FolderRepositoryError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, err.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{State, Query},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
|
||||
use crate::application::services::i18n_application_service::I18nApplicationService;
|
||||
use crate::application::dtos::i18n_dto::{LocaleDto, TranslationRequestDto, TranslationResponseDto, TranslationErrorDto};
|
||||
use crate::domain::services::i18n_service::{Locale, I18nError};
|
||||
|
||||
type AppState = Arc<I18nApplicationService>;
|
||||
|
||||
/// Handler for i18n-related API endpoints
|
||||
pub struct I18nHandler;
|
||||
|
||||
impl I18nHandler {
|
||||
/// Gets a list of available locales
|
||||
pub async fn get_locales(
|
||||
State(service): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let locales = service.available_locales().await;
|
||||
let locale_dtos: Vec<LocaleDto> = locales.into_iter().map(LocaleDto::from).collect();
|
||||
|
||||
(StatusCode::OK, Json(locale_dtos)).into_response()
|
||||
}
|
||||
|
||||
/// Translates a key to the requested locale
|
||||
pub async fn translate(
|
||||
State(service): State<AppState>,
|
||||
Query(query): Query<TranslationRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
let locale = match &query.locale {
|
||||
Some(locale_str) => {
|
||||
match Locale::from_str(locale_str) {
|
||||
Some(locale) => Some(locale),
|
||||
None => {
|
||||
let error = TranslationErrorDto {
|
||||
key: query.key.clone(),
|
||||
locale: locale_str.clone(),
|
||||
error: format!("Unsupported locale: {}", locale_str),
|
||||
};
|
||||
return (StatusCode::BAD_REQUEST, Json(error)).into_response();
|
||||
}
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
match service.translate(&query.key, locale).await {
|
||||
Ok(text) => {
|
||||
let response = TranslationResponseDto {
|
||||
key: query.key,
|
||||
locale: locale.unwrap_or(Locale::default()).as_str().to_string(),
|
||||
text,
|
||||
};
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
let status = match &err {
|
||||
I18nError::KeyNotFound(_) => StatusCode::NOT_FOUND,
|
||||
I18nError::InvalidLocale(_) => StatusCode::BAD_REQUEST,
|
||||
I18nError::LoadError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
let error = TranslationErrorDto {
|
||||
key: query.key,
|
||||
locale: locale.unwrap_or(Locale::default()).as_str().to_string(),
|
||||
error: err.to_string(),
|
||||
};
|
||||
|
||||
(status, Json(error)).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets all translations for a locale
|
||||
pub async fn get_translations(
|
||||
State(_service): State<AppState>,
|
||||
locale_code: String,
|
||||
) -> impl IntoResponse {
|
||||
let locale = match Locale::from_str(&locale_code) {
|
||||
Some(locale) => locale,
|
||||
None => {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": format!("Unsupported locale: {}", locale_code)
|
||||
}))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// This implementation is a bit weird, as we don't have a way to get all translations
|
||||
// We should improve the I18nService to support this
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"locale": locale.as_str()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod file_handler;
|
||||
pub mod folder_handler;
|
||||
pub mod i18n_handler;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::create_api_routes;
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
routing::{get, post, put, delete},
|
||||
Router,
|
||||
extract::State,
|
||||
};
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||
|
||||
use crate::application::services::folder_service::FolderService;
|
||||
use crate::application::services::file_service::FileService;
|
||||
use crate::application::services::i18n_application_service::I18nApplicationService;
|
||||
use crate::interfaces::api::handlers::folder_handler::FolderHandler;
|
||||
use crate::interfaces::api::handlers::file_handler::FileHandler;
|
||||
use crate::interfaces::api::handlers::i18n_handler::I18nHandler;
|
||||
|
||||
/// Creates API routes for the application
|
||||
pub fn create_api_routes(
|
||||
folder_service: Arc<FolderService>,
|
||||
file_service: Arc<FileService>,
|
||||
i18n_service: Option<Arc<I18nApplicationService>>,
|
||||
) -> Router {
|
||||
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("/{id}", get(FolderHandler::get_folder))
|
||||
.route("/{id}/rename", put(FolderHandler::rename_folder))
|
||||
.route("/{id}/move", put(FolderHandler::move_folder))
|
||||
.route("/{id}", delete(FolderHandler::delete_folder))
|
||||
.with_state(folder_service);
|
||||
|
||||
let files_router = Router::new()
|
||||
.route("/", get(|
|
||||
State(service): State<Arc<FileService>>,
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
| async move {
|
||||
// Get folder_id from query parameter if present
|
||||
let folder_id = params.get("folder_id").map(|id| id.as_str());
|
||||
tracing::info!("API: Listando archivos con folder_id: {:?}", folder_id);
|
||||
FileHandler::list_files(State(service), folder_id).await
|
||||
}))
|
||||
.route("/upload", post(FileHandler::upload_file))
|
||||
.route("/{id}", get(FileHandler::download_file))
|
||||
.route("/{id}", delete(FileHandler::delete_file))
|
||||
.route("/{id}/move", put(FileHandler::move_file))
|
||||
.with_state(file_service);
|
||||
|
||||
// Create a router without the i18n routes
|
||||
let mut router = Router::new()
|
||||
.nest("/folders", folders_router)
|
||||
.nest("/files", files_router);
|
||||
|
||||
// Add i18n routes if the service is provided
|
||||
if let Some(i18n_service) = i18n_service {
|
||||
let i18n_router = Router::new()
|
||||
.route("/locales", get(I18nHandler::get_locales))
|
||||
.route("/translate", get(I18nHandler::translate))
|
||||
.route("/locales/{locale_code}", get(|
|
||||
State(service): State<Arc<I18nApplicationService>>,
|
||||
axum::extract::Path(locale_code): axum::extract::Path<String>,
|
||||
| async move {
|
||||
I18nHandler::get_translations(State(service), locale_code).await
|
||||
}))
|
||||
.with_state(i18n_service);
|
||||
|
||||
router = router.nest("/i18n", i18n_router);
|
||||
}
|
||||
|
||||
router
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod api;
|
||||
pub mod web;
|
||||
|
||||
pub use api::create_api_routes;
|
||||
@@ -0,0 +1,11 @@
|
||||
use axum::Router;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Creates web routes for serving static files
|
||||
pub fn create_web_routes() -> Router {
|
||||
Router::new()
|
||||
.fallback_service(
|
||||
ServeDir::new(PathBuf::from("static"))
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user