Merge pull request #331 from EdouardVanbelle/feat/full-openapi-coverage
This commit is contained in:
@@ -100,7 +100,17 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, Str
|
||||
}
|
||||
|
||||
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
||||
async fn get_oidc_settings(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/settings/oidc",
|
||||
responses(
|
||||
(status = 200, description = "OIDC settings"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn get_oidc_settings(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -120,7 +130,17 @@ async fn get_oidc_settings(
|
||||
}
|
||||
|
||||
/// PUT /api/admin/settings/oidc — save OIDC settings + hot-reload
|
||||
async fn save_oidc_settings(
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/admin/settings/oidc",
|
||||
responses(
|
||||
(status = 200, description = "OIDC settings saved"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn save_oidc_settings(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<SaveOidcSettingsDto>,
|
||||
@@ -170,7 +190,17 @@ async fn test_oidc_connection(
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// GET /api/admin/settings/storage — get storage backend settings
|
||||
async fn get_storage_settings(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/settings/storage",
|
||||
responses(
|
||||
(status = 200, description = "Storage settings"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn get_storage_settings(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -190,7 +220,17 @@ async fn get_storage_settings(
|
||||
}
|
||||
|
||||
/// PUT /api/admin/settings/storage — save storage backend settings
|
||||
async fn save_storage_settings(
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/admin/settings/storage",
|
||||
responses(
|
||||
(status = 200, description = "Storage settings saved"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn save_storage_settings(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<SaveStorageSettingsDto>,
|
||||
@@ -240,7 +280,17 @@ async fn test_storage_connection(
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// GET /api/admin/storage/migration — current migration progress
|
||||
async fn get_migration_status(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/storage/migration",
|
||||
responses(
|
||||
(status = 200, description = "Current migration status"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn get_migration_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -250,7 +300,18 @@ async fn get_migration_status(
|
||||
}
|
||||
|
||||
/// POST /api/admin/storage/migration/start — begin background migration
|
||||
async fn start_migration(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/admin/storage/migration/start",
|
||||
responses(
|
||||
(status = 200, description = "Migration started"),
|
||||
(status = 400, description = "Migration already running"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn start_migration(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<StartMigrationDto>,
|
||||
@@ -316,7 +377,18 @@ async fn start_migration(
|
||||
}
|
||||
|
||||
/// POST /api/admin/storage/migration/pause — pause running migration
|
||||
async fn pause_migration(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/admin/storage/migration/pause",
|
||||
responses(
|
||||
(status = 200, description = "Migration paused"),
|
||||
(status = 400, description = "No running migration"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn pause_migration(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -335,7 +407,18 @@ async fn pause_migration(
|
||||
}
|
||||
|
||||
/// POST /api/admin/storage/migration/resume — resume paused migration
|
||||
async fn resume_migration(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/admin/storage/migration/resume",
|
||||
responses(
|
||||
(status = 200, description = "Migration resumed"),
|
||||
(status = 400, description = "No paused migration"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn resume_migration(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -355,7 +438,18 @@ async fn resume_migration(
|
||||
}
|
||||
|
||||
/// POST /api/admin/storage/migration/complete — finalize migration
|
||||
async fn complete_migration(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/admin/storage/migration/complete",
|
||||
responses(
|
||||
(status = 200, description = "Migration finalized"),
|
||||
(status = 400, description = "Migration not completed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn complete_migration(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -383,7 +477,18 @@ async fn complete_migration(
|
||||
}
|
||||
|
||||
/// POST /api/admin/storage/migration/verify — run integrity check
|
||||
async fn verify_migration(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/admin/storage/migration/verify",
|
||||
responses(
|
||||
(status = 200, description = "Verification result"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required"),
|
||||
(status = 500, description = "Verification failed")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn verify_migration(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<VerifyMigrationDto>,
|
||||
@@ -450,7 +555,17 @@ fn migration_state_to_dto(
|
||||
}
|
||||
|
||||
/// POST /api/admin/settings/storage/generate-key — generate a random AES-256 key.
|
||||
async fn generate_encryption_key(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/admin/settings/storage/generate-key",
|
||||
responses(
|
||||
(status = 200, description = "Generated AES-256 key"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn generate_encryption_key(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -496,7 +611,17 @@ fn build_backend_from_config(
|
||||
}
|
||||
|
||||
/// GET /api/admin/settings/general — system overview (backward compat)
|
||||
async fn get_general_settings(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/settings/general",
|
||||
responses(
|
||||
(status = 200, description = "General system settings"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn get_general_settings(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -527,7 +652,17 @@ async fn get_general_settings(
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/admin/dashboard — full dashboard statistics
|
||||
async fn get_dashboard_stats(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/dashboard",
|
||||
responses(
|
||||
(status = 200, description = "Dashboard statistics"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn get_dashboard_stats(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -603,7 +738,21 @@ async fn get_dashboard_stats(
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/admin/users?limit=50&offset=0 — list all users
|
||||
async fn list_users(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/users",
|
||||
params(
|
||||
("limit" = Option<i64>, Query, description = "Max users to return (default 100, max 500)"),
|
||||
("offset" = Option<i64>, Query, description = "Pagination offset")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of users"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn list_users(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<ListUsersQueryDto>,
|
||||
@@ -639,7 +788,19 @@ async fn list_users(
|
||||
}
|
||||
|
||||
/// GET /api/admin/users/:id — get single user
|
||||
async fn get_user(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/users/{id}",
|
||||
params(("id" = String, Path, description = "User UUID")),
|
||||
responses(
|
||||
(status = 200, description = "User details"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required"),
|
||||
(status = 404, description = "User not found")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn get_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
@@ -663,7 +824,19 @@ async fn get_user(
|
||||
}
|
||||
|
||||
/// DELETE /api/admin/users/:id — delete a user
|
||||
async fn delete_user(
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/admin/users/{id}",
|
||||
params(("id" = String, Path, description = "User UUID")),
|
||||
responses(
|
||||
(status = 200, description = "User deleted"),
|
||||
(status = 400, description = "Cannot delete own account"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn delete_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
@@ -700,7 +873,19 @@ async fn delete_user(
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/role — change user role
|
||||
async fn update_user_role(
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/admin/users/{id}/role",
|
||||
params(("id" = String, Path, description = "User UUID")),
|
||||
responses(
|
||||
(status = 200, description = "Role updated"),
|
||||
(status = 400, description = "Cannot change own role"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn update_user_role(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
@@ -738,7 +923,19 @@ async fn update_user_role(
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/active — activate/deactivate user
|
||||
async fn update_user_active(
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/admin/users/{id}/active",
|
||||
params(("id" = String, Path, description = "User UUID")),
|
||||
responses(
|
||||
(status = 200, description = "User active status updated"),
|
||||
(status = 400, description = "Cannot deactivate own account"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn update_user_active(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
@@ -781,7 +978,18 @@ async fn update_user_active(
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/quota — update user storage quota
|
||||
async fn update_user_quota(
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/admin/users/{id}/quota",
|
||||
params(("id" = String, Path, description = "User UUID")),
|
||||
responses(
|
||||
(status = 200, description = "Quota updated"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn update_user_quota(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
@@ -815,7 +1023,18 @@ async fn update_user_quota(
|
||||
// ============================================================================
|
||||
|
||||
/// POST /api/admin/users — create a new user (admin only)
|
||||
async fn create_user(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/admin/users",
|
||||
responses(
|
||||
(status = 201, description = "User created"),
|
||||
(status = 400, description = "Invalid user data"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn create_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<AdminCreateUserDto>,
|
||||
@@ -843,7 +1062,19 @@ async fn create_user(
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/password — reset a user's password (admin only)
|
||||
async fn reset_user_password(
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/admin/users/{id}/password",
|
||||
params(("id" = String, Path, description = "User UUID")),
|
||||
responses(
|
||||
(status = 200, description = "Password reset"),
|
||||
(status = 400, description = "Invalid password"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn reset_user_password(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
@@ -882,7 +1113,17 @@ async fn reset_user_password(
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/admin/settings/registration — check if public registration is enabled
|
||||
async fn get_registration_setting(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/settings/registration",
|
||||
responses(
|
||||
(status = 200, description = "Registration setting"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn get_registration_setting(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
@@ -901,7 +1142,18 @@ async fn get_registration_setting(
|
||||
}
|
||||
|
||||
/// PUT /api/admin/settings/registration — enable/disable public registration
|
||||
async fn set_registration_setting(
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/admin/settings/registration",
|
||||
responses(
|
||||
(status = 200, description = "Registration setting updated"),
|
||||
(status = 400, description = "Missing field"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Admin required")
|
||||
),
|
||||
tag = "admin"
|
||||
)]
|
||||
pub async fn set_registration_setting(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
|
||||
@@ -5,6 +5,7 @@ use axum::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::FolderDto;
|
||||
@@ -25,7 +26,7 @@ pub struct BatchHandlerState {
|
||||
}
|
||||
|
||||
/// DTO for batch file operation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct BatchFileOperationRequest {
|
||||
/// IDs of the files to process
|
||||
pub file_ids: Vec<String>,
|
||||
@@ -35,7 +36,7 @@ pub struct BatchFileOperationRequest {
|
||||
}
|
||||
|
||||
/// DTO for batch folder operation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct BatchFolderOperationRequest {
|
||||
/// IDs of the folders to process
|
||||
pub folder_ids: Vec<String>,
|
||||
@@ -48,14 +49,14 @@ pub struct BatchFolderOperationRequest {
|
||||
}
|
||||
|
||||
/// DTO for batch folder creation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct BatchCreateFoldersRequest {
|
||||
/// Details of the folders to create
|
||||
pub folders: Vec<CreateFolderDetail>,
|
||||
}
|
||||
|
||||
/// Detail for folder creation
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateFolderDetail {
|
||||
/// Folder name
|
||||
pub name: String,
|
||||
@@ -132,6 +133,17 @@ where
|
||||
}
|
||||
|
||||
/// Handler for moving multiple files in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/files/move",
|
||||
responses(
|
||||
(status = 200, description = "All files moved"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn move_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -188,6 +200,17 @@ pub async fn move_files_batch(
|
||||
}
|
||||
|
||||
/// Handler for copying multiple files in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/files/copy",
|
||||
responses(
|
||||
(status = 200, description = "All files copied"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn copy_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -244,6 +267,17 @@ pub async fn copy_files_batch(
|
||||
}
|
||||
|
||||
/// Handler for deleting multiple files in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/files/delete",
|
||||
responses(
|
||||
(status = 200, description = "All files deleted"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn delete_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -308,6 +342,17 @@ pub async fn delete_files_batch(
|
||||
}
|
||||
|
||||
/// Handler for deleting multiple folders in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/folders/delete",
|
||||
responses(
|
||||
(status = 200, description = "All folders deleted"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn delete_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -372,6 +417,17 @@ pub async fn delete_folders_batch(
|
||||
}
|
||||
|
||||
/// Handler for creating multiple folders in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/folders/create",
|
||||
responses(
|
||||
(status = 201, description = "All folders created"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn create_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -435,6 +491,17 @@ pub async fn create_folders_batch(
|
||||
}
|
||||
|
||||
/// Handler for getting multiple files in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/files/get",
|
||||
responses(
|
||||
(status = 200, description = "Batch file details"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn get_files_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -491,6 +558,17 @@ pub async fn get_files_batch(
|
||||
}
|
||||
|
||||
/// Handler for getting multiple folders in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/folders/get",
|
||||
responses(
|
||||
(status = 200, description = "Batch folder details"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn get_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -547,7 +625,7 @@ pub async fn get_folders_batch(
|
||||
}
|
||||
|
||||
/// DTO for batch trash operation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct BatchTrashRequest {
|
||||
/// IDs of the files to move to trash
|
||||
#[serde(default)]
|
||||
@@ -558,7 +636,7 @@ pub struct BatchTrashRequest {
|
||||
}
|
||||
|
||||
/// DTO for batch download requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct BatchDownloadRequest {
|
||||
/// IDs of the files to include in the ZIP
|
||||
#[serde(default)]
|
||||
@@ -569,6 +647,17 @@ pub struct BatchDownloadRequest {
|
||||
}
|
||||
|
||||
/// Handler for moving multiple files and folders to trash in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/trash",
|
||||
responses(
|
||||
(status = 200, description = "All items trashed"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn trash_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -681,6 +770,17 @@ pub async fn trash_batch(
|
||||
}
|
||||
|
||||
/// Handler for moving multiple folders in batch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/folders/move",
|
||||
responses(
|
||||
(status = 200, description = "All folders moved"),
|
||||
(status = 206, description = "Partial success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn move_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
@@ -736,6 +836,17 @@ pub async fn move_folders_batch(
|
||||
///
|
||||
/// The ZIP is written to a temporary file and streamed to the client,
|
||||
/// so RAM usage is O(buffer_size) regardless of archive size.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/batch/download",
|
||||
responses(
|
||||
(status = 200, description = "ZIP archive stream"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "ZIP creation failed")
|
||||
),
|
||||
tag = "batch"
|
||||
)]
|
||||
pub async fn download_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
extract::{Path, Query, Request, State},
|
||||
http::{HeaderMap, StatusCode, header},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::application::ports::chunked_upload_ports::ChunkedUploadPort;
|
||||
use crate::application::ports::chunked_upload_ports::DEFAULT_CHUNK_SIZE;
|
||||
@@ -26,7 +27,7 @@ use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Request body for creating an upload session
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateUploadRequest {
|
||||
pub filename: String,
|
||||
pub folder_id: Option<String>,
|
||||
@@ -43,7 +44,7 @@ pub struct ChunkUploadParams {
|
||||
}
|
||||
|
||||
/// Final response after completing upload
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct CompleteUploadResponse {
|
||||
pub file_id: String,
|
||||
pub filename: String,
|
||||
@@ -52,9 +53,21 @@ pub struct CompleteUploadResponse {
|
||||
}
|
||||
|
||||
/// Chunked Upload Handler
|
||||
///
|
||||
/// The handler struct exists as a named grouping. All route functions are free
|
||||
/// functions at module scope — see the section below the impl block for the reason.
|
||||
pub struct ChunkedUploadHandler;
|
||||
|
||||
impl ChunkedUploadHandler {
|
||||
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
|
||||
// utoipa 5.4.0's proc macro generates helper structs / impls inside its expansion.
|
||||
// Rust allows struct definitions at module scope but forbids them inside impl blocks,
|
||||
// so `#[utoipa::path]` fails on every method in this impl block regardless of HTTP
|
||||
// verb or annotation content. The same macro works fine on FileHandler / FolderHandler
|
||||
// (root cause in utoipa unknown — likely a 5.4.x bug). All five route handlers are
|
||||
// therefore declared as free functions below, which delegate to these `*_impl` methods.
|
||||
// TODO: try removing free-function indirection after a utoipa upgrade.
|
||||
|
||||
/// POST /api/uploads - Create a new upload session
|
||||
///
|
||||
/// Request body:
|
||||
@@ -77,7 +90,7 @@ impl ChunkedUploadHandler {
|
||||
/// "expires_at": 86400
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn create_upload(
|
||||
pub(super) async fn create_upload_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(request): Json<CreateUploadRequest>,
|
||||
@@ -171,7 +184,7 @@ impl ChunkedUploadHandler {
|
||||
/// - checksum: Optional MD5 checksum for verification
|
||||
///
|
||||
/// Body: Raw bytes of the chunk
|
||||
pub async fn upload_chunk(
|
||||
pub(super) async fn upload_chunk_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(upload_id): Path<String>,
|
||||
@@ -220,7 +233,7 @@ impl ChunkedUploadHandler {
|
||||
/// HEAD /api/uploads/:upload_id - Get upload status
|
||||
///
|
||||
/// Returns upload progress and pending chunks
|
||||
pub async fn get_upload_status(
|
||||
pub(super) async fn get_upload_status_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(upload_id): Path<String>,
|
||||
@@ -251,7 +264,7 @@ impl ChunkedUploadHandler {
|
||||
/// POST /api/uploads/:upload_id/complete - Finalize upload
|
||||
///
|
||||
/// Assembles all chunks into the final file and creates the file record
|
||||
pub async fn complete_upload(
|
||||
pub(super) async fn complete_upload_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(upload_id): Path<String>,
|
||||
@@ -324,7 +337,7 @@ impl ChunkedUploadHandler {
|
||||
/// DELETE /api/uploads/:upload_id - Cancel upload
|
||||
///
|
||||
/// Cancels an in-progress upload and cleans up temp files
|
||||
pub async fn cancel_upload(
|
||||
pub(super) async fn cancel_upload_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(upload_id): Path<String>,
|
||||
@@ -342,3 +355,134 @@ impl ChunkedUploadHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Route handlers (free functions) ──────────────────────────────────────────
|
||||
//
|
||||
// All five route functions live here rather than as methods on ChunkedUploadHandler
|
||||
// because utoipa 5.4.0's #[utoipa::path] macro generates helper structs inside its
|
||||
// expansion. Rust allows struct definitions at module scope but forbids them inside
|
||||
// impl blocks — so every #[utoipa::path] annotation on a ChunkedUploadHandler method
|
||||
// fails to compile regardless of HTTP verb or annotation content.
|
||||
//
|
||||
// FileHandler and FolderHandler are not affected (root cause in utoipa unknown, likely
|
||||
// a 5.4.x regression). All logic lives in the ChunkedUploadHandler::*_impl methods
|
||||
// above; these thin wrappers exist solely to carry the OpenAPI annotation at a scope
|
||||
// where utoipa can generate its helper types.
|
||||
//
|
||||
// routes.rs calls these free functions directly.
|
||||
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/uploads",
|
||||
request_body(content = CreateUploadRequest, content_type = "application/json", description = "Upload session parameters"),
|
||||
responses(
|
||||
(status = 201, description = "Upload session created", body = crate::application::ports::chunked_upload_ports::CreateUploadResponseDto),
|
||||
(status = 400, description = "Invalid request (empty filename, zero size, chunk too small)"),
|
||||
(status = 507, description = "Storage quota exceeded"),
|
||||
),
|
||||
tag = "uploads",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn create_upload(
|
||||
state: State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
request: Json<CreateUploadRequest>,
|
||||
) -> impl IntoResponse {
|
||||
ChunkedUploadHandler::create_upload_impl(state, auth_user, request).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/api/uploads/{upload_id}",
|
||||
params(
|
||||
("upload_id" = String, Path, description = "Upload session ID"),
|
||||
("chunk_index" = usize, Query, description = "Zero-based chunk index"),
|
||||
("checksum" = Option<String>, Query, description = "Optional MD5 checksum for integrity verification"),
|
||||
),
|
||||
request_body(content_type = "application/octet-stream", description = "Raw chunk bytes"),
|
||||
responses(
|
||||
(status = 200, description = "Chunk received", body = crate::application::ports::chunked_upload_ports::ChunkUploadResponseDto),
|
||||
(status = 400, description = "Invalid chunk or checksum mismatch"),
|
||||
(status = 404, description = "Upload session not found"),
|
||||
),
|
||||
tag = "uploads",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn upload_chunk(
|
||||
state: State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
query: Query<ChunkUploadParams>,
|
||||
headers: HeaderMap,
|
||||
request: Request,
|
||||
) -> impl IntoResponse {
|
||||
let body = axum::body::to_bytes(request.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
ChunkedUploadHandler::upload_chunk_impl(state, auth_user, path, query, headers, body).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
head,
|
||||
path = "/api/uploads/{upload_id}",
|
||||
params(
|
||||
("upload_id" = String, Path, description = "Upload session ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Upload status in response headers and body", body = crate::application::ports::chunked_upload_ports::UploadStatusResponseDto),
|
||||
(status = 404, description = "Upload session not found"),
|
||||
),
|
||||
tag = "uploads",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn get_upload_status(
|
||||
state: State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
ChunkedUploadHandler::get_upload_status_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/uploads/{upload_id}/complete",
|
||||
params(
|
||||
("upload_id" = String, Path, description = "Upload session ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "File assembled and created", body = CompleteUploadResponse),
|
||||
(status = 404, description = "Upload session not found"),
|
||||
(status = 500, description = "Assembly or file creation failed"),
|
||||
),
|
||||
tag = "uploads",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn complete_upload(
|
||||
state: State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
ChunkedUploadHandler::complete_upload_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/uploads/{upload_id}",
|
||||
params(
|
||||
("upload_id" = String, Path, description = "Upload session ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "Upload cancelled and temp files cleaned up"),
|
||||
(status = 500, description = "Cancel failed"),
|
||||
),
|
||||
tag = "uploads",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn cancel_upload(
|
||||
state: State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
ChunkedUploadHandler::cancel_upload_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use axum::{
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::application::ports::dedup_ports::DedupResultDto;
|
||||
use crate::common::di::AppState;
|
||||
@@ -16,7 +17,7 @@ use std::sync::Arc;
|
||||
type GlobalState = Arc<AppState>;
|
||||
|
||||
/// Response for hash check endpoint
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct HashCheckResponse {
|
||||
/// Whether a blob with this hash already exists
|
||||
pub exists: bool,
|
||||
@@ -31,7 +32,7 @@ pub struct HashCheckResponse {
|
||||
}
|
||||
|
||||
/// Response for upload with dedup endpoint
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct DedupUploadResponse {
|
||||
/// Whether this was a new file or an existing one
|
||||
pub is_new: bool,
|
||||
@@ -46,7 +47,7 @@ pub struct DedupUploadResponse {
|
||||
}
|
||||
|
||||
/// Response for dedup stats endpoint
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct StatsResponse {
|
||||
/// Total number of unique blobs stored
|
||||
pub unique_blobs: u64,
|
||||
@@ -64,8 +65,10 @@ pub struct StatsResponse {
|
||||
pub savings_percentage: f64,
|
||||
}
|
||||
|
||||
/// Handler for deduplication-related endpoints
|
||||
/// Handler for deduplication-related endpoints.
|
||||
///
|
||||
/// All route functions are free functions at module scope — see the section
|
||||
/// below the impl block for the reason (utoipa 5.4.0 limitation).
|
||||
/// Provides endpoints for:
|
||||
/// - Checking if content already exists (by hash)
|
||||
/// - Uploading files with automatic deduplication
|
||||
@@ -73,13 +76,19 @@ pub struct StatsResponse {
|
||||
pub struct DedupHandler;
|
||||
|
||||
impl DedupHandler {
|
||||
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
|
||||
// Same utoipa 5.4.0 limitation as ChunkedUploadHandler: the macro generates
|
||||
// helper structs inside its expansion and Rust forbids structs inside impl blocks.
|
||||
// All route handlers are free functions below; they delegate to these *_impl methods.
|
||||
// TODO: collapse back into the impl block after a utoipa upgrade.
|
||||
|
||||
/// Check if the authenticated user already has a file with the given hash.
|
||||
///
|
||||
/// User-scoped: only reveals whether **this user** owns a file that
|
||||
/// references the blob — never exposes global existence or ref_count.
|
||||
///
|
||||
/// GET /api/dedup/check/{hash}
|
||||
pub async fn check_hash(
|
||||
pub(super) async fn check_hash_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path(hash): Path<String>,
|
||||
@@ -142,7 +151,7 @@ impl DedupHandler {
|
||||
/// the pre-computed hash so the file is never re-read for hashing.
|
||||
///
|
||||
/// POST /api/dedup/upload
|
||||
pub async fn upload_with_dedup(
|
||||
pub(super) async fn upload_with_dedup_impl(
|
||||
State(state): State<GlobalState>,
|
||||
_auth_user: AuthUser,
|
||||
mut multipart: Multipart,
|
||||
@@ -300,7 +309,7 @@ impl DedupHandler {
|
||||
/// - Total references
|
||||
/// - Bytes saved
|
||||
/// - Deduplication ratio
|
||||
pub async fn get_stats(
|
||||
pub(super) async fn get_stats_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
) -> impl IntoResponse {
|
||||
@@ -349,7 +358,7 @@ impl DedupHandler {
|
||||
/// Returns the raw content of a blob **only if** the authenticated user
|
||||
/// owns at least one file that references it. Returns 404 otherwise
|
||||
/// (does not reveal whether the blob exists globally).
|
||||
pub async fn get_blob(
|
||||
pub(super) async fn get_blob_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path(hash): Path<String>,
|
||||
@@ -423,7 +432,7 @@ impl DedupHandler {
|
||||
///
|
||||
/// Verifies integrity and returns current statistics.
|
||||
/// Useful for health checks and auditing.
|
||||
pub async fn recalculate_stats(
|
||||
pub(super) async fn recalculate_stats_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
) -> impl IntoResponse {
|
||||
@@ -485,6 +494,112 @@ impl DedupHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Route handlers (free functions) ──────────────────────────────────────────
|
||||
//
|
||||
// Same utoipa 5.4.0 limitation as ChunkedUploadHandler: #[utoipa::path] cannot
|
||||
// be applied to methods on DedupHandler because the macro generates helper structs
|
||||
// that Rust forbids inside impl blocks. All logic lives in the DedupHandler::*_impl
|
||||
// methods above; these thin wrappers carry the OpenAPI annotation at module scope.
|
||||
//
|
||||
// routes.rs calls these free functions directly instead of DedupHandler::method.
|
||||
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/dedup/check/{hash}",
|
||||
params(
|
||||
("hash" = String, Path, description = "SHA-256 hash (64 hex characters)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Hash check result (user-scoped)", body = HashCheckResponse),
|
||||
(status = 400, description = "Invalid hash format"),
|
||||
),
|
||||
tag = "dedup",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn check_hash(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
DedupHandler::check_hash_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/dedup/upload",
|
||||
request_body(content_type = "multipart/form-data", description = "Multipart form with a 'file' field"),
|
||||
responses(
|
||||
(status = 201, description = "New blob stored", body = DedupUploadResponse),
|
||||
(status = 200, description = "Blob already existed (dedup hit)", body = DedupUploadResponse),
|
||||
(status = 400, description = "No file field or empty file"),
|
||||
(status = 500, description = "Upload failed"),
|
||||
),
|
||||
tag = "dedup",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn upload_with_dedup(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
DedupHandler::upload_with_dedup_impl(state, auth_user, multipart).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/dedup/stats",
|
||||
responses(
|
||||
(status = 200, description = "Deduplication statistics", body = StatsResponse),
|
||||
(status = 403, description = "Admin role required"),
|
||||
),
|
||||
tag = "dedup",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn get_stats(state: State<GlobalState>, auth_user: AuthUser) -> impl IntoResponse {
|
||||
DedupHandler::get_stats_impl(state, auth_user).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/dedup/blob/{hash}",
|
||||
params(
|
||||
("hash" = String, Path, description = "SHA-256 hash of the blob (64 hex characters)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Raw blob content (user-scoped)"),
|
||||
(status = 400, description = "Invalid hash format"),
|
||||
(status = 404, description = "Blob not found or not owned by this user"),
|
||||
),
|
||||
tag = "dedup",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn get_blob(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
DedupHandler::get_blob_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/dedup/recalculate",
|
||||
responses(
|
||||
(status = 200, description = "Statistics after integrity verification", body = StatsResponse),
|
||||
(status = 403, description = "Admin role required"),
|
||||
(status = 500, description = "Integrity verification failed"),
|
||||
),
|
||||
tag = "dedup",
|
||||
security(("bearerAuth" = []))
|
||||
)]
|
||||
pub async fn recalculate_stats(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
) -> impl IntoResponse {
|
||||
DedupHandler::recalculate_stats_impl(state, auth_user).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -11,6 +11,7 @@ use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::ports::file_ports::OptimizedFileContent;
|
||||
use crate::application::ports::file_ports::{
|
||||
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase,
|
||||
@@ -39,6 +40,13 @@ type GlobalState = Arc<AppState>;
|
||||
pub struct FileHandler;
|
||||
|
||||
impl FileHandler {
|
||||
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
|
||||
// utoipa 5.4.0's proc macro generates helper structs / impls inside its expansion.
|
||||
// Rust allows struct definitions at module scope but forbids them inside impl blocks,
|
||||
// so `#[utoipa::path]` fails on every method in this impl block regardless of HTTP
|
||||
// verb or annotation content. All route handlers are free functions below.
|
||||
// TODO: collapse after utoipa upgrade.
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UPLOAD
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -312,7 +320,7 @@ impl FileHandler {
|
||||
/// The DB path is only taken on a **cache miss for images** where the
|
||||
/// thumbnail hasn't been generated yet (first access after upload if
|
||||
/// background generation hasn't finished).
|
||||
pub async fn get_thumbnail(
|
||||
pub(super) async fn get_thumbnail_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
headers: HeaderMap,
|
||||
@@ -462,7 +470,7 @@ impl FileHandler {
|
||||
/// subsequent `GET …/thumbnail/{size}` requests are served instantly.
|
||||
///
|
||||
/// **Max body: 512 KB** — thumbnails are small.
|
||||
pub async fn upload_thumbnail(
|
||||
pub(super) async fn upload_thumbnail_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path((id, size)): Path<(String, String)>,
|
||||
@@ -527,7 +535,7 @@ impl FileHandler {
|
||||
/// streaming) is fully handled by `FileRetrievalUseCase::get_file_optimized`.
|
||||
/// This handler only deals with HTTP concerns: ETag, Range, Content-Disposition,
|
||||
/// and optional compression.
|
||||
pub async fn download_file(
|
||||
pub(super) async fn download_file_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -694,10 +702,8 @@ impl FileHandler {
|
||||
// LIST
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Lists files, extracting `folder_id` from query parameters.
|
||||
///
|
||||
/// Axum-compatible handler wrapper around [`Self::list_files`].
|
||||
pub async fn list_files_query(
|
||||
/// Lists files in a folder, extracting `folder_id` from query parameters.
|
||||
pub(super) async fn list_files_query_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
headers: HeaderMap,
|
||||
@@ -745,7 +751,7 @@ impl FileHandler {
|
||||
/// Delegates to [`Self::upload_file_inner`] and, on success, spawns
|
||||
/// a background task to generate all thumbnail sizes before serialising
|
||||
/// the `FileDto` once.
|
||||
pub async fn upload_file_with_thumbnails(
|
||||
pub(super) async fn upload_file_with_thumbnails_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
multipart: Multipart,
|
||||
@@ -813,7 +819,7 @@ impl FileHandler {
|
||||
/// Returns EXIF/media metadata for a file.
|
||||
///
|
||||
/// Used by the Photos lightbox and for testing EXIF extraction.
|
||||
pub async fn get_file_metadata(
|
||||
pub(super) async fn get_file_metadata_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path(file_id): Path<String>,
|
||||
@@ -859,7 +865,7 @@ impl FileHandler {
|
||||
///
|
||||
/// When auth is available, uses trash-first deletion; otherwise falls back
|
||||
/// to permanent delete so the endpoint works with or without auth.
|
||||
pub async fn delete_file(
|
||||
pub(super) async fn delete_file_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -889,7 +895,7 @@ impl FileHandler {
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Renames a file (ownership-verified)
|
||||
pub async fn rename_file(
|
||||
pub(super) async fn rename_file_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -936,8 +942,8 @@ impl FileHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a file to a different folder (simplified payload, ownership-verified)
|
||||
pub async fn move_file_simple(
|
||||
/// Moves a file to a different folder (ownership-verified)
|
||||
pub(super) async fn move_file_simple_impl(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -1066,3 +1072,207 @@ pub struct MoveFilePayload {
|
||||
/// Target folder ID (None means root)
|
||||
pub folder_id: Option<String>,
|
||||
}
|
||||
|
||||
// ── Route handlers (free functions) ──────────────────────────────────────────
|
||||
//
|
||||
// All annotated route functions live here rather than as methods on FileHandler
|
||||
// because utoipa 5.4.0's #[utoipa::path] macro generates helper structs inside
|
||||
// its expansion. Rust allows struct definitions at module scope but forbids them
|
||||
// inside impl blocks — so every #[utoipa::path] annotation on a FileHandler
|
||||
// method fails to compile regardless of HTTP verb or annotation content.
|
||||
//
|
||||
// All logic lives in the FileHandler::*_impl methods above; these thin wrappers
|
||||
// exist solely to carry the OpenAPI annotation at a scope where utoipa can
|
||||
// generate its helper types.
|
||||
//
|
||||
// routes.rs calls these free functions directly.
|
||||
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/files",
|
||||
params(("folder_id" = Option<String>, Query, description = "Filter by folder ID")),
|
||||
responses(
|
||||
(status = 200, description = "List of files", body = Vec<FileDto>),
|
||||
(status = 304, description = "Not modified"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn list_files_query(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
headers: HeaderMap,
|
||||
query: Query<HashMap<String, String>>,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::list_files_query_impl(state, auth_user, headers, query).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/files/upload",
|
||||
request_body(content_type = "multipart/form-data", description = "File data + optional folder_id field"),
|
||||
responses(
|
||||
(status = 201, description = "File uploaded", body = FileDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 507, description = "Storage quota exceeded"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn upload_file_with_thumbnails(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::upload_file_with_thumbnails_impl(state, auth_user, multipart).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/files/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "File ID"),
|
||||
("metadata" = Option<bool>, Query, description = "Return metadata JSON instead of file content"),
|
||||
("original" = Option<bool>, Query, description = "Skip WebP transcoding"),
|
||||
("inline" = Option<bool>, Query, description = "Content-Disposition: inline"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "File content"),
|
||||
(status = 206, description = "Partial content (Range request)"),
|
||||
(status = 304, description = "Not modified"),
|
||||
(status = 404, description = "File not found"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn download_file(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
query: Query<HashMap<String, String>>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::download_file_impl(state, auth_user, path, query, headers).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/files/{id}/thumbnail/{size}",
|
||||
params(
|
||||
("id" = String, Path, description = "File ID"),
|
||||
("size" = String, Path, description = "Thumbnail size: icon | preview | large"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Thumbnail image (image/jpeg or image/webp)"),
|
||||
(status = 204, description = "No thumbnail available for this file type"),
|
||||
(status = 304, description = "Not modified"),
|
||||
(status = 404, description = "File not found"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn get_thumbnail(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
headers: HeaderMap,
|
||||
path: Path<(String, String)>,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::get_thumbnail_impl(state, auth_user, headers, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/files/{id}/thumbnail/{size}",
|
||||
params(
|
||||
("id" = String, Path, description = "File ID"),
|
||||
("size" = String, Path, description = "Thumbnail size: icon | preview | large"),
|
||||
),
|
||||
request_body(content_type = "application/octet-stream", description = "Raw image bytes (max 512 KB)"),
|
||||
responses(
|
||||
(status = 201, description = "Thumbnail stored"),
|
||||
(status = 400, description = "Invalid image or size too large"),
|
||||
(status = 404, description = "File not found"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn upload_thumbnail(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<(String, String)>,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::upload_thumbnail_impl(state, auth_user, path, body).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/files/{id}/metadata",
|
||||
params(("id" = String, Path, description = "File ID")),
|
||||
responses(
|
||||
(status = 200, description = "File metadata (EXIF, dimensions, duration, etc.)"),
|
||||
(status = 404, description = "File not found"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn get_file_metadata(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::get_file_metadata_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/files/{id}",
|
||||
params(("id" = String, Path, description = "File ID")),
|
||||
responses(
|
||||
(status = 204, description = "File deleted (moved to trash if enabled)"),
|
||||
(status = 404, description = "File not found"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn delete_file(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::delete_file_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/files/{id}/rename",
|
||||
params(("id" = String, Path, description = "File ID")),
|
||||
request_body(content_type = "application/json", description = r#"{"name": "new-name.txt"}"#),
|
||||
responses(
|
||||
(status = 200, description = "Renamed file", body = FileDto),
|
||||
(status = 404, description = "File not found"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn rename_file(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
json: Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::rename_file_impl(state, auth_user, path, json).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/files/{id}/move",
|
||||
params(("id" = String, Path, description = "File ID")),
|
||||
request_body(content = MoveFilePayload, content_type = "application/json", description = "MoveFilePayload"),
|
||||
responses(
|
||||
(status = 200, description = "Moved file", body = FileDto),
|
||||
(status = 404, description = "File or destination not found"),
|
||||
),
|
||||
tag = "files"
|
||||
)]
|
||||
pub async fn move_file_simple(
|
||||
state: State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
json: Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
FileHandler::move_file_simple_impl(state, auth_user, path, json).await
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::application::dtos::folder_dto::{CreateFolderDto, MoveFolderDto, RenameFolderDto};
|
||||
use crate::application::dtos::folder_dto::{
|
||||
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
||||
};
|
||||
use crate::application::dtos::folder_listing_dto::FolderListingDto;
|
||||
use crate::application::dtos::pagination::PaginationRequestDto;
|
||||
use crate::application::ports::file_ports::FileRetrievalUseCase;
|
||||
@@ -27,10 +29,17 @@ type AppState = Arc<FolderService>;
|
||||
pub struct FolderHandler;
|
||||
|
||||
impl FolderHandler {
|
||||
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
|
||||
// utoipa 5.4.0's proc macro generates helper structs / impls inside its expansion.
|
||||
// Rust allows struct definitions at module scope but forbids them inside impl blocks,
|
||||
// so `#[utoipa::path]` fails on every method in this impl block regardless of HTTP
|
||||
// verb or annotation content. All route handlers are free functions below.
|
||||
// TODO: collapse after utoipa upgrade.
|
||||
|
||||
/// Creates a new folder.
|
||||
/// When parent_id is not provided, the folder is created inside the
|
||||
/// authenticated user's home folder rather than at the storage root.
|
||||
pub async fn create_folder(
|
||||
pub(super) async fn create_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Json(mut dto): Json<CreateFolderDto>,
|
||||
@@ -93,7 +102,7 @@ impl FolderHandler {
|
||||
|
||||
/// Gets a folder by ID.
|
||||
/// Validates that the authenticated user owns the folder.
|
||||
pub async fn get_folder(
|
||||
pub(super) async fn get_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -120,7 +129,7 @@ impl FolderHandler {
|
||||
|
||||
/// Lists root folders for the authenticated user.
|
||||
/// Only returns folders owned by this user — no information disclosure.
|
||||
pub async fn list_root_folders(
|
||||
pub(super) async fn list_root_folders_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
) -> axum::response::Response {
|
||||
@@ -129,7 +138,7 @@ impl FolderHandler {
|
||||
|
||||
/// Lists contents of a specific folder by its ID.
|
||||
/// Scoped to the authenticated user's folders.
|
||||
pub async fn list_folder_contents(
|
||||
pub(super) async fn list_folder_contents_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -137,8 +146,9 @@ impl FolderHandler {
|
||||
Self::list_folders_scoped(service, Some(&id), &auth_user).await
|
||||
}
|
||||
|
||||
/// Lists root folders with pagination support.
|
||||
pub async fn list_root_folders_paginated(
|
||||
/// Lists root folders with pagination.
|
||||
/// Scoped to the authenticated user — only returns folders owned by this user.
|
||||
pub(super) async fn list_root_folders_paginated_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
_pagination: Query<PaginationRequestDto>,
|
||||
@@ -146,9 +156,8 @@ impl FolderHandler {
|
||||
Self::list_folders_scoped(service, None, &auth_user).await
|
||||
}
|
||||
|
||||
/// Lists contents of a specific folder with pagination.
|
||||
/// Scoped to the authenticated user — only returns folders owned by this user.
|
||||
pub async fn list_folder_contents_paginated(
|
||||
/// Lists sub-folders inside a folder with pagination.
|
||||
pub(super) async fn list_folder_contents_paginated_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -204,7 +213,7 @@ impl FolderHandler {
|
||||
///
|
||||
/// Both queries run concurrently via `tokio::join!`.
|
||||
/// Supports `If-None-Match` / ETag for conditional responses (304).
|
||||
pub async fn list_folder_listing(
|
||||
pub(super) async fn list_folder_listing_impl(
|
||||
State(state): State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
headers: HeaderMap,
|
||||
@@ -246,8 +255,8 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renames a folder (ownership enforced by service layer)
|
||||
pub async fn rename_folder(
|
||||
/// Renames a folder (ownership enforced).
|
||||
pub(super) async fn rename_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -259,8 +268,8 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a folder to a new parent (ownership enforced by service layer)
|
||||
pub async fn move_folder(
|
||||
/// Moves a folder to a new parent (ownership enforced).
|
||||
pub(super) async fn move_folder_impl(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -284,8 +293,8 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a folder with trash functionality (ownership enforced by service layer)
|
||||
pub async fn delete_folder_with_trash(
|
||||
/// Deletes a folder (moves to trash if enabled, otherwise permanent).
|
||||
pub(super) async fn delete_folder_with_trash_impl(
|
||||
State(state): State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -322,8 +331,8 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads a folder as a ZIP file (ownership enforced)
|
||||
pub async fn download_folder_zip(
|
||||
/// Downloads a folder and all its contents as a ZIP archive.
|
||||
pub(super) async fn download_folder_zip_impl(
|
||||
State(state): State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
@@ -427,3 +436,224 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Route handlers (free functions) ──────────────────────────────────────────
|
||||
//
|
||||
// All annotated route functions live here rather than as methods on FolderHandler
|
||||
// because utoipa 5.4.0's #[utoipa::path] macro generates helper structs inside
|
||||
// its expansion. Rust allows struct definitions at module scope but forbids them
|
||||
// inside impl blocks — so every #[utoipa::path] annotation on a FolderHandler
|
||||
// method fails to compile regardless of HTTP verb or annotation content.
|
||||
//
|
||||
// All logic lives in the FolderHandler::*_impl methods above; these thin wrappers
|
||||
// exist solely to carry the OpenAPI annotation at a scope where utoipa can
|
||||
// generate its helper types.
|
||||
//
|
||||
// routes.rs calls these free functions directly.
|
||||
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/folders",
|
||||
request_body(content = CreateFolderDto, content_type = "application/json", description = "Folder creation payload"),
|
||||
responses(
|
||||
(status = 201, description = "Folder created", body = FolderDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn create_folder(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
json: Json<CreateFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
FolderHandler::create_folder_impl(state, auth_user, json).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}",
|
||||
params(("id" = String, Path, description = "Folder ID")),
|
||||
responses(
|
||||
(status = 200, description = "Folder", body = FolderDto),
|
||||
(status = 404, description = "Folder not found"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn get_folder(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
FolderHandler::get_folder_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders",
|
||||
responses(
|
||||
(status = 200, description = "List of root folders", body = Vec<FolderDto>),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn list_root_folders(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
) -> axum::response::Response {
|
||||
FolderHandler::list_root_folders_impl(state, auth_user).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/contents",
|
||||
params(("id" = String, Path, description = "Folder ID")),
|
||||
responses(
|
||||
(status = 200, description = "List of sub-folders", body = Vec<FolderDto>),
|
||||
(status = 404, description = "Folder not found"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn list_folder_contents(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> axum::response::Response {
|
||||
FolderHandler::list_folder_contents_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/paginated",
|
||||
params(PaginationRequestDto),
|
||||
responses(
|
||||
(status = 200, description = "Paginated list of root folders"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn list_root_folders_paginated(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
pagination: Query<PaginationRequestDto>,
|
||||
) -> axum::response::Response {
|
||||
FolderHandler::list_root_folders_paginated_impl(state, auth_user, pagination).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/contents/paginated",
|
||||
params(
|
||||
("id" = String, Path, description = "Folder ID"),
|
||||
PaginationRequestDto,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Paginated list of sub-folders"),
|
||||
(status = 404, description = "Folder not found"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn list_folder_contents_paginated(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
pagination: Query<PaginationRequestDto>,
|
||||
) -> axum::response::Response {
|
||||
FolderHandler::list_folder_contents_paginated_impl(state, auth_user, path, pagination).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/listing",
|
||||
params(("id" = String, Path, description = "Folder ID")),
|
||||
responses(
|
||||
(status = 200, description = "Folder listing (sub-folders + files)", body = FolderListingDto),
|
||||
(status = 304, description = "Not modified"),
|
||||
(status = 404, description = "Folder not found"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn list_folder_listing(
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
headers: HeaderMap,
|
||||
path: Path<String>,
|
||||
) -> axum::response::Response {
|
||||
FolderHandler::list_folder_listing_impl(state, auth_user, headers, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/folders/{id}/rename",
|
||||
params(("id" = String, Path, description = "Folder ID")),
|
||||
request_body(content = RenameFolderDto, content_type = "application/json", description = "Rename payload"),
|
||||
responses(
|
||||
(status = 200, description = "Renamed folder", body = FolderDto),
|
||||
(status = 404, description = "Folder not found"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn rename_folder(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
json: Json<RenameFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
FolderHandler::rename_folder_impl(state, auth_user, path, json).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/folders/{id}/move",
|
||||
params(("id" = String, Path, description = "Folder ID")),
|
||||
request_body(content = MoveFolderDto, content_type = "application/json", description = "Move payload"),
|
||||
responses(
|
||||
(status = 200, description = "Moved folder", body = FolderDto),
|
||||
(status = 404, description = "Folder or destination not found"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn move_folder(
|
||||
state: State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
json: Json<MoveFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
FolderHandler::move_folder_impl(state, auth_user, path, json).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/folders/{id}",
|
||||
params(("id" = String, Path, description = "Folder ID")),
|
||||
responses(
|
||||
(status = 204, description = "Folder deleted"),
|
||||
(status = 404, description = "Folder not found"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn delete_folder_with_trash(
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
FolderHandler::delete_folder_with_trash_impl(state, auth_user, path).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/download",
|
||||
params(("id" = String, Path, description = "Folder ID")),
|
||||
responses(
|
||||
(status = 200, description = "ZIP archive stream (application/zip)"),
|
||||
(status = 404, description = "Folder not found"),
|
||||
(status = 501, description = "ZIP service not available"),
|
||||
),
|
||||
tag = "folders"
|
||||
)]
|
||||
pub async fn download_folder_zip(
|
||||
state: State<Arc<GlobalAppState>>,
|
||||
auth_user: AuthUser,
|
||||
path: Path<String>,
|
||||
query: Query<HashMap<String, String>>,
|
||||
) -> impl IntoResponse {
|
||||
FolderHandler::download_folder_zip_impl(state, auth_user, path, query).await
|
||||
}
|
||||
|
||||
@@ -18,16 +18,21 @@ type AppState = Arc<I18nApplicationService>;
|
||||
pub struct I18nHandler;
|
||||
|
||||
impl I18nHandler {
|
||||
/// Gets a list of available locales
|
||||
pub async fn get_locales(State(service): State<AppState>) -> impl IntoResponse {
|
||||
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
|
||||
// utoipa 5.4.0's proc macro generates helper structs / impls inside its expansion.
|
||||
// Rust allows struct definitions at module scope but forbids them inside impl blocks,
|
||||
// so `#[utoipa::path]` fails on every method in this impl block regardless of HTTP
|
||||
// verb or annotation content. All route handlers are free functions below.
|
||||
// TODO: collapse after utoipa upgrade.
|
||||
pub(super) async fn get_locales_impl(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(
|
||||
/// Translates a single key to the requested locale.
|
||||
pub(super) async fn translate_impl(
|
||||
State(service): State<AppState>,
|
||||
Query(query): Query<TranslationRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -79,8 +84,8 @@ impl I18nHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets all translations for a locale (Axum-compatible: extracts locale from path)
|
||||
pub async fn get_translations_by_locale(
|
||||
/// Returns all translations for a locale as a flat key→value object.
|
||||
pub(super) async fn get_translations_by_locale_impl(
|
||||
State(service): State<AppState>,
|
||||
Path(locale_code): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -116,3 +121,68 @@ impl I18nHandler {
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Route handlers (free functions) ──────────────────────────────────────────
|
||||
//
|
||||
// All three route functions live here rather than as methods on I18nHandler
|
||||
// because utoipa 5.4.0's #[utoipa::path] macro generates helper structs inside
|
||||
// its expansion. Rust allows struct definitions at module scope but forbids them
|
||||
// inside impl blocks — so every #[utoipa::path] annotation on an I18nHandler
|
||||
// method fails to compile regardless of HTTP verb or annotation content.
|
||||
//
|
||||
// All logic lives in the I18nHandler::*_impl methods above; these thin wrappers
|
||||
// exist solely to carry the OpenAPI annotation at a scope where utoipa can
|
||||
// generate its helper types.
|
||||
//
|
||||
// routes.rs calls these free functions directly.
|
||||
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/i18n/locales",
|
||||
responses(
|
||||
(status = 200, description = "Available locales", body = Vec<LocaleDto>),
|
||||
),
|
||||
tag = "i18n"
|
||||
)]
|
||||
pub async fn get_locales(state: State<AppState>) -> impl IntoResponse {
|
||||
I18nHandler::get_locales_impl(state).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/i18n/translate",
|
||||
params(
|
||||
("key" = String, Query, description = "Translation key"),
|
||||
("locale" = Option<String>, Query, description = "Target locale code (defaults to en)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Translation", body = TranslationResponseDto),
|
||||
(status = 400, description = "Unsupported locale", body = TranslationErrorDto),
|
||||
(status = 404, description = "Key not found"),
|
||||
),
|
||||
tag = "i18n"
|
||||
)]
|
||||
pub async fn translate(
|
||||
state: State<AppState>,
|
||||
query: Query<TranslationRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
I18nHandler::translate_impl(state, query).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/i18n/locales/{locale_code}",
|
||||
params(("locale_code" = String, Path, description = "Locale code, e.g. en, fr, de")),
|
||||
responses(
|
||||
(status = 200, description = "All translations for this locale"),
|
||||
(status = 400, description = "Unsupported locale"),
|
||||
),
|
||||
tag = "i18n"
|
||||
)]
|
||||
pub async fn get_translations_by_locale(
|
||||
state: State<AppState>,
|
||||
path: Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
I18nHandler::get_translations_by_locale_impl(state, path).await
|
||||
}
|
||||
|
||||
@@ -23,6 +23,16 @@ pub struct PaginationQuery {
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/playlists",
|
||||
responses(
|
||||
(status = 201, description = "Playlist created"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn create_playlist(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -34,6 +44,17 @@ pub async fn create_playlist(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/playlists/{playlist_id}",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 200, description = "Playlist details"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn get_playlist(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -45,6 +66,15 @@ pub async fn get_playlist(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/playlists",
|
||||
responses(
|
||||
(status = 200, description = "List of playlists"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn list_playlists(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -62,6 +92,17 @@ pub struct IncludeSharedQuery {
|
||||
pub include_public: Option<bool>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/playlists/{playlist_id}",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 200, description = "Playlist updated"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn update_playlist(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -77,6 +118,17 @@ pub async fn update_playlist(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/playlists/{playlist_id}",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 204, description = "Playlist deleted"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn delete_playlist(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -91,6 +143,17 @@ pub async fn delete_playlist(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/playlists/{playlist_id}/tracks",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 201, description = "Tracks added"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn add_tracks(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -106,6 +169,20 @@ pub async fn add_tracks(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/playlists/{playlist_id}/tracks/{file_id}",
|
||||
params(
|
||||
("playlist_id" = String, Path, description = "Playlist ID"),
|
||||
("file_id" = String, Path, description = "File ID to remove")
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "Track removed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist or track not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn remove_track(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -120,6 +197,17 @@ pub async fn remove_track(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/playlists/{playlist_id}/reorder",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 204, description = "Tracks reordered"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn reorder_tracks(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -135,6 +223,17 @@ pub async fn reorder_tracks(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/playlists/{playlist_id}/tracks",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 200, description = "List of playlist tracks"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn list_playlist_tracks(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -149,6 +248,17 @@ pub async fn list_playlist_tracks(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/playlists/{playlist_id}/share",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 204, description = "Playlist shared"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn share_playlist(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -164,6 +274,20 @@ pub async fn share_playlist(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/playlists/{playlist_id}/share/{user_id}",
|
||||
params(
|
||||
("playlist_id" = String, Path, description = "Playlist ID"),
|
||||
("user_id" = String, Path, description = "User ID to remove share")
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "Share removed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist or share not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn remove_share(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -178,6 +302,17 @@ pub async fn remove_share(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/playlists/{playlist_id}/shares",
|
||||
params(("playlist_id" = String, Path, description = "Playlist ID")),
|
||||
responses(
|
||||
(status = 200, description = "List of playlist shares"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Playlist not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn get_playlist_shares(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
@@ -192,6 +327,17 @@ pub async fn get_playlist_shares(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/playlists/audio-metadata/{file_id}",
|
||||
params(("file_id" = String, Path, description = "Audio file ID")),
|
||||
responses(
|
||||
(status = 200, description = "Audio metadata"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "File not found")
|
||||
),
|
||||
tag = "playlists"
|
||||
)]
|
||||
pub async fn get_audio_metadata(
|
||||
State(music_service): State<Arc<MusicService>>,
|
||||
auth_user: AuthUser,
|
||||
|
||||
@@ -26,6 +26,20 @@ pub struct PhotosQueryParams {
|
||||
///
|
||||
/// Supports cursor-based pagination via the `before` parameter.
|
||||
/// The `X-Next-Cursor` response header contains the cursor for the next page.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/photos",
|
||||
params(
|
||||
("before" = Option<i64>, Query, description = "Cursor: only return items with sort_date before this epoch value"),
|
||||
("limit" = Option<i64>, Query, description = "Max items to return (default 200, max 500)")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of media files sorted by capture date"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "photos"
|
||||
)]
|
||||
pub async fn list_photos(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
|
||||
@@ -6,7 +6,9 @@ use axum::{
|
||||
use serde_json::json;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::application::dtos::search_dto::SearchCriteriaDto;
|
||||
use crate::application::dtos::search_dto::{
|
||||
SearchCriteriaDto, SearchResultsDto, SearchSuggestionsDto,
|
||||
};
|
||||
use crate::application::ports::inbound::SearchUseCase;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
@@ -22,8 +24,13 @@ use std::sync::Arc;
|
||||
pub struct SearchHandler;
|
||||
|
||||
impl SearchHandler {
|
||||
/// GET /search — simple query-parameter-based search.
|
||||
pub async fn search_files_get(
|
||||
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
|
||||
// utoipa 5.4.0's proc macro generates helper structs / impls inside its expansion.
|
||||
// Rust allows struct definitions at module scope but forbids them inside impl blocks,
|
||||
// so `#[utoipa::path]` fails on every method in this impl block regardless of HTTP
|
||||
// verb or annotation content. All route handlers are free functions below.
|
||||
// TODO: collapse after utoipa upgrade.
|
||||
pub(super) async fn search_files_get_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Query(params): Query<SearchParams>,
|
||||
@@ -81,8 +88,8 @@ impl SearchHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /search/advanced — full criteria in the request body.
|
||||
pub async fn search_files_post(
|
||||
/// Advanced search with full criteria in the request body.
|
||||
pub(super) async fn search_files_post_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(criteria): Json<SearchCriteriaDto>,
|
||||
@@ -122,8 +129,8 @@ impl SearchHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /search/suggest — lightweight autocomplete suggestions.
|
||||
pub async fn suggest_files(
|
||||
/// Autocomplete suggestions for search.
|
||||
pub(super) async fn suggest_files_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<SuggestParams>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -167,7 +174,9 @@ impl SearchHandler {
|
||||
}
|
||||
|
||||
/// DELETE /search/cache — clears the search results cache.
|
||||
pub async fn clear_search_cache(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
pub(super) async fn clear_search_cache_impl(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
info!("API: Clearing search cache");
|
||||
|
||||
let search_service = match &state.applications.search_service {
|
||||
@@ -259,3 +268,95 @@ pub struct SuggestParams {
|
||||
/// Maximum number of suggestions (default 10, max 20)
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
// ── Route handlers (free functions) ──────────────────────────────────────────
|
||||
//
|
||||
// All four route functions live here rather than as methods on SearchHandler
|
||||
// because utoipa 5.4.0's #[utoipa::path] macro generates helper structs inside
|
||||
// its expansion. Rust allows struct definitions at module scope but forbids them
|
||||
// inside impl blocks — so every #[utoipa::path] annotation on a SearchHandler
|
||||
// method fails to compile regardless of HTTP verb or annotation content.
|
||||
//
|
||||
// All logic lives in the SearchHandler::*_impl methods above; these thin wrappers
|
||||
// exist solely to carry the OpenAPI annotation at a scope where utoipa can
|
||||
// generate its helper types.
|
||||
//
|
||||
// routes.rs calls these free functions directly.
|
||||
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/search",
|
||||
params(
|
||||
("query" = Option<String>, Query, description = "Text to search in names"),
|
||||
("type" = Option<String>, Query, description = "Comma-separated MIME type filter"),
|
||||
("folder_id" = Option<String>, Query, description = "Restrict search to this folder"),
|
||||
("recursive" = Option<bool>, Query, description = "Include sub-folders"),
|
||||
("limit" = Option<u32>, Query, description = "Max results"),
|
||||
("offset" = Option<u32>, Query, description = "Pagination offset"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Search results", body = SearchResultsDto),
|
||||
(status = 503, description = "Search service unavailable"),
|
||||
),
|
||||
tag = "search"
|
||||
)]
|
||||
pub async fn search_files_get(
|
||||
state: State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
query: Query<SearchParams>,
|
||||
) -> impl IntoResponse {
|
||||
SearchHandler::search_files_get_impl(state, auth_user, query).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/search/advanced",
|
||||
request_body(content = SearchCriteriaDto, content_type = "application/json", description = "Search criteria"),
|
||||
responses(
|
||||
(status = 200, description = "Search results", body = SearchResultsDto),
|
||||
(status = 503, description = "Search service unavailable"),
|
||||
),
|
||||
tag = "search"
|
||||
)]
|
||||
pub async fn search_files_post(
|
||||
state: State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
json: Json<SearchCriteriaDto>,
|
||||
) -> impl IntoResponse {
|
||||
SearchHandler::search_files_post_impl(state, auth_user, json).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/search/suggest",
|
||||
params(
|
||||
("query" = String, Query, description = "Partial name to complete"),
|
||||
("folder_id" = Option<String>, Query, description = "Restrict to this folder"),
|
||||
("limit" = Option<u32>, Query, description = "Max suggestions (default 10, max 20)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Suggestions", body = SearchSuggestionsDto),
|
||||
(status = 503, description = "Search service unavailable"),
|
||||
),
|
||||
tag = "search"
|
||||
)]
|
||||
pub async fn suggest_files(
|
||||
state: State<Arc<AppState>>,
|
||||
query: Query<SuggestParams>,
|
||||
) -> impl IntoResponse {
|
||||
SearchHandler::suggest_files_impl(state, query).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/search/cache",
|
||||
responses(
|
||||
(status = 200, description = "Cache cleared"),
|
||||
(status = 503, description = "Search service unavailable"),
|
||||
),
|
||||
tag = "search"
|
||||
)]
|
||||
pub async fn clear_search_cache(state: State<Arc<AppState>>) -> impl IntoResponse {
|
||||
SearchHandler::clear_search_cache_impl(state).await
|
||||
}
|
||||
|
||||
@@ -280,6 +280,19 @@ pub async fn verify_shared_item_password(
|
||||
///
|
||||
/// Validates the share token, checks it refers to a file (not folder),
|
||||
/// then streams the file content to the caller.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/s/{token}/download",
|
||||
params(("token" = String, Path, description = "Share token")),
|
||||
responses(
|
||||
(status = 200, description = "File content stream"),
|
||||
(status = 401, description = "Password required"),
|
||||
(status = 404, description = "Share not found"),
|
||||
(status = 410, description = "Share expired"),
|
||||
(status = 503, description = "Sharing disabled")
|
||||
),
|
||||
tag = "shares"
|
||||
)]
|
||||
pub async fn download_shared_file(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(token): Path<String>,
|
||||
|
||||
Reference in New Issue
Block a user