2026-03-04 23:55:08 +01:00
|
|
|
use crate::application::services::batch_operations::BatchOperationService;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::common::di::AppState;
|
2025-03-17 21:28:08 +01:00
|
|
|
use axum::{
|
|
|
|
|
Router,
|
2026-05-11 00:22:03 +02:00
|
|
|
extract::{DefaultBodyLimit, State},
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
response::{IntoResponse, Json as AxumJson},
|
2026-02-14 01:29:34 +01:00
|
|
|
routing::{delete, get, post, put},
|
2025-03-17 21:28:08 +01:00
|
|
|
};
|
2026-02-08 22:44:42 +01:00
|
|
|
use serde_json::json;
|
2026-02-14 01:29:34 +01:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
2026-03-29 18:49:10 +02:00
|
|
|
use utoipa::OpenApi;
|
2025-03-19 00:44:27 +01:00
|
|
|
|
2026-05-11 00:22:03 +02:00
|
|
|
/// Liveness probe — returns 200 if the process is running, no DB check.
|
|
|
|
|
async fn health() -> impl IntoResponse {
|
|
|
|
|
(StatusCode::OK, AxumJson(json!({"status": "ok"})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Readiness probe — returns 200 if the DB pool can serve queries, 503 otherwise.
|
|
|
|
|
async fn ready(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
|
|
|
|
match &state.db_pool {
|
|
|
|
|
Some(pool) => match sqlx::query("SELECT 1").execute(pool.as_ref()).await {
|
|
|
|
|
Ok(_) => (
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
AxumJson(json!({"status": "ok", "db": "ok"})),
|
|
|
|
|
),
|
|
|
|
|
Err(_) => (
|
|
|
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
AxumJson(json!({"status": "error", "db": "error"})),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
None => (
|
|
|
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
AxumJson(json!({"status": "error", "db": "not configured"})),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 12:25:10 +02:00
|
|
|
/// Returns the application version from Cargo.toml (compile-time constant)
|
2026-02-08 22:44:42 +01:00
|
|
|
async fn get_version() -> AxumJson<serde_json::Value> {
|
|
|
|
|
AxumJson(json!({
|
|
|
|
|
"name": "OxiCloud",
|
|
|
|
|
"version": env!("CARGO_PKG_VERSION")
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
async fn get_openapi_spec() -> AxumJson<utoipa::openapi::OpenApi> {
|
|
|
|
|
AxumJson(super::ApiDoc::openapi())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::interfaces::api::handlers::admin_handler;
|
|
|
|
|
use crate::interfaces::api::handlers::batch_handler::{self, BatchHandlerState};
|
2026-04-27 22:59:18 +02:00
|
|
|
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,
|
|
|
|
|
};
|
2026-05-28 00:44:20 +02:00
|
|
|
#[allow(deprecated)]
|
2026-04-27 22:59:18 +02:00
|
|
|
use crate::interfaces::api::handlers::folder_handler::{
|
|
|
|
|
create_folder, delete_folder_with_trash, download_folder_zip, get_folder, list_folder_contents,
|
2026-05-28 00:44:20 +02:00
|
|
|
list_folder_contents_paginated, list_folder_listing, list_folder_resources, list_root_folders,
|
2026-04-27 22:59:18 +02:00
|
|
|
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,
|
|
|
|
|
};
|
2026-02-08 13:40:23 +01:00
|
|
|
use crate::interfaces::api::handlers::trash_handler;
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2026-05-11 00:22:03 +02:00
|
|
|
/// Creates root-level health check routes — mounted directly at `/`, not under `/api/`.
|
|
|
|
|
/// (follow docker/kubernetes best practices)
|
|
|
|
|
///
|
|
|
|
|
/// - `GET /health` — liveness probe, no DB check, always 200 if process is up.
|
|
|
|
|
/// - `GET /ready` — readiness probe, pings DB pool, returns 503 if unreachable.
|
|
|
|
|
pub fn create_health_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/health", get(health))
|
|
|
|
|
.route("/ready", get(ready))
|
|
|
|
|
.with_state(app_state.clone())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
/// Creates public API routes that should NOT require authentication.
|
2026-02-24 15:11:56 +01:00
|
|
|
pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
2026-02-08 13:40:23 +01:00
|
|
|
let share_service = app_state.share_service.clone();
|
|
|
|
|
let i18n_service = Some(app_state.applications.i18n_service.clone());
|
|
|
|
|
|
|
|
|
|
let mut router = Router::new();
|
|
|
|
|
|
|
|
|
|
// Public share access routes — no auth required
|
|
|
|
|
if let Some(share_service) = share_service {
|
|
|
|
|
use crate::interfaces::api::handlers::share_handler;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
let public_share_router = Router::new()
|
|
|
|
|
.route("/{token}", get(share_handler::access_shared_item))
|
2026-02-14 01:29:34 +01:00
|
|
|
.route(
|
|
|
|
|
"/{token}/verify",
|
|
|
|
|
post(share_handler::verify_shared_item_password),
|
|
|
|
|
)
|
2026-02-08 13:40:23 +01:00
|
|
|
.with_state(share_service);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
router = router.nest("/s", public_share_router);
|
2026-04-12 01:51:30 +02:00
|
|
|
|
2026-05-05 22:41:55 +02:00
|
|
|
// AppState-backed share endpoints (download, contents, file, zip)
|
|
|
|
|
router = router
|
|
|
|
|
.route(
|
|
|
|
|
"/s/{token}/download",
|
|
|
|
|
get(share_handler::download_shared_file),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/s/{token}/contents",
|
|
|
|
|
get(share_handler::list_share_contents_root),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/s/{token}/contents/{folder_id}",
|
|
|
|
|
get(share_handler::list_share_contents_subfolder),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/s/{token}/file/{file_id}",
|
|
|
|
|
get(share_handler::download_share_file_in_folder),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/s/{token}/zip",
|
|
|
|
|
get(share_handler::download_share_zip_root),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/s/{token}/zip/{folder_id}",
|
|
|
|
|
get(share_handler::download_share_zip_subfolder),
|
|
|
|
|
);
|
2026-02-08 13:40:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// i18n routes — no auth required (localization should be available before login)
|
|
|
|
|
if let Some(i18n_service) = i18n_service {
|
|
|
|
|
let i18n_router = Router::new()
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/locales", get(get_locales))
|
|
|
|
|
.route("/translate", get(translate))
|
|
|
|
|
.route("/locales/{locale_code}", get(get_translations_by_locale))
|
2026-02-08 13:40:23 +01:00
|
|
|
.with_state(i18n_service);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
router = router.nest("/i18n", i18n_router);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// Version endpoint — public, no auth required
|
|
|
|
|
router = router.route("/version", get(get_version));
|
2026-03-29 18:49:10 +02:00
|
|
|
router = router.route("/openapi.json", get(get_openapi_spec));
|
2026-02-08 22:44:42 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
router
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates protected API routes for the application.
|
|
|
|
|
///
|
|
|
|
|
/// These routes require authentication when auth is enabled.
|
|
|
|
|
/// Receives the fully-assembled `AppState` and extracts all needed services
|
|
|
|
|
/// from it, avoiding a long parameter list.
|
2026-05-28 00:44:20 +02:00
|
|
|
// Legacy folder endpoints (contents, listing) are kept for backward-compat;
|
|
|
|
|
// they are marked #[deprecated] so the OpenAPI spec shows them as deprecated.
|
|
|
|
|
#[allow(deprecated)]
|
2026-02-24 15:11:56 +01:00
|
|
|
pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
2026-02-08 13:40:23 +01:00
|
|
|
// Extract services from the pre-built AppState
|
|
|
|
|
let folder_service = app_state.applications.folder_service_concrete.clone();
|
|
|
|
|
let file_retrieval_service = app_state.applications.file_retrieval_service.clone();
|
|
|
|
|
let file_management_service = app_state.applications.file_management_service.clone();
|
|
|
|
|
let trash_service = app_state.trash_service.clone();
|
|
|
|
|
let search_service = app_state.applications.search_service.clone();
|
|
|
|
|
let share_service = app_state.share_service.clone();
|
|
|
|
|
let favorites_service = app_state.favorites_service.clone();
|
|
|
|
|
let recent_service = app_state.recent_service.clone();
|
2026-05-24 01:05:13 +02:00
|
|
|
// authorization is no longer extracted separately — the grants router now
|
|
|
|
|
// uses app_state directly so handlers can access all services.
|
2026-02-08 13:40:23 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Initialize the batch operations service
|
2026-02-15 23:45:11 +01:00
|
|
|
let mut batch_service_builder = BatchOperationService::default(
|
2026-02-08 13:40:23 +01:00
|
|
|
file_retrieval_service.clone(),
|
|
|
|
|
file_management_service.clone(),
|
2026-02-14 01:29:34 +01:00
|
|
|
folder_service.clone(),
|
2026-02-15 23:45:11 +01:00
|
|
|
);
|
|
|
|
|
if let Some(ref ts) = trash_service {
|
|
|
|
|
batch_service_builder = batch_service_builder.with_trash_service(ts.clone());
|
|
|
|
|
}
|
|
|
|
|
let batch_service = Arc::new(batch_service_builder);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Create state for the batch operations handler
|
2025-03-19 00:44:27 +01:00
|
|
|
let batch_handler_state = BatchHandlerState {
|
|
|
|
|
batch_service: batch_service.clone(),
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
// Create the basic folders router with service operations
|
|
|
|
|
let folders_basic_router = Router::new()
|
2026-04-27 22:59:18 +02:00
|
|
|
.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))
|
2026-02-14 01:29:34 +01:00
|
|
|
.route(
|
|
|
|
|
"/{id}/contents/paginated",
|
2026-04-27 22:59:18 +02:00
|
|
|
get(list_folder_contents_paginated),
|
2026-02-14 01:29:34 +01:00
|
|
|
)
|
2026-05-28 00:44:20 +02:00
|
|
|
.route("/{id}/resources", get(list_folder_resources))
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/{id}/rename", put(rename_folder))
|
|
|
|
|
.route("/{id}/move", put(move_folder))
|
2025-03-26 18:33:22 +01:00
|
|
|
.with_state(folder_service.clone());
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-02 01:22:05 +02:00
|
|
|
// Special route for ZIP download that requires AppState instead of just FolderService
|
|
|
|
|
let folder_zip_router = Router::new()
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/{id}/download", get(download_folder_zip))
|
2025-04-02 01:22:05 +02:00
|
|
|
.with_state(app_state.clone());
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-16 21:51:53 +01:00
|
|
|
// 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()
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/{id}/listing", get(list_folder_listing))
|
2026-02-16 21:51:53 +01:00
|
|
|
.with_state(app_state.clone());
|
|
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
// Create folder operations that use trash (requires full AppState)
|
2026-04-27 22:59:18 +02:00
|
|
|
let folders_ops_router = Router::new().route("/{id}", delete(delete_folder_with_trash));
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
// Merge the routers
|
2026-02-14 01:29:34 +01:00
|
|
|
let folders_router = folders_basic_router
|
|
|
|
|
.merge(folders_ops_router)
|
2026-02-16 21:51:53 +01:00
|
|
|
.merge(folder_zip_router)
|
|
|
|
|
.merge(folder_listing_router);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
// Create file routes for basic operations and trash-enabled delete
|
|
|
|
|
let basic_file_router = Router::new()
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/", get(list_files_query))
|
|
|
|
|
.route("/upload", post(upload_file_with_thumbnails))
|
|
|
|
|
.route("/{id}", get(download_file))
|
2026-03-09 14:34:07 +01:00
|
|
|
.route(
|
|
|
|
|
"/{id}/thumbnail/{size}",
|
2026-04-27 22:59:18 +02:00
|
|
|
get(get_thumbnail).put(upload_thumbnail),
|
2026-03-09 14:34:07 +01:00
|
|
|
)
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/{id}/metadata", get(get_file_metadata))
|
2026-03-17 04:39:18 +08:00
|
|
|
.layer(DefaultBodyLimit::max({
|
|
|
|
|
// Use architecture-appropriate body limit: 10 GB on 64-bit, 1 GB on 32-bit
|
|
|
|
|
#[cfg(target_pointer_width = "64")]
|
|
|
|
|
const FILE_BODY_LIMIT: usize = 10 * 1024 * 1024 * 1024;
|
|
|
|
|
#[cfg(target_pointer_width = "32")]
|
|
|
|
|
const FILE_BODY_LIMIT: usize = 1024 * 1024 * 1024;
|
|
|
|
|
FILE_BODY_LIMIT
|
|
|
|
|
})) // for file uploads
|
2026-02-03 17:59:04 +01:00
|
|
|
.with_state(app_state.clone());
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
// File operations with trash support
|
2025-03-26 18:33:22 +01:00
|
|
|
let file_operations_router = Router::new()
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/{id}", delete(delete_file))
|
|
|
|
|
.route("/{id}/move", put(move_file_simple))
|
|
|
|
|
.route("/{id}/rename", put(rename_file));
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
// Merge the routers
|
|
|
|
|
let files_router = basic_file_router.merge(file_operations_router);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Create routes for batch operations
|
2025-03-19 00:44:27 +01:00
|
|
|
let batch_router = Router::new()
|
2026-02-12 09:41:25 +01:00
|
|
|
// File operations
|
2025-03-19 00:44:27 +01:00
|
|
|
.route("/files/move", post(batch_handler::move_files_batch))
|
|
|
|
|
.route("/files/copy", post(batch_handler::copy_files_batch))
|
|
|
|
|
.route("/files/delete", post(batch_handler::delete_files_batch))
|
|
|
|
|
.route("/files/get", post(batch_handler::get_files_batch))
|
2026-02-12 09:41:25 +01:00
|
|
|
// Folder operations
|
2025-03-19 00:44:27 +01:00
|
|
|
.route("/folders/delete", post(batch_handler::delete_folders_batch))
|
|
|
|
|
.route("/folders/create", post(batch_handler::create_folders_batch))
|
|
|
|
|
.route("/folders/get", post(batch_handler::get_folders_batch))
|
2026-05-11 22:58:33 +02:00
|
|
|
.route("/folders/copy", post(batch_handler::copy_folders_batch))
|
2026-02-15 23:45:11 +01:00
|
|
|
.route("/folders/move", post(batch_handler::move_folders_batch))
|
|
|
|
|
// Trash operations (soft delete)
|
|
|
|
|
.route("/trash", post(batch_handler::trash_batch))
|
|
|
|
|
// Download as ZIP
|
2026-04-28 13:15:08 +02:00
|
|
|
.route("/download", post(batch_handler::download_batch_post))
|
|
|
|
|
// work arround for drag & drop (does not support POST requests)
|
|
|
|
|
.route("/download", get(batch_handler::download_batch_querystring))
|
2025-03-19 00:44:27 +01:00
|
|
|
.with_state(batch_handler_state);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-27 01:13:34 +01:00
|
|
|
// Create search routes if the service is available
|
|
|
|
|
let search_router = if search_service.is_some() {
|
|
|
|
|
Router::new()
|
|
|
|
|
// Simple search with query parameters
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/", get(search_files_get))
|
2026-02-15 23:45:11 +01:00
|
|
|
// Lightweight autocomplete suggestions
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/suggest", get(suggest_files))
|
2025-03-27 01:13:34 +01:00
|
|
|
// Advanced search with full criteria object
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/advanced", post(search_files_post))
|
2025-03-27 01:13:34 +01:00
|
|
|
// Clear search cache
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/cache", delete(clear_search_cache))
|
2025-03-27 01:13:34 +01:00
|
|
|
.with_state(app_state.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Router::new()
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Direct handler implementations for sharing, without depending on ShareHandler
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
// Create routes for shared resources management (requires auth)
|
2025-03-28 12:38:48 +01:00
|
|
|
let share_router = if let Some(share_service) = share_service.clone() {
|
|
|
|
|
use crate::interfaces::api::handlers::share_handler;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-28 12:38:48 +01:00
|
|
|
Router::new()
|
|
|
|
|
.route("/", post(share_handler::create_shared_link))
|
|
|
|
|
.route("/", get(share_handler::get_user_shares))
|
|
|
|
|
.route("/{id}", get(share_handler::get_shared_link))
|
|
|
|
|
.route("/{id}", put(share_handler::update_shared_link))
|
|
|
|
|
.route("/{id}", delete(share_handler::delete_shared_link))
|
|
|
|
|
.with_state(share_service.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Router::new()
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-24 01:05:13 +02:00
|
|
|
// Create routes for ReBAC grants (/api/grants).
|
|
|
|
|
// State is Arc<AppState> so that the new list_shared_with_me handler can
|
|
|
|
|
// access file/folder services. Existing handlers still extract
|
|
|
|
|
// State<Arc<PgAclEngine>> via the FromRef impl in di.rs.
|
2026-05-20 22:56:00 +02:00
|
|
|
let grants_router = {
|
|
|
|
|
use crate::interfaces::api::handlers::grant_handler;
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/", post(grant_handler::create_grant))
|
|
|
|
|
.route("/", get(grant_handler::list_on_resource))
|
|
|
|
|
.route("/{id}", delete(grant_handler::revoke_grant))
|
|
|
|
|
.route("/role", put(grant_handler::set_role))
|
|
|
|
|
.route("/incoming", get(grant_handler::list_incoming))
|
2026-05-24 01:05:13 +02:00
|
|
|
.route(
|
|
|
|
|
"/incoming/resources",
|
|
|
|
|
get(grant_handler::list_shared_with_me),
|
|
|
|
|
)
|
2026-05-20 22:56:00 +02:00
|
|
|
.route("/outgoing", get(grant_handler::list_outgoing))
|
2026-05-24 01:05:13 +02:00
|
|
|
.with_state(app_state.clone())
|
2026-05-20 22:56:00 +02:00
|
|
|
};
|
|
|
|
|
|
2025-03-17 21:28:08 +01:00
|
|
|
// Create a router without the i18n routes
|
2025-04-02 03:43:44 +02:00
|
|
|
// Create routes for favorites if the service is available
|
|
|
|
|
let favorites_router = if let Some(favorites_service) = favorites_service.clone() {
|
2026-05-28 10:08:38 +02:00
|
|
|
#[allow(deprecated)]
|
|
|
|
|
use crate::interfaces::api::handlers::favorites_handler::{
|
|
|
|
|
self, get_favorites, list_favorites_resources,
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-02 03:43:44 +02:00
|
|
|
Router::new()
|
2026-05-28 10:08:38 +02:00
|
|
|
.route("/", get(get_favorites)) // deprecated, kept for compat
|
|
|
|
|
.route("/resources", get(list_favorites_resources)) // new cursor-paginated endpoint
|
2026-02-16 09:17:54 +01:00
|
|
|
.route("/batch", post(favorites_handler::batch_add_favorites))
|
2026-02-14 01:29:34 +01:00
|
|
|
.route(
|
|
|
|
|
"/{item_type}/{item_id}",
|
|
|
|
|
post(favorites_handler::add_favorite),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{item_type}/{item_id}",
|
|
|
|
|
delete(favorites_handler::remove_favorite),
|
|
|
|
|
)
|
2025-04-02 03:43:44 +02:00
|
|
|
.with_state(favorites_service.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Router::new()
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-02 05:08:30 +02:00
|
|
|
// Create routes for recent items if the service is available
|
|
|
|
|
let recent_router = if let Some(recent_service) = recent_service.clone() {
|
2026-05-28 11:50:32 +02:00
|
|
|
#[allow(deprecated)]
|
2025-04-02 05:08:30 +02:00
|
|
|
use crate::interfaces::api::handlers::recent_handler;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-04-02 05:08:30 +02:00
|
|
|
Router::new()
|
|
|
|
|
.route("/", get(recent_handler::get_recent_items))
|
2026-05-28 11:50:32 +02:00
|
|
|
.route("/resources", get(recent_handler::list_recent_resources))
|
2026-02-14 01:29:34 +01:00
|
|
|
.route(
|
|
|
|
|
"/{item_type}/{item_id}",
|
|
|
|
|
post(recent_handler::record_item_access),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{item_type}/{item_id}",
|
|
|
|
|
delete(recent_handler::remove_from_recent),
|
|
|
|
|
)
|
2025-04-02 05:08:30 +02:00
|
|
|
.route("/clear", delete(recent_handler::clear_recent_items))
|
|
|
|
|
.with_state(recent_service.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Router::new()
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-04-27 22:59:18 +02:00
|
|
|
// 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.
|
2026-02-03 17:59:04 +01:00
|
|
|
let chunked_upload_router = Router::new()
|
2026-04-27 22:59:18 +02:00
|
|
|
.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))
|
2026-02-24 15:11:56 +01:00
|
|
|
.with_state(app_state.clone());
|
2026-02-03 17:59:04 +01:00
|
|
|
|
2026-04-27 22:59:18 +02:00
|
|
|
// 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,
|
|
|
|
|
};
|
2026-02-03 17:59:04 +01:00
|
|
|
let dedup_router = Router::new()
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/check/{hash}", get(check_hash))
|
|
|
|
|
.route("/upload", post(upload_with_dedup))
|
|
|
|
|
.route("/stats", get(get_stats))
|
|
|
|
|
.route("/blob/{hash}", get(get_blob))
|
2026-03-05 13:15:34 +01:00
|
|
|
// 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.
|
2026-04-27 22:59:18 +02:00
|
|
|
.route("/recalculate", post(recalculate_stats))
|
2026-02-03 17:59:04 +01:00
|
|
|
.with_state(app_state.clone());
|
2025-04-02 03:43:44 +02:00
|
|
|
|
2025-03-17 21:28:08 +01:00
|
|
|
let mut router = Router::new()
|
|
|
|
|
.nest("/folders", folders_router)
|
2025-03-19 00:44:27 +01:00
|
|
|
.nest("/files", files_router)
|
2026-02-03 17:59:04 +01:00
|
|
|
.nest("/uploads", chunked_upload_router)
|
|
|
|
|
.nest("/dedup", dedup_router)
|
2025-03-27 01:13:34 +01:00
|
|
|
.nest("/batch", batch_router)
|
2025-03-28 12:38:48 +01:00
|
|
|
.nest("/search", search_router)
|
|
|
|
|
.nest("/shares", share_router)
|
2026-05-20 22:56:00 +02:00
|
|
|
.nest("/grants", grants_router)
|
2025-04-02 03:43:44 +02:00
|
|
|
.nest("/favorites", favorites_router)
|
2026-02-14 01:29:34 +01:00
|
|
|
.nest("/recent", recent_router);
|
|
|
|
|
|
2026-03-05 13:40:02 -05:00
|
|
|
// Photos timeline endpoint — lists all image/video files sorted by capture date
|
|
|
|
|
{
|
|
|
|
|
use crate::interfaces::api::handlers::photos_handler;
|
|
|
|
|
|
|
|
|
|
let photos_router = Router::new()
|
|
|
|
|
.route("/", get(photos_handler::list_photos))
|
|
|
|
|
.with_state(app_state.clone());
|
|
|
|
|
|
|
|
|
|
router = router.nest("/photos", photos_router);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
// Re-enable trash routes to make the trash view work
|
2025-03-28 06:15:09 +01:00
|
|
|
if let Some(_trash_service_ref) = trash_service.clone() {
|
2025-03-26 18:33:22 +01:00
|
|
|
tracing::info!("Setting up trash routes for trash view");
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
let trash_router = Router::new()
|
2026-02-08 13:40:23 +01:00
|
|
|
.route("/", get(trash_handler::get_trash_items))
|
|
|
|
|
.route("/files/{id}", delete(trash_handler::move_file_to_trash))
|
|
|
|
|
.route("/folders/{id}", delete(trash_handler::move_folder_to_trash))
|
|
|
|
|
.route("/{id}/restore", post(trash_handler::restore_from_trash))
|
|
|
|
|
.route("/{id}", delete(trash_handler::delete_permanently))
|
|
|
|
|
.route("/empty", delete(trash_handler::empty_trash))
|
2025-03-26 18:33:22 +01:00
|
|
|
.with_state(app_state.clone());
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
router = router.nest("/trash", trash_router);
|
|
|
|
|
} else {
|
|
|
|
|
tracing::warn!("Trash service not available - trash view will not work");
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-04-08 15:14:03 +03:00
|
|
|
// Music/Playlist routes
|
|
|
|
|
if let Some(ref music_svc) = app_state.music_service {
|
|
|
|
|
use crate::interfaces::api::handlers::music_handler;
|
|
|
|
|
|
|
|
|
|
let music_router = Router::new()
|
|
|
|
|
.route("/", post(music_handler::create_playlist))
|
|
|
|
|
.route("/", get(music_handler::list_playlists))
|
|
|
|
|
.route("/{playlist_id}", get(music_handler::get_playlist))
|
|
|
|
|
.route("/{playlist_id}", put(music_handler::update_playlist))
|
|
|
|
|
.route(
|
|
|
|
|
"/{playlist_id}",
|
|
|
|
|
axum::routing::delete(music_handler::delete_playlist),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{playlist_id}/tracks",
|
|
|
|
|
get(music_handler::list_playlist_tracks),
|
|
|
|
|
)
|
|
|
|
|
.route("/{playlist_id}/tracks", post(music_handler::add_tracks))
|
|
|
|
|
.route(
|
|
|
|
|
"/{playlist_id}/tracks/{file_id}",
|
|
|
|
|
axum::routing::delete(music_handler::remove_track),
|
|
|
|
|
)
|
|
|
|
|
.route("/{playlist_id}/reorder", put(music_handler::reorder_tracks))
|
|
|
|
|
.route("/{playlist_id}/share", post(music_handler::share_playlist))
|
|
|
|
|
.route(
|
|
|
|
|
"/{playlist_id}/share/{user_id}",
|
|
|
|
|
axum::routing::delete(music_handler::remove_share),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{playlist_id}/shares",
|
|
|
|
|
get(music_handler::get_playlist_shares),
|
|
|
|
|
)
|
2026-04-11 10:59:01 +02:00
|
|
|
.route(
|
|
|
|
|
"/audio-metadata/{file_id}",
|
|
|
|
|
get(music_handler::get_audio_metadata),
|
|
|
|
|
)
|
2026-04-08 15:14:03 +03:00
|
|
|
.with_state(music_svc.clone());
|
|
|
|
|
|
|
|
|
|
router = router.nest("/playlists", music_router);
|
|
|
|
|
tracing::info!("Music routes initialized");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 19:18:33 +02:00
|
|
|
// REST browse API for CardDAV contacts, groups, and OxiCloud users.
|
|
|
|
|
// Write operations and protocol sync remain on the /carddav endpoint.
|
|
|
|
|
if let Some(contact_service) = app_state.contact_use_case.clone() {
|
|
|
|
|
use crate::interfaces::api::handlers::contacts_handler::{self, ContactsApiState};
|
|
|
|
|
|
|
|
|
|
let auth_svc = app_state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|s| s.auth_application_service.clone());
|
|
|
|
|
|
|
|
|
|
let contacts_state = ContactsApiState {
|
|
|
|
|
contact_service,
|
|
|
|
|
auth_service: auth_svc,
|
2026-05-10 22:12:11 +02:00
|
|
|
expose_system_users: app_state.core.config.features.expose_system_users,
|
2026-05-09 19:18:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let contacts_router = Router::new()
|
|
|
|
|
.route(
|
|
|
|
|
"/",
|
|
|
|
|
get(contacts_handler::list_address_books)
|
|
|
|
|
.post(contacts_handler::create_address_book),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{book_id}",
|
|
|
|
|
put(contacts_handler::update_address_book)
|
|
|
|
|
.delete(contacts_handler::delete_address_book),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{book_id}/contacts",
|
|
|
|
|
get(contacts_handler::list_contacts).post(contacts_handler::create_contact),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{book_id}/contacts/{contact_id}",
|
|
|
|
|
get(contacts_handler::get_contact)
|
|
|
|
|
.put(contacts_handler::update_contact)
|
|
|
|
|
.delete(contacts_handler::delete_contact),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{book_id}/groups",
|
|
|
|
|
get(contacts_handler::list_groups).post(contacts_handler::create_group),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{book_id}/groups/{group_id}",
|
|
|
|
|
get(contacts_handler::get_group)
|
|
|
|
|
.put(contacts_handler::update_group)
|
|
|
|
|
.delete(contacts_handler::delete_group),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{book_id}/groups/{group_id}/contacts",
|
|
|
|
|
get(contacts_handler::list_contacts_in_group)
|
|
|
|
|
.post(contacts_handler::add_contact_to_group),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/{book_id}/groups/{group_id}/contacts/{contact_id}",
|
|
|
|
|
delete(contacts_handler::remove_contact_from_group),
|
|
|
|
|
)
|
|
|
|
|
.with_state(contacts_state);
|
|
|
|
|
|
|
|
|
|
router = router.nest("/address-books", contacts_router);
|
|
|
|
|
tracing::info!("Contacts REST API routes initialized");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:26:28 +01:00
|
|
|
// NOTE: WebDAV routes are mounted at top-level (/webdav) in main.rs
|
|
|
|
|
// for client compatibility, NOT under /api.
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-10 18:46:59 +01:00
|
|
|
// NOTE: CalDAV and CardDAV routes are mounted at top-level (/caldav, /carddav)
|
|
|
|
|
// in main.rs for protocol compliance, NOT under /api.
|
2025-03-26 18:33:22 +01:00
|
|
|
|
2026-02-11 00:15:26 +01:00
|
|
|
// Admin settings routes (protected by admin_guard inside the handler)
|
2026-02-14 01:29:34 +01:00
|
|
|
let admin_router = admin_handler::admin_routes().with_state(app_state.clone());
|
2026-02-11 00:15:26 +01:00
|
|
|
router = router.nest("/admin", admin_router);
|
|
|
|
|
|
2026-02-22 14:12:53 +01:00
|
|
|
// Transparent compression (gzip + brotli) for all API responses.
|
|
|
|
|
// tower-http negotiates via Accept-Encoding and skips already-compressed
|
|
|
|
|
// content types automatically. No manual compression in handlers.
|
2025-03-17 21:28:08 +01:00
|
|
|
router
|
2026-02-17 22:58:56 -08:00
|
|
|
.layer(CompressionLayer::new().br(true).gzip(true))
|
2025-03-17 21:28:08 +01:00
|
|
|
.layer(TraceLayer::new_for_http())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|