feat(openapi): upgrade code to gnerate openapi on all path
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>,
|
||||
|
||||
+135
-1
@@ -15,6 +15,9 @@ use crate::application::dtos::folder_dto::{
|
||||
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
||||
};
|
||||
use crate::application::dtos::folder_listing_dto::FolderListingDto;
|
||||
use crate::application::dtos::i18n_dto::{
|
||||
LocaleDto, TranslationErrorDto, TranslationRequestDto, TranslationResponseDto,
|
||||
};
|
||||
use crate::application::dtos::pagination::{PaginationDto, PaginationRequestDto};
|
||||
use crate::application::dtos::recent_dto::RecentItemDto;
|
||||
use crate::application::dtos::search_dto::{
|
||||
@@ -31,17 +34,72 @@ use crate::application::dtos::user_dto::{
|
||||
AuthResponseDto, ChangePasswordDto, LoginDto, RefreshTokenDto, RegisterDto, SetupAdminDto,
|
||||
UserDto,
|
||||
};
|
||||
use crate::application::ports::chunked_upload_ports::{
|
||||
ChunkUploadResponseDto, CreateUploadResponseDto, UploadStatusResponseDto,
|
||||
};
|
||||
use crate::interfaces::api::handlers::chunked_upload_handler::{
|
||||
CompleteUploadResponse, CreateUploadRequest,
|
||||
};
|
||||
use crate::interfaces::api::handlers::dedup_handler::{
|
||||
DedupUploadResponse, HashCheckResponse, StatsResponse,
|
||||
};
|
||||
use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
// File handlers (free functions — see file_handler.rs for why)
|
||||
handlers::file_handler::list_files_query,
|
||||
handlers::file_handler::upload_file_with_thumbnails,
|
||||
handlers::file_handler::download_file,
|
||||
handlers::file_handler::get_thumbnail,
|
||||
handlers::file_handler::upload_thumbnail,
|
||||
handlers::file_handler::get_file_metadata,
|
||||
handlers::file_handler::delete_file,
|
||||
handlers::file_handler::rename_file,
|
||||
handlers::file_handler::move_file_simple,
|
||||
// Folder handlers (free functions — see folder_handler.rs for why)
|
||||
handlers::folder_handler::create_folder,
|
||||
handlers::folder_handler::get_folder,
|
||||
handlers::folder_handler::list_root_folders,
|
||||
handlers::folder_handler::list_folder_contents,
|
||||
handlers::folder_handler::list_root_folders_paginated,
|
||||
handlers::folder_handler::list_folder_contents_paginated,
|
||||
handlers::folder_handler::list_folder_listing,
|
||||
handlers::folder_handler::rename_folder,
|
||||
handlers::folder_handler::move_folder,
|
||||
handlers::folder_handler::delete_folder_with_trash,
|
||||
handlers::folder_handler::download_folder_zip,
|
||||
// Search handlers (free functions — see search_handler.rs for why)
|
||||
handlers::search_handler::search_files_get,
|
||||
handlers::search_handler::search_files_post,
|
||||
handlers::search_handler::suggest_files,
|
||||
handlers::search_handler::clear_search_cache,
|
||||
// i18n handlers (free functions — see i18n_handler.rs for why)
|
||||
handlers::i18n_handler::get_locales,
|
||||
handlers::i18n_handler::translate,
|
||||
handlers::i18n_handler::get_translations_by_locale,
|
||||
// Chunked upload handlers — all five are free functions (not impl methods) because
|
||||
// utoipa 5.4.0 cannot annotate methods on ChunkedUploadHandler; see handler file.
|
||||
handlers::chunked_upload_handler::create_upload,
|
||||
handlers::chunked_upload_handler::upload_chunk,
|
||||
handlers::chunked_upload_handler::get_upload_status,
|
||||
handlers::chunked_upload_handler::complete_upload,
|
||||
handlers::chunked_upload_handler::cancel_upload,
|
||||
// Dedup handlers — all free functions for the same utoipa reason as chunked uploads.
|
||||
handlers::dedup_handler::check_hash,
|
||||
handlers::dedup_handler::upload_with_dedup,
|
||||
handlers::dedup_handler::get_stats,
|
||||
handlers::dedup_handler::get_blob,
|
||||
handlers::dedup_handler::recalculate_stats,
|
||||
// Trash handlers (free functions)
|
||||
handlers::trash_handler::get_trash_items,
|
||||
handlers::trash_handler::move_file_to_trash,
|
||||
handlers::trash_handler::move_folder_to_trash,
|
||||
handlers::trash_handler::restore_from_trash,
|
||||
handlers::trash_handler::delete_permanently,
|
||||
handlers::trash_handler::empty_trash,
|
||||
// Share handlers (free functions)
|
||||
handlers::share_handler::create_shared_link,
|
||||
handlers::share_handler::get_shared_link,
|
||||
handlers::share_handler::get_user_shares,
|
||||
@@ -49,14 +107,68 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
handlers::share_handler::delete_shared_link,
|
||||
handlers::share_handler::access_shared_item,
|
||||
handlers::share_handler::verify_shared_item_password,
|
||||
handlers::share_handler::download_shared_file,
|
||||
// Favorites handlers (free functions)
|
||||
handlers::favorites_handler::get_favorites,
|
||||
handlers::favorites_handler::add_favorite,
|
||||
handlers::favorites_handler::remove_favorite,
|
||||
handlers::favorites_handler::batch_add_favorites,
|
||||
// Recent handlers (free functions)
|
||||
handlers::recent_handler::get_recent_items,
|
||||
handlers::recent_handler::record_item_access,
|
||||
handlers::recent_handler::remove_from_recent,
|
||||
handlers::recent_handler::clear_recent_items,
|
||||
// Photos handler (free function)
|
||||
handlers::photos_handler::list_photos,
|
||||
// Batch handlers (free functions)
|
||||
handlers::batch_handler::move_files_batch,
|
||||
handlers::batch_handler::copy_files_batch,
|
||||
handlers::batch_handler::delete_files_batch,
|
||||
handlers::batch_handler::get_files_batch,
|
||||
handlers::batch_handler::delete_folders_batch,
|
||||
handlers::batch_handler::create_folders_batch,
|
||||
handlers::batch_handler::get_folders_batch,
|
||||
handlers::batch_handler::move_folders_batch,
|
||||
handlers::batch_handler::trash_batch,
|
||||
handlers::batch_handler::download_batch,
|
||||
// Music/playlist handlers (free functions)
|
||||
handlers::music_handler::create_playlist,
|
||||
handlers::music_handler::list_playlists,
|
||||
handlers::music_handler::get_playlist,
|
||||
handlers::music_handler::update_playlist,
|
||||
handlers::music_handler::delete_playlist,
|
||||
handlers::music_handler::list_playlist_tracks,
|
||||
handlers::music_handler::add_tracks,
|
||||
handlers::music_handler::remove_track,
|
||||
handlers::music_handler::reorder_tracks,
|
||||
handlers::music_handler::share_playlist,
|
||||
handlers::music_handler::remove_share,
|
||||
handlers::music_handler::get_playlist_shares,
|
||||
handlers::music_handler::get_audio_metadata,
|
||||
// Admin handlers (pub free functions)
|
||||
handlers::admin_handler::get_dashboard_stats,
|
||||
handlers::admin_handler::list_users,
|
||||
handlers::admin_handler::get_user,
|
||||
handlers::admin_handler::create_user,
|
||||
handlers::admin_handler::delete_user,
|
||||
handlers::admin_handler::update_user_role,
|
||||
handlers::admin_handler::update_user_active,
|
||||
handlers::admin_handler::update_user_quota,
|
||||
handlers::admin_handler::reset_user_password,
|
||||
handlers::admin_handler::get_registration_setting,
|
||||
handlers::admin_handler::set_registration_setting,
|
||||
handlers::admin_handler::get_general_settings,
|
||||
handlers::admin_handler::get_oidc_settings,
|
||||
handlers::admin_handler::save_oidc_settings,
|
||||
handlers::admin_handler::get_storage_settings,
|
||||
handlers::admin_handler::save_storage_settings,
|
||||
handlers::admin_handler::get_migration_status,
|
||||
handlers::admin_handler::start_migration,
|
||||
handlers::admin_handler::pause_migration,
|
||||
handlers::admin_handler::resume_migration,
|
||||
handlers::admin_handler::complete_migration,
|
||||
handlers::admin_handler::verify_migration,
|
||||
handlers::admin_handler::generate_encryption_key,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@@ -102,16 +214,38 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
BatchFavoritesStats,
|
||||
// Recent schemas
|
||||
RecentItemDto,
|
||||
// i18n schemas
|
||||
LocaleDto,
|
||||
TranslationRequestDto,
|
||||
TranslationResponseDto,
|
||||
TranslationErrorDto,
|
||||
// Chunked upload schemas
|
||||
CreateUploadRequest,
|
||||
CompleteUploadResponse,
|
||||
CreateUploadResponseDto,
|
||||
ChunkUploadResponseDto,
|
||||
UploadStatusResponseDto,
|
||||
// Dedup schemas
|
||||
HashCheckResponse,
|
||||
DedupUploadResponse,
|
||||
StatsResponse,
|
||||
)
|
||||
),
|
||||
tags(
|
||||
(name = "folders", description = "Folder management endpoints"),
|
||||
(name = "files", description = "File management endpoints"),
|
||||
(name = "folders", description = "Folder management endpoints"),
|
||||
(name = "trash", description = "Trash / recycle bin endpoints"),
|
||||
(name = "search", description = "Search endpoints"),
|
||||
(name = "shares", description = "Shared links endpoints"),
|
||||
(name = "favorites", description = "Favorites management endpoints"),
|
||||
(name = "recent", description = "Recent items endpoints"),
|
||||
(name = "photos", description = "Photos timeline endpoints"),
|
||||
(name = "i18n", description = "Internationalisation endpoints"),
|
||||
(name = "uploads", description = "Chunked / resumable upload endpoints"),
|
||||
(name = "dedup", description = "Content deduplication endpoints"),
|
||||
(name = "batch", description = "Batch operation endpoints"),
|
||||
(name = "playlists", description = "Music playlist endpoints"),
|
||||
(name = "admin", description = "Admin management endpoints"),
|
||||
),
|
||||
info(
|
||||
title = "OxiCloud API",
|
||||
|
||||
@@ -25,10 +25,24 @@ async fn get_openapi_spec() -> AxumJson<utoipa::openapi::OpenApi> {
|
||||
|
||||
use crate::interfaces::api::handlers::admin_handler;
|
||||
use crate::interfaces::api::handlers::batch_handler::{self, BatchHandlerState};
|
||||
use crate::interfaces::api::handlers::chunked_upload_handler::ChunkedUploadHandler;
|
||||
use crate::interfaces::api::handlers::file_handler::FileHandler;
|
||||
use crate::interfaces::api::handlers::folder_handler::FolderHandler;
|
||||
use crate::interfaces::api::handlers::i18n_handler::I18nHandler;
|
||||
use crate::interfaces::api::handlers::chunked_upload_handler::{
|
||||
cancel_upload, complete_upload, create_upload, get_upload_status, upload_chunk,
|
||||
};
|
||||
use crate::interfaces::api::handlers::file_handler::{
|
||||
delete_file, download_file, get_file_metadata, get_thumbnail, list_files_query,
|
||||
move_file_simple, rename_file, upload_file_with_thumbnails, upload_thumbnail,
|
||||
};
|
||||
use crate::interfaces::api::handlers::folder_handler::{
|
||||
create_folder, delete_folder_with_trash, download_folder_zip, get_folder, list_folder_contents,
|
||||
list_folder_contents_paginated, list_folder_listing, list_root_folders,
|
||||
list_root_folders_paginated, move_folder, rename_folder,
|
||||
};
|
||||
use crate::interfaces::api::handlers::i18n_handler::{
|
||||
get_locales, get_translations_by_locale, translate,
|
||||
};
|
||||
use crate::interfaces::api::handlers::search_handler::{
|
||||
clear_search_cache, search_files_get, search_files_post, suggest_files,
|
||||
};
|
||||
use crate::interfaces::api::handlers::trash_handler;
|
||||
|
||||
/// Creates public API routes that should NOT require authentication.
|
||||
@@ -62,12 +76,9 @@ pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppStat
|
||||
// i18n routes — no auth required (localization should be available before login)
|
||||
if let Some(i18n_service) = i18n_service {
|
||||
let i18n_router = Router::new()
|
||||
.route("/locales", get(I18nHandler::get_locales))
|
||||
.route("/translate", get(I18nHandler::translate))
|
||||
.route(
|
||||
"/locales/{locale_code}",
|
||||
get(I18nHandler::get_translations_by_locale),
|
||||
)
|
||||
.route("/locales", get(get_locales))
|
||||
.route("/translate", get(translate))
|
||||
.route("/locales/{locale_code}", get(get_translations_by_locale))
|
||||
.with_state(i18n_service);
|
||||
|
||||
router = router.nest("/i18n", i18n_router);
|
||||
@@ -114,37 +125,33 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
|
||||
// Create the basic folders router with service operations
|
||||
let folders_basic_router = Router::new()
|
||||
.route("/", post(FolderHandler::create_folder))
|
||||
.route("/", get(FolderHandler::list_root_folders))
|
||||
.route(
|
||||
"/paginated",
|
||||
get(FolderHandler::list_root_folders_paginated),
|
||||
)
|
||||
.route("/{id}", get(FolderHandler::get_folder))
|
||||
.route("/{id}/contents", get(FolderHandler::list_folder_contents))
|
||||
.route("/", post(create_folder))
|
||||
.route("/", get(list_root_folders))
|
||||
.route("/paginated", get(list_root_folders_paginated))
|
||||
.route("/{id}", get(get_folder))
|
||||
.route("/{id}/contents", get(list_folder_contents))
|
||||
.route(
|
||||
"/{id}/contents/paginated",
|
||||
get(FolderHandler::list_folder_contents_paginated),
|
||||
get(list_folder_contents_paginated),
|
||||
)
|
||||
.route("/{id}/rename", put(FolderHandler::rename_folder))
|
||||
.route("/{id}/move", put(FolderHandler::move_folder))
|
||||
.route("/{id}/rename", put(rename_folder))
|
||||
.route("/{id}/move", put(move_folder))
|
||||
.with_state(folder_service.clone());
|
||||
|
||||
// Special route for ZIP download that requires AppState instead of just FolderService
|
||||
let folder_zip_router = Router::new()
|
||||
.route("/{id}/download", get(FolderHandler::download_folder_zip))
|
||||
.route("/{id}/download", get(download_folder_zip))
|
||||
.with_state(app_state.clone());
|
||||
|
||||
// Combined listing endpoint: returns both sub-folders AND files in one
|
||||
// response. Needs full AppState because it calls both FolderService
|
||||
// and FileRetrievalService concurrently.
|
||||
let folder_listing_router = Router::new()
|
||||
.route("/{id}/listing", get(FolderHandler::list_folder_listing))
|
||||
.route("/{id}/listing", get(list_folder_listing))
|
||||
.with_state(app_state.clone());
|
||||
|
||||
// Create folder operations that use trash (requires full AppState)
|
||||
let folders_ops_router =
|
||||
Router::new().route("/{id}", delete(FolderHandler::delete_folder_with_trash));
|
||||
let folders_ops_router = Router::new().route("/{id}", delete(delete_folder_with_trash));
|
||||
|
||||
// Merge the routers
|
||||
let folders_router = folders_basic_router
|
||||
@@ -154,14 +161,14 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
|
||||
// Create file routes for basic operations and trash-enabled delete
|
||||
let basic_file_router = Router::new()
|
||||
.route("/", get(FileHandler::list_files_query))
|
||||
.route("/upload", post(FileHandler::upload_file_with_thumbnails))
|
||||
.route("/{id}", get(FileHandler::download_file))
|
||||
.route("/", get(list_files_query))
|
||||
.route("/upload", post(upload_file_with_thumbnails))
|
||||
.route("/{id}", get(download_file))
|
||||
.route(
|
||||
"/{id}/thumbnail/{size}",
|
||||
get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail),
|
||||
get(get_thumbnail).put(upload_thumbnail),
|
||||
)
|
||||
.route("/{id}/metadata", get(FileHandler::get_file_metadata))
|
||||
.route("/{id}/metadata", get(get_file_metadata))
|
||||
.layer(DefaultBodyLimit::max({
|
||||
// Use architecture-appropriate body limit: 10 GB on 64-bit, 1 GB on 32-bit
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
@@ -174,9 +181,9 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
|
||||
// File operations with trash support
|
||||
let file_operations_router = Router::new()
|
||||
.route("/{id}", delete(FileHandler::delete_file))
|
||||
.route("/{id}/move", put(FileHandler::move_file_simple))
|
||||
.route("/{id}/rename", put(FileHandler::rename_file));
|
||||
.route("/{id}", delete(delete_file))
|
||||
.route("/{id}/move", put(move_file_simple))
|
||||
.route("/{id}/rename", put(rename_file));
|
||||
|
||||
// Merge the routers
|
||||
let files_router = basic_file_router.merge(file_operations_router);
|
||||
@@ -201,17 +208,15 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
|
||||
// Create search routes if the service is available
|
||||
let search_router = if search_service.is_some() {
|
||||
use crate::interfaces::api::handlers::search_handler::SearchHandler;
|
||||
|
||||
Router::new()
|
||||
// Simple search with query parameters
|
||||
.route("/", get(SearchHandler::search_files_get))
|
||||
.route("/", get(search_files_get))
|
||||
// Lightweight autocomplete suggestions
|
||||
.route("/suggest", get(SearchHandler::suggest_files))
|
||||
.route("/suggest", get(suggest_files))
|
||||
// Advanced search with full criteria object
|
||||
.route("/advanced", post(SearchHandler::search_files_post))
|
||||
.route("/advanced", post(search_files_post))
|
||||
// Clear search cache
|
||||
.route("/cache", delete(SearchHandler::clear_search_cache))
|
||||
.route("/cache", delete(clear_search_cache))
|
||||
.with_state(app_state.clone())
|
||||
} else {
|
||||
Router::new()
|
||||
@@ -275,49 +280,32 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
};
|
||||
|
||||
// Create routes for chunked uploads (large files >10MB)
|
||||
// Create routes for chunked uploads (large files >10MB).
|
||||
// All five handlers are free functions — see chunked_upload_handler.rs for why
|
||||
// #[utoipa::path] cannot be applied to ChunkedUploadHandler impl methods directly.
|
||||
let chunked_upload_router = Router::new()
|
||||
.route("/", post(ChunkedUploadHandler::create_upload))
|
||||
.route(
|
||||
"/{upload_id}",
|
||||
axum::routing::patch(ChunkedUploadHandler::upload_chunk),
|
||||
)
|
||||
.route(
|
||||
"/{upload_id}",
|
||||
axum::routing::head(ChunkedUploadHandler::get_upload_status),
|
||||
)
|
||||
.route(
|
||||
"/{upload_id}/complete",
|
||||
post(ChunkedUploadHandler::complete_upload),
|
||||
)
|
||||
.route("/{upload_id}", delete(ChunkedUploadHandler::cancel_upload))
|
||||
.route("/", post(create_upload))
|
||||
.route("/{upload_id}", axum::routing::patch(upload_chunk))
|
||||
.route("/{upload_id}", axum::routing::head(get_upload_status))
|
||||
.route("/{upload_id}/complete", post(complete_upload))
|
||||
.route("/{upload_id}", delete(cancel_upload))
|
||||
.with_state(app_state.clone());
|
||||
|
||||
// Create routes for deduplication endpoints
|
||||
// Create routes for deduplication endpoints.
|
||||
// All handlers are free functions — see dedup_handler.rs for why
|
||||
// #[utoipa::path] cannot be applied to DedupHandler impl methods directly.
|
||||
use super::handlers::dedup_handler::{
|
||||
check_hash, get_blob, get_stats, recalculate_stats, upload_with_dedup,
|
||||
};
|
||||
let dedup_router = Router::new()
|
||||
.route(
|
||||
"/check/{hash}",
|
||||
get(super::handlers::dedup_handler::DedupHandler::check_hash),
|
||||
)
|
||||
.route(
|
||||
"/upload",
|
||||
post(super::handlers::dedup_handler::DedupHandler::upload_with_dedup),
|
||||
)
|
||||
.route(
|
||||
"/stats",
|
||||
get(super::handlers::dedup_handler::DedupHandler::get_stats),
|
||||
)
|
||||
.route(
|
||||
"/blob/{hash}",
|
||||
get(super::handlers::dedup_handler::DedupHandler::get_blob),
|
||||
)
|
||||
.route("/check/{hash}", get(check_hash))
|
||||
.route("/upload", post(upload_with_dedup))
|
||||
.route("/stats", get(get_stats))
|
||||
.route("/blob/{hash}", get(get_blob))
|
||||
// NOTE: remove_reference is intentionally NOT exposed as a public
|
||||
// endpoint — ref_count management is an internal concern handled
|
||||
// automatically when files are deleted via the file API.
|
||||
.route(
|
||||
"/recalculate",
|
||||
post(super::handlers::dedup_handler::DedupHandler::recalculate_stats),
|
||||
)
|
||||
.route("/recalculate", post(recalculate_stats))
|
||||
.with_state(app_state.clone());
|
||||
|
||||
let mut router = Router::new()
|
||||
|
||||
Reference in New Issue
Block a user