feat(openapi): upgrade code to gnerate openapi on all path

This commit is contained in:
Edouard Vanbelle
2026-04-27 22:59:18 +02:00
parent 05d3b2abaa
commit d0c6bd6a73
17 changed files with 1723 additions and 191 deletions
+223 -13
View File
@@ -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
}