pinakes-server: update remaining route imports and handlers

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I67206fd813d514f8903041eea0a4cd266a6a6964
This commit is contained in:
raf 2026-03-08 00:42:20 +03:00
commit eb6c0a3577
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
20 changed files with 169 additions and 87 deletions

View file

@ -4,7 +4,11 @@ use axum::{
};
use pinakes_core::model::Pagination;
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{AuditEntryResponse, PaginationParams},
error::ApiError,
state::AppState,
};
pub async fn list_audit(
State(state): State<AppState>,

View file

@ -5,7 +5,16 @@ use axum::{
use pinakes_core::model::{CollectionKind, MediaId};
use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{
AddMemberRequest,
CollectionResponse,
CreateCollectionRequest,
MediaResponse,
},
error::ApiError,
state::AppState,
};
pub async fn create_collection(
State(state): State<AppState>,

View file

@ -1,6 +1,18 @@
use axum::{Json, extract::State};
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{
ConfigResponse,
RootDirRequest,
ScanningConfigResponse,
ServerConfigResponse,
UiConfigResponse,
UpdateScanningRequest,
UpdateUiConfigRequest,
},
error::ApiError,
state::AppState,
};
pub async fn get_config(
State(state): State<AppState>,
@ -15,18 +27,11 @@ pub async fn get_config(
let config_writable = match &state.config_path {
Some(path) => {
if path.exists() {
std::fs::metadata(path)
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
} else {
path
.parent()
.map(|parent| {
std::fs::metadata(parent)
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
})
.unwrap_or(false)
path.parent().is_some_and(|parent| {
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
})
}
},
None => false,
@ -128,18 +133,11 @@ pub async fn update_scanning_config(
let config_writable = match &state.config_path {
Some(path) => {
if path.exists() {
std::fs::metadata(path)
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
} else {
path
.parent()
.map(|parent| {
std::fs::metadata(parent)
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
})
.unwrap_or(false)
path.parent().is_some_and(|parent| {
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
})
}
},
None => false,

View file

@ -5,7 +5,11 @@ use axum::{
use pinakes_core::model::MediaId;
use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{BatchDeleteRequest, ExternalMetadataResponse},
error::ApiError,
state::AppState,
};
pub async fn trigger_enrichment(
State(state): State<AppState>,

View file

@ -65,7 +65,7 @@ pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
Err(e) => {
response.status = "degraded".to_string();
DatabaseHealth {
status: format!("error: {}", e),
status: format!("error: {e}"),
latency_ms: db_start.elapsed().as_millis() as u64,
media_count: None,
}
@ -168,7 +168,7 @@ pub async fn health_detailed(
let db_start = Instant::now();
let (db_status, media_count) = match state.storage.count_media().await {
Ok(count) => ("ok".to_string(), Some(count)),
Err(e) => (format!("error: {}", e), None),
Err(e) => (format!("error: {e}"), None),
};
let db_latency = db_start.elapsed().as_millis() as u64;

View file

@ -56,7 +56,7 @@ pub async fn generate_all_thumbnails(
State(state): State<AppState>,
body: Option<Json<GenerateThumbnailsRequest>>,
) -> Result<Json<serde_json::Value>, ApiError> {
let only_missing = body.map(|b| b.only_missing).unwrap_or(false);
let only_missing = body.is_some_and(|b| b.only_missing);
let media_ids = state
.storage
.list_media_ids_for_thumbnails(only_missing)

View file

@ -175,7 +175,7 @@ pub struct GraphQuery {
pub depth: u32,
}
fn default_depth() -> u32 {
const fn default_depth() -> u32 {
2
}
@ -280,7 +280,7 @@ pub async fn reindex_links(
// Read the file content
let content = tokio::fs::read_to_string(&media.path)
.await
.map_err(|e| ApiError::internal(format!("Failed to read file: {}", e)))?;
.map_err(|e| ApiError::internal(format!("Failed to read file: {e}")))?;
// Extract links
let links = pinakes_core::links::extract_links(media_id, &content);

View file

@ -33,7 +33,7 @@ pub struct TimelineQuery {
pub limit: u64,
}
fn default_timeline_limit() -> u64 {
const fn default_timeline_limit() -> u64 {
10000
}

View file

@ -5,7 +5,19 @@ use axum::{
use pinakes_core::{model::MediaId, playlists::Playlist, users::UserId};
use uuid::Uuid;
use crate::{auth::resolve_user_id, dto::*, error::ApiError, state::AppState};
use crate::{
auth::resolve_user_id,
dto::{
CreatePlaylistRequest,
MediaResponse,
PlaylistItemRequest,
PlaylistResponse,
ReorderPlaylistRequest,
UpdatePlaylistRequest,
},
error::ApiError,
state::AppState,
};
/// Check whether a user has access to a playlist.
///
@ -138,12 +150,11 @@ pub async fn add_item(
) -> Result<Json<serde_json::Value>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, true).await?;
let position = match req.position {
Some(p) => p,
None => {
let items = state.storage.get_playlist_items(id).await?;
items.len() as i32
},
let position = if let Some(p) = req.position {
p
} else {
let items = state.storage.get_playlist_items(id).await?;
items.len() as i32
};
state
.storage

View file

@ -3,7 +3,11 @@ use axum::{
extract::{Path, State},
};
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{InstallPluginRequest, PluginResponse, TogglePluginRequest},
error::ApiError,
state::AppState,
};
/// List all installed plugins
pub async fn list_plugins(
@ -37,8 +41,7 @@ pub async fn get_plugin(
let plugin = plugin_manager.get_plugin(&id).await.ok_or_else(|| {
ApiError(pinakes_core::error::PinakesError::NotFound(format!(
"Plugin not found: {}",
id
"Plugin not found: {id}"
)))
})?;
@ -63,7 +66,7 @@ pub async fn install_plugin(
.await
.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to install plugin: {}", e),
format!("Failed to install plugin: {e}"),
))
})?;
@ -91,7 +94,7 @@ pub async fn uninstall_plugin(
plugin_manager.uninstall_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to uninstall plugin: {}", e),
format!("Failed to uninstall plugin: {e}"),
))
})?;
@ -113,13 +116,13 @@ pub async fn toggle_plugin(
if req.enabled {
plugin_manager.enable_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to enable plugin: {}", e),
format!("Failed to enable plugin: {e}"),
))
})?;
} else {
plugin_manager.disable_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to disable plugin: {}", e),
format!("Failed to disable plugin: {e}"),
))
})?;
}
@ -143,7 +146,7 @@ pub async fn reload_plugin(
plugin_manager.reload_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to reload plugin: {}", e),
format!("Failed to reload plugin: {e}"),
))
})?;

View file

@ -1,6 +1,10 @@
use axum::{Json, extract::State};
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{ScanJobResponse, ScanRequest, ScanStatusResponse},
error::ApiError,
state::AppState,
};
/// Trigger a scan as a background job. Returns the job ID immediately.
pub async fn trigger_scan(

View file

@ -7,7 +7,11 @@ use pinakes_core::{
search::{SearchRequest, SortOrder, parse_search_query},
};
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{MediaResponse, SearchParams, SearchRequestBody, SearchResponse},
error::ApiError,
state::AppState,
};
fn resolve_sort(sort: Option<&str>) -> SortOrder {
match sort {

View file

@ -6,7 +6,21 @@ use pinakes_core::model::{MediaId, Pagination};
use serde::Deserialize;
use uuid::Uuid;
use crate::{auth::resolve_user_id, dto::*, error::ApiError, state::AppState};
use crate::{
auth::resolve_user_id,
dto::{
CommentResponse,
CreateCommentRequest,
CreateRatingRequest,
CreateShareLinkRequest,
FavoriteRequest,
MediaResponse,
RatingResponse,
ShareLinkResponse,
},
error::ApiError,
state::AppState,
};
#[derive(Deserialize)]
pub struct ShareLinkQuery {
@ -133,8 +147,7 @@ pub async fn create_share_link(
{
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(format!(
"expires_in_hours cannot exceed {}",
MAX_EXPIRY_HOURS
"expires_in_hours cannot exceed {MAX_EXPIRY_HOURS}"
)),
));
}

View file

@ -98,7 +98,7 @@ pub async fn hls_variant_playlist(
);
for i in 0..num_segments.max(1) {
let seg_dur = if i == num_segments.saturating_sub(1) && duration > 0.0 {
duration - (i as f64 * segment_duration)
(i as f64).mul_add(-segment_duration, duration)
} else {
segment_duration
};
@ -143,7 +143,7 @@ pub async fn hls_segment(
if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e),
format!("failed to read segment: {e}"),
))
})?;
@ -246,7 +246,7 @@ pub async fn dash_segment(
if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e),
format!("failed to read segment: {e}"),
))
})?;

View file

@ -8,7 +8,11 @@ use pinakes_core::{
};
use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{AddSubtitleRequest, SubtitleResponse, UpdateSubtitleOffsetRequest},
error::ApiError,
state::AppState,
};
pub async fn list_subtitles(
State(state): State<AppState>,

View file

@ -5,7 +5,11 @@ use axum::{
use pinakes_core::model::MediaId;
use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{CreateTagRequest, TagMediaRequest, TagResponse},
error::ApiError,
state::AppState,
};
pub async fn create_tag(
State(state): State<AppState>,

View file

@ -5,7 +5,11 @@ use axum::{
use pinakes_core::model::MediaId;
use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{CreateTranscodeRequest, PaginationParams, TranscodeSessionResponse},
error::ApiError,
state::AppState,
};
pub async fn start_transcode(
State(state): State<AppState>,

View file

@ -4,7 +4,16 @@ use axum::{
};
use pinakes_core::users::{CreateUserRequest, UpdateUserRequest, UserId};
use crate::{dto::*, error::ApiError, state::AppState};
use crate::{
dto::{
GrantLibraryAccessRequest,
RevokeLibraryAccessRequest,
UserLibraryResponse,
UserResponse,
},
error::ApiError,
state::AppState,
};
/// List all users (admin only)
pub async fn list_users(
@ -175,7 +184,7 @@ pub async fn grant_library_access(
/// Revoke library access from a user (admin only)
///
/// Uses a JSON body instead of a path parameter because root_path may contain
/// Uses a JSON body instead of a path parameter because `root_path` may contain
/// slashes that conflict with URL routing.
pub async fn revoke_library_access(
State(state): State<AppState>,

View file

@ -19,6 +19,7 @@ use pinakes_core::{
ManagedStorageConfig,
PhotoConfig,
PluginsConfig,
RateLimitConfig,
ScanningConfig,
ServerConfig,
SharingConfig,
@ -41,19 +42,19 @@ use pinakes_core::{
use tokio::sync::RwLock;
use tower::ServiceExt;
/// Fake socket address for tests (governor needs ConnectInfo<SocketAddr>)
/// Fake socket address for tests (governor needs `ConnectInfo`<SocketAddr>)
fn test_addr() -> ConnectInfo<SocketAddr> {
ConnectInfo("127.0.0.1:9999".parse().unwrap())
}
/// Build a GET request with ConnectInfo for rate limiter compatibility
/// Build a GET request with `ConnectInfo` for rate limiter compatibility
fn get(uri: &str) -> Request<Body> {
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
req.extensions_mut().insert(test_addr());
req
}
/// Build a POST request with ConnectInfo
/// Build a POST request with `ConnectInfo`
fn post_json(uri: &str, body: &str) -> Request<Body> {
let mut req = Request::builder()
.method("POST")
@ -69,7 +70,7 @@ fn post_json(uri: &str, body: &str) -> Request<Body> {
fn get_authed(uri: &str, token: &str) -> Request<Body> {
let mut req = Request::builder()
.uri(uri)
.header("authorization", format!("Bearer {}", token))
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
req.extensions_mut().insert(test_addr());
@ -82,7 +83,7 @@ fn post_json_authed(uri: &str, body: &str, token: &str) -> Request<Body> {
.method("POST")
.uri(uri)
.header("content-type", "application/json")
.header("authorization", format!("Bearer {}", token))
.header("authorization", format!("Bearer {token}"))
.body(Body::from(body.to_string()))
.unwrap();
req.extensions_mut().insert(test_addr());
@ -94,7 +95,7 @@ fn delete_authed(uri: &str, token: &str) -> Request<Body> {
let mut req = Request::builder()
.method("DELETE")
.uri(uri)
.header("authorization", format!("Bearer {}", token))
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
req.extensions_mut().insert(test_addr());
@ -107,7 +108,7 @@ fn patch_json_authed(uri: &str, body: &str, token: &str) -> Request<Body> {
.method("PATCH")
.uri(uri)
.header("content-type", "application/json")
.header("authorization", format!("Bearer {}", token))
.header("authorization", format!("Bearer {token}"))
.body(Body::from(body.to_string()))
.unwrap();
req.extensions_mut().insert(test_addr());
@ -136,7 +137,10 @@ fn default_config() -> Config {
api_key: None,
tls: TlsConfig::default(),
authentication_disabled: true,
cors_enabled: false,
cors_origins: vec![],
},
rate_limits: RateLimitConfig::default(),
ui: UiConfig::default(),
accounts: AccountsConfig::default(),
jobs: JobsConfig::default(),
@ -164,7 +168,7 @@ async fn setup_app() -> axum::Router {
let config = default_config();
let job_queue =
JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
JobQueue::new(1, 0, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
let config = Arc::new(RwLock::new(config));
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
job_queue.clone(),
@ -186,9 +190,10 @@ async fn setup_app() -> axum::Router {
managed_storage: None,
chunked_upload_manager: None,
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
webhook_dispatcher: None,
};
pinakes_server::app::create_router(state)
pinakes_server::app::create_router(state, &RateLimitConfig::default())
}
/// Hash a password for test user accounts
@ -197,7 +202,7 @@ fn hash_password(password: &str) -> String {
}
/// Set up an app with accounts enabled and three pre-seeded users.
/// Returns (Router, admin_token, editor_token, viewer_token).
/// Returns (Router, `admin_token`, `editor_token`, `viewer_token`).
async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
let backend = SqliteBackend::in_memory().expect("in-memory SQLite");
backend.run_migrations().await.expect("migrations");
@ -239,7 +244,7 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
];
let job_queue =
JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
JobQueue::new(1, 0, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
let config = Arc::new(RwLock::new(config));
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
job_queue.clone(),
@ -261,9 +266,11 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
managed_storage: None,
chunked_upload_manager: None,
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
webhook_dispatcher: None,
};
let app = pinakes_server::app::create_router(state);
let app =
pinakes_server::app::create_router(state, &RateLimitConfig::default());
// Login each user to get tokens
let admin_token = login_user(app.clone(), "admin", "adminpass").await;
@ -278,8 +285,7 @@ async fn login_user(
username: &str,
password: &str,
) -> String {
let body =
format!(r#"{{"username":"{}","password":"{}"}}"#, username, password);
let body = format!(r#"{{"username":"{username}","password":"{password}"}}"#);
let response = app
.oneshot(post_json("/api/v1/auth/login", &body))
.await
@ -287,8 +293,7 @@ async fn login_user(
assert_eq!(
response.status(),
StatusCode::OK,
"login failed for user {}",
username
"login failed for user {username}"
);
let body = response.into_body().collect().await.unwrap().to_bytes();
let result: serde_json::Value = serde_json::from_slice(&body).unwrap();
@ -449,7 +454,7 @@ async fn test_user_management_crud() {
// Get specific user
let response = app
.clone()
.oneshot(get(&format!("/api/v1/users/{}", user_id)))
.oneshot(get(&format!("/api/v1/users/{user_id}")))
.await
.unwrap();
@ -462,7 +467,7 @@ async fn test_user_management_crud() {
// Delete user
let mut req = Request::builder()
.method("DELETE")
.uri(&format!("/api/v1/users/{}", user_id))
.uri(format!("/api/v1/users/{user_id}"))
.body(Body::empty())
.unwrap();
req.extensions_mut().insert(test_addr());
@ -472,7 +477,7 @@ async fn test_user_management_crud() {
// Verify user is deleted
let response = app
.oneshot(get(&format!("/api/v1/users/{}", user_id)))
.oneshot(get(&format!("/api/v1/users/{user_id}")))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
@ -796,7 +801,7 @@ async fn test_playlist_crud() {
let response = app
.clone()
.oneshot(get_authed(
&format!("/api/v1/playlists/{}", playlist_id),
&format!("/api/v1/playlists/{playlist_id}"),
&editor_token,
))
.await
@ -807,7 +812,7 @@ async fn test_playlist_crud() {
let response = app
.clone()
.oneshot(patch_json_authed(
&format!("/api/v1/playlists/{}", playlist_id),
&format!("/api/v1/playlists/{playlist_id}"),
r#"{"name":"Updated Playlist","description":"A test description"}"#,
&editor_token,
))
@ -821,7 +826,7 @@ async fn test_playlist_crud() {
let response = app
.clone()
.oneshot(delete_authed(
&format!("/api/v1/playlists/{}", playlist_id),
&format!("/api/v1/playlists/{playlist_id}"),
&editor_token,
))
.await
@ -972,7 +977,7 @@ async fn test_oversized_comment() {
let (app, _, editor_token, _) = setup_app_with_auth().await;
let long_text: String = "x".repeat(10_001);
let body = format!(r#"{{"text":"{}"}}"#, long_text);
let body = format!(r#"{{"text":"{long_text}"}}"#);
let response = app
.oneshot(post_json_authed(
"/api/v1/media/00000000-0000-0000-0000-000000000000/comments",

View file

@ -19,6 +19,7 @@ use pinakes_core::{
ManagedStorageConfig,
PhotoConfig,
PluginsConfig,
RateLimitConfig,
ScanningConfig,
ServerConfig,
SharingConfig,
@ -40,12 +41,12 @@ use pinakes_core::{
use tokio::sync::RwLock;
use tower::ServiceExt;
/// Fake socket address for tests (governor needs ConnectInfo<SocketAddr>)
/// Fake socket address for tests (governor needs `ConnectInfo`<SocketAddr>)
fn test_addr() -> ConnectInfo<SocketAddr> {
ConnectInfo("127.0.0.1:9999".parse().unwrap())
}
/// Build a GET request with ConnectInfo for rate limiter compatibility
/// Build a GET request with `ConnectInfo` for rate limiter compatibility
fn get(uri: &str) -> Request<Body> {
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
req.extensions_mut().insert(test_addr());
@ -103,7 +104,10 @@ async fn setup_app_with_plugins()
api_key: None,
tls: TlsConfig::default(),
authentication_disabled: true,
cors_enabled: false,
cors_origins: vec![],
},
rate_limits: RateLimitConfig::default(),
ui: UiConfig::default(),
accounts: AccountsConfig::default(),
jobs: JobsConfig::default(),
@ -123,7 +127,7 @@ async fn setup_app_with_plugins()
};
let job_queue =
JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
JobQueue::new(1, 0, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
let config = Arc::new(RwLock::new(config));
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
job_queue.clone(),
@ -145,9 +149,11 @@ async fn setup_app_with_plugins()
managed_storage: None,
chunked_upload_manager: None,
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
webhook_dispatcher: None,
};
let router = pinakes_server::app::create_router(state);
let router =
pinakes_server::app::create_router(state, &RateLimitConfig::default());
(router, plugin_manager, temp_dir)
}