Files
Oxicloud/src/interfaces/api/handlers/folder_handler.rs
T

406 lines
16 KiB
Rust
Raw Normal View History

use std::sync::Arc;
use std::collections::HashMap;
2025-03-17 21:28:08 +01:00
use axum::{
extract::{Path, State, Query},
http::{StatusCode, header, HeaderName, HeaderValue, Response},
2026-02-14 01:29:34 +01:00
response::IntoResponse,
Json,
2025-03-17 21:28:08 +01:00
};
use crate::application::services::folder_service::FolderService;
use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto};
2025-03-19 00:44:27 +01:00
use crate::application::dtos::pagination::PaginationRequestDto;
use crate::common::errors::ErrorKind;
2025-03-19 00:44:27 +01:00
use crate::application::ports::inbound::FolderUseCase;
2025-03-26 18:33:22 +01:00
use crate::common::di::AppState as GlobalAppState;
use crate::interfaces::middleware::auth::{OptionalAuthUser, AuthUser};
2025-03-17 21:28:08 +01:00
type AppState = Arc<FolderService>;
/// Handler for folder-related API endpoints
pub struct FolderHandler;
impl FolderHandler {
/// Creates a new folder
/// When parent_id is not provided, the folder is created inside the
/// authenticated user's home folder ("My Folder - {username}") rather
/// than at the storage root. This prevents user-created directories
/// from being placed flat in ./storage/.
2025-03-17 21:28:08 +01:00
pub async fn create_folder(
State(service): State<AppState>,
auth_user: AuthUser,
Json(mut dto): Json<CreateFolderDto>,
2025-03-17 21:28:08 +01:00
) -> impl IntoResponse {
// If no parent_id was supplied, resolve the user's home folder as
// the default parent so the new folder is nested correctly.
if dto.parent_id.is_none() {
let home_folder_name = format!("My Folder - {}", auth_user.username);
tracing::info!(
"create_folder: parent_id is None for user '{}', looking up home folder '{}'",
auth_user.username, home_folder_name
);
match service.list_folders(None).await {
Ok(folders) => {
if let Some(home) = folders.iter().find(|f| f.name == home_folder_name) {
tracing::info!(
"create_folder: resolved home folder ID '{}' for user '{}'",
home.id, auth_user.username
);
dto.parent_id = Some(home.id.clone());
} else {
tracing::warn!(
"create_folder: home folder '{}' not found, folder will be created at root",
home_folder_name
);
}
}
Err(e) => {
tracing::error!("create_folder: failed to list folders for home resolution: {}", e);
}
}
}
2025-03-17 21:28:08 +01:00
match service.create_folder(dto).await {
Ok(folder) => (StatusCode::CREATED, Json(folder)).into_response(),
Err(err) => {
2025-03-19 00:44:27 +01:00
let status = match err.kind {
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
ErrorKind::NotFound => StatusCode::NOT_FOUND,
2025-03-17 21:28:08 +01:00
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
2025-03-17 21:28:08 +01:00
(status, err.to_string()).into_response()
}
}
}
2025-03-17 21:28:08 +01:00
/// 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) => {
2025-03-19 00:44:27 +01:00
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
2025-03-17 21:28:08 +01:00
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
2025-03-17 21:28:08 +01:00
(status, err.to_string()).into_response()
}
}
}
/// Lists root folders (no parent ID)
/// Non-admin users only see their own home folder.
pub async fn list_root_folders(
State(service): State<AppState>,
auth_user: AuthUser,
2026-02-13 23:00:16 +01:00
) -> axum::response::Response {
Self::list_folders_for_user(service, None, &auth_user).await
}
/// Lists contents of a specific folder by its ID
pub async fn list_folder_contents(
State(service): State<AppState>,
Path(id): Path<String>,
2026-02-13 23:00:16 +01:00
) -> axum::response::Response {
Self::list_folders_inner(service, Some(&id)).await
}
/// Lists root folders with pagination support
pub async fn list_root_folders_paginated(
State(service): State<AppState>,
auth_user: AuthUser,
_pagination: Query<PaginationRequestDto>,
2026-02-13 23:00:16 +01:00
) -> axum::response::Response {
// For paginated root listing, filter by user as well
2026-02-13 23:00:16 +01:00
Self::list_folders_for_user(service, None, &auth_user).await
}
/// Lists contents of a specific folder with pagination
pub async fn list_folder_contents_paginated(
State(service): State<AppState>,
Path(id): Path<String>,
pagination: Query<PaginationRequestDto>,
2026-02-13 23:00:16 +01:00
) -> axum::response::Response {
Self::list_folders_paginated_inner(service, pagination, Some(&id)).await
}
/// Checks if a folder name matches the user home-folder convention.
fn is_user_home_folder(folder_name: &str) -> bool {
folder_name.starts_with("My Folder - ")
}
/// Checks if a folder belongs to the given user.
fn folder_belongs_to_user(folder_name: &str, username: &str) -> bool {
let expected = format!("My Folder - {}", username);
folder_name == expected
}
2026-02-13 23:00:16 +01:00
/// Lists folders, optionally filtered by parent ID (internal helper)
async fn list_folders_inner(
service: AppState,
2025-03-17 21:28:08 +01:00
parent_id: Option<&str>,
2026-02-13 23:00:16 +01:00
) -> axum::response::Response {
2025-03-17 21:28:08 +01:00
match service.list_folders(parent_id).await {
Ok(folders) => {
(StatusCode::OK, Json(folders)).into_response()
},
2025-03-17 21:28:08 +01:00
Err(err) => {
2025-03-19 00:44:27 +01:00
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
}
}
}
/// Lists folders with user-based filtering for root listings.
/// Non-admin users only see their own home folder at the root level.
2026-02-13 23:00:16 +01:00
async fn list_folders_for_user(
service: AppState,
parent_id: Option<&str>,
auth_user: &AuthUser,
2026-02-13 23:00:16 +01:00
) -> axum::response::Response {
match service.list_folders(parent_id).await {
Ok(folders) => {
// Only filter at root level (parent_id == None)
let filtered = if parent_id.is_none() {
folders.into_iter().filter(|f| {
// Skip hidden/system folders
if f.name.starts_with('.') {
return false;
}
// If it's a user home folder, only show if it belongs to this user
if Self::is_user_home_folder(&f.name) {
return Self::folder_belongs_to_user(&f.name, &auth_user.username);
}
// Non-home folders are visible to everyone
true
}).collect()
} else {
folders
};
(StatusCode::OK, Json(filtered)).into_response()
},
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
2025-03-19 00:44:27 +01:00
}
}
}
2026-02-13 23:00:16 +01:00
/// Lists folders with pagination support (internal helper)
async fn list_folders_paginated_inner(
service: AppState,
2025-03-19 00:44:27 +01:00
Query(pagination): Query<PaginationRequestDto>,
parent_id: Option<&str>,
2026-02-13 23:00:16 +01:00
) -> axum::response::Response {
2025-03-19 00:44:27 +01:00
match service.list_folders_paginated(parent_id, &pagination).await {
Ok(paginated_result) => {
(StatusCode::OK, Json(paginated_result)).into_response()
},
2025-03-19 00:44:27 +01:00
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
2025-03-17 21:28:08 +01:00
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
2025-03-17 21:28:08 +01:00
// Return a JSON error response
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
2025-03-17 21:28:08 +01:00
}
}
}
2025-03-17 21:28:08 +01:00
/// 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) => {
2025-03-19 00:44:27 +01:00
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
2025-03-17 21:28:08 +01:00
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
2025-03-17 21:28:08 +01:00
// Return a proper JSON error response
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
2025-03-17 21:28:08 +01:00
}
}
}
2025-03-17 21:28:08 +01:00
/// 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) => {
2025-03-19 00:44:27 +01:00
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
2025-03-17 21:28:08 +01:00
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
2025-03-17 21:28:08 +01:00
(status, err.to_string()).into_response()
}
}
}
2025-03-26 18:33:22 +01:00
/// Deletes a folder (with trash support)
2025-03-17 21:28:08 +01:00
pub async fn delete_folder(
State(service): State<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
2025-03-26 18:33:22 +01:00
// For folder deletion without trash functionality
2025-03-17 21:28:08 +01:00
match service.delete_folder(&id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => {
2025-03-19 00:44:27 +01:00
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
2025-03-17 21:28:08 +01:00
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
2025-03-17 21:28:08 +01:00
(status, err.to_string()).into_response()
}
}
}
2025-03-26 18:33:22 +01:00
/// Deletes a folder with trash functionality
pub async fn delete_folder_with_trash(
State(state): State<GlobalAppState>,
OptionalAuthUser(auth_user): OptionalAuthUser,
2025-03-26 18:33:22 +01:00
Path(id): Path<String>,
) -> impl IntoResponse {
let user_id = auth_user.as_ref().map(|u| u.id.as_str()).unwrap_or("anonymous");
2025-03-26 18:33:22 +01:00
// Check if trash service is available
if let Some(trash_service) = &state.trash_service {
tracing::info!("Moving folder to trash: {}", id);
2025-03-26 18:33:22 +01:00
// Try to move to trash first
match trash_service.move_to_trash(&id, "folder", user_id).await {
2025-03-26 18:33:22 +01:00
Ok(_) => {
tracing::info!("Folder successfully moved to trash: {}", id);
return StatusCode::NO_CONTENT.into_response();
},
2025-03-26 18:33:22 +01:00
Err(err) => {
tracing::warn!("Could not move folder to trash, falling back to permanent delete: {}", err);
2025-03-26 18:33:22 +01:00
// Fall through to regular delete if trash fails
}
}
}
2025-03-26 18:33:22 +01:00
// Fallback to permanent delete if trash is unavailable or failed
let folder_service = &state.applications.folder_service;
match folder_service.delete_folder(&id).await {
Ok(_) => {
tracing::info!("Folder permanently deleted: {}", id);
StatusCode::NO_CONTENT.into_response()
},
2025-03-26 18:33:22 +01:00
Err(err) => {
tracing::error!("Error deleting folder: {}", err);
2025-03-26 18:33:22 +01:00
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": format!("Error deleting folder: {}", err)
}))).into_response()
2025-03-26 18:33:22 +01:00
}
}
}
2025-04-02 01:22:05 +02:00
/// Downloads a folder as a ZIP file
pub async fn download_folder_zip(
State(state): State<GlobalAppState>,
Path(id): Path<String>,
Query(_params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
tracing::info!("Downloading folder as ZIP: {}", id);
2025-04-02 01:22:05 +02:00
// Get folder information first to check it exists and get name
let folder_service = &state.applications.folder_service;
2025-04-02 01:22:05 +02:00
match folder_service.get_folder(&id).await {
Ok(folder) => {
tracing::info!("Preparing ZIP for folder: {} ({})", folder.name, id);
// Use ZIP service from DI container
let zip_service = &state.core.zip_service;
2025-04-02 01:22:05 +02:00
// Create the ZIP file
match zip_service.create_folder_zip(&id, &folder.name).await {
Ok(zip_data) => {
tracing::info!("ZIP file created successfully, size: {} bytes", zip_data.len());
2025-04-02 01:22:05 +02:00
// Setup headers for download
let filename = format!("{}.zip", folder.name);
let content_disposition = format!("attachment; filename=\"{}\"", filename);
2025-04-02 01:22:05 +02:00
// Build response with the ZIP data
let mut headers = HashMap::new();
headers.insert(header::CONTENT_TYPE.to_string(), "application/zip".to_string());
headers.insert(header::CONTENT_DISPOSITION.to_string(), content_disposition);
headers.insert(header::CONTENT_LENGTH.to_string(), zip_data.len().to_string());
2025-04-02 01:22:05 +02:00
// Build the response
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(zip_data))
.unwrap();
2025-04-02 01:22:05 +02:00
// 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()
2025-04-02 01:22:05 +02:00
);
}
2025-04-02 01:22:05 +02:00
response
},
2025-04-02 01:22:05 +02:00
Err(err) => {
tracing::error!("Error creating ZIP file: {}", err);
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": format!("Error creating ZIP file: {}", err)
}))).into_response()
2025-04-02 01:22:05 +02:00
}
}
},
2025-04-02 01:22:05 +02:00
Err(err) => {
tracing::error!("Folder not found: {}", err);
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": format!("Error finding folder: {}", err)
}))).into_response()
2025-04-02 01:22:05 +02:00
}
}
}
}