pinakes-server: update remaining route imports and handlers
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I67206fd813d514f8903041eea0a4cd266a6a6964
This commit is contained in:
parent
2b2c1830a1
commit
eb6c0a3577
20 changed files with 169 additions and 87 deletions
|
|
@ -4,7 +4,11 @@ use axum::{
|
||||||
};
|
};
|
||||||
use pinakes_core::model::Pagination;
|
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(
|
pub async fn list_audit(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,16 @@ use axum::{
|
||||||
use pinakes_core::model::{CollectionKind, MediaId};
|
use pinakes_core::model::{CollectionKind, MediaId};
|
||||||
use uuid::Uuid;
|
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(
|
pub async fn create_collection(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
use axum::{Json, extract::State};
|
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(
|
pub async fn get_config(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -15,18 +27,11 @@ pub async fn get_config(
|
||||||
let config_writable = match &state.config_path {
|
let config_writable = match &state.config_path {
|
||||||
Some(path) => {
|
Some(path) => {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
std::fs::metadata(path)
|
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
|
||||||
.map(|m| !m.permissions().readonly())
|
|
||||||
.unwrap_or(false)
|
|
||||||
} else {
|
} else {
|
||||||
path
|
path.parent().is_some_and(|parent| {
|
||||||
.parent()
|
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
|
||||||
.map(|parent| {
|
})
|
||||||
std::fs::metadata(parent)
|
|
||||||
.map(|m| !m.permissions().readonly())
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => false,
|
None => false,
|
||||||
|
|
@ -128,18 +133,11 @@ pub async fn update_scanning_config(
|
||||||
let config_writable = match &state.config_path {
|
let config_writable = match &state.config_path {
|
||||||
Some(path) => {
|
Some(path) => {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
std::fs::metadata(path)
|
std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
|
||||||
.map(|m| !m.permissions().readonly())
|
|
||||||
.unwrap_or(false)
|
|
||||||
} else {
|
} else {
|
||||||
path
|
path.parent().is_some_and(|parent| {
|
||||||
.parent()
|
std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
|
||||||
.map(|parent| {
|
})
|
||||||
std::fs::metadata(parent)
|
|
||||||
.map(|m| !m.permissions().readonly())
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => false,
|
None => false,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ use axum::{
|
||||||
use pinakes_core::model::MediaId;
|
use pinakes_core::model::MediaId;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{dto::*, error::ApiError, state::AppState};
|
use crate::{
|
||||||
|
dto::{BatchDeleteRequest, ExternalMetadataResponse},
|
||||||
|
error::ApiError,
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn trigger_enrichment(
|
pub async fn trigger_enrichment(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
response.status = "degraded".to_string();
|
response.status = "degraded".to_string();
|
||||||
DatabaseHealth {
|
DatabaseHealth {
|
||||||
status: format!("error: {}", e),
|
status: format!("error: {e}"),
|
||||||
latency_ms: db_start.elapsed().as_millis() as u64,
|
latency_ms: db_start.elapsed().as_millis() as u64,
|
||||||
media_count: None,
|
media_count: None,
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +168,7 @@ pub async fn health_detailed(
|
||||||
let db_start = Instant::now();
|
let db_start = Instant::now();
|
||||||
let (db_status, media_count) = match state.storage.count_media().await {
|
let (db_status, media_count) = match state.storage.count_media().await {
|
||||||
Ok(count) => ("ok".to_string(), Some(count)),
|
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;
|
let db_latency = db_start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ pub async fn generate_all_thumbnails(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
body: Option<Json<GenerateThumbnailsRequest>>,
|
body: Option<Json<GenerateThumbnailsRequest>>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> 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
|
let media_ids = state
|
||||||
.storage
|
.storage
|
||||||
.list_media_ids_for_thumbnails(only_missing)
|
.list_media_ids_for_thumbnails(only_missing)
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ pub struct GraphQuery {
|
||||||
pub depth: u32,
|
pub depth: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_depth() -> u32 {
|
const fn default_depth() -> u32 {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +280,7 @@ pub async fn reindex_links(
|
||||||
// Read the file content
|
// Read the file content
|
||||||
let content = tokio::fs::read_to_string(&media.path)
|
let content = tokio::fs::read_to_string(&media.path)
|
||||||
.await
|
.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
|
// Extract links
|
||||||
let links = pinakes_core::links::extract_links(media_id, &content);
|
let links = pinakes_core::links::extract_links(media_id, &content);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ pub struct TimelineQuery {
|
||||||
pub limit: u64,
|
pub limit: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_timeline_limit() -> u64 {
|
const fn default_timeline_limit() -> u64 {
|
||||||
10000
|
10000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,19 @@ use axum::{
|
||||||
use pinakes_core::{model::MediaId, playlists::Playlist, users::UserId};
|
use pinakes_core::{model::MediaId, playlists::Playlist, users::UserId};
|
||||||
use uuid::Uuid;
|
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.
|
/// Check whether a user has access to a playlist.
|
||||||
///
|
///
|
||||||
|
|
@ -138,12 +150,11 @@ pub async fn add_item(
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||||
check_playlist_access(&state.storage, id, user_id, true).await?;
|
check_playlist_access(&state.storage, id, user_id, true).await?;
|
||||||
let position = match req.position {
|
let position = if let Some(p) = req.position {
|
||||||
Some(p) => p,
|
p
|
||||||
None => {
|
} else {
|
||||||
let items = state.storage.get_playlist_items(id).await?;
|
let items = state.storage.get_playlist_items(id).await?;
|
||||||
items.len() as i32
|
items.len() as i32
|
||||||
},
|
|
||||||
};
|
};
|
||||||
state
|
state
|
||||||
.storage
|
.storage
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{dto::*, error::ApiError, state::AppState};
|
use crate::{
|
||||||
|
dto::{InstallPluginRequest, PluginResponse, TogglePluginRequest},
|
||||||
|
error::ApiError,
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
/// List all installed plugins
|
/// List all installed plugins
|
||||||
pub async fn list_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(|| {
|
let plugin = plugin_manager.get_plugin(&id).await.ok_or_else(|| {
|
||||||
ApiError(pinakes_core::error::PinakesError::NotFound(format!(
|
ApiError(pinakes_core::error::PinakesError::NotFound(format!(
|
||||||
"Plugin not found: {}",
|
"Plugin not found: {id}"
|
||||||
id
|
|
||||||
)))
|
)))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -63,7 +66,7 @@ pub async fn install_plugin(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
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| {
|
plugin_manager.uninstall_plugin(&id).await.map_err(|e| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
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 {
|
if req.enabled {
|
||||||
plugin_manager.enable_plugin(&id).await.map_err(|e| {
|
plugin_manager.enable_plugin(&id).await.map_err(|e| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||||
format!("Failed to enable plugin: {}", e),
|
format!("Failed to enable plugin: {e}"),
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
} else {
|
} else {
|
||||||
plugin_manager.disable_plugin(&id).await.map_err(|e| {
|
plugin_manager.disable_plugin(&id).await.map_err(|e| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
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| {
|
plugin_manager.reload_plugin(&id).await.map_err(|e| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||||
format!("Failed to reload plugin: {}", e),
|
format!("Failed to reload plugin: {e}"),
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
use axum::{Json, extract::State};
|
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.
|
/// Trigger a scan as a background job. Returns the job ID immediately.
|
||||||
pub async fn trigger_scan(
|
pub async fn trigger_scan(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ use pinakes_core::{
|
||||||
search::{SearchRequest, SortOrder, parse_search_query},
|
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 {
|
fn resolve_sort(sort: Option<&str>) -> SortOrder {
|
||||||
match sort {
|
match sort {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,21 @@ use pinakes_core::model::{MediaId, Pagination};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
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)]
|
#[derive(Deserialize)]
|
||||||
pub struct ShareLinkQuery {
|
pub struct ShareLinkQuery {
|
||||||
|
|
@ -133,8 +147,7 @@ pub async fn create_share_link(
|
||||||
{
|
{
|
||||||
return Err(ApiError(
|
return Err(ApiError(
|
||||||
pinakes_core::error::PinakesError::InvalidOperation(format!(
|
pinakes_core::error::PinakesError::InvalidOperation(format!(
|
||||||
"expires_in_hours cannot exceed {}",
|
"expires_in_hours cannot exceed {MAX_EXPIRY_HOURS}"
|
||||||
MAX_EXPIRY_HOURS
|
|
||||||
)),
|
)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ pub async fn hls_variant_playlist(
|
||||||
);
|
);
|
||||||
for i in 0..num_segments.max(1) {
|
for i in 0..num_segments.max(1) {
|
||||||
let seg_dur = if i == num_segments.saturating_sub(1) && duration > 0.0 {
|
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 {
|
} else {
|
||||||
segment_duration
|
segment_duration
|
||||||
};
|
};
|
||||||
|
|
@ -143,7 +143,7 @@ pub async fn hls_segment(
|
||||||
if segment_path.exists() {
|
if segment_path.exists() {
|
||||||
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
|
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
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() {
|
if segment_path.exists() {
|
||||||
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
|
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||||
format!("failed to read segment: {}", e),
|
format!("failed to read segment: {e}"),
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@ use pinakes_core::{
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
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(
|
pub async fn list_subtitles(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ use axum::{
|
||||||
use pinakes_core::model::MediaId;
|
use pinakes_core::model::MediaId;
|
||||||
use uuid::Uuid;
|
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(
|
pub async fn create_tag(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ use axum::{
|
||||||
use pinakes_core::model::MediaId;
|
use pinakes_core::model::MediaId;
|
||||||
use uuid::Uuid;
|
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(
|
pub async fn start_transcode(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,16 @@ use axum::{
|
||||||
};
|
};
|
||||||
use pinakes_core::users::{CreateUserRequest, UpdateUserRequest, UserId};
|
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)
|
/// List all users (admin only)
|
||||||
pub async fn list_users(
|
pub async fn list_users(
|
||||||
|
|
@ -175,7 +184,7 @@ pub async fn grant_library_access(
|
||||||
|
|
||||||
/// Revoke library access from a user (admin only)
|
/// 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.
|
/// slashes that conflict with URL routing.
|
||||||
pub async fn revoke_library_access(
|
pub async fn revoke_library_access(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use pinakes_core::{
|
||||||
ManagedStorageConfig,
|
ManagedStorageConfig,
|
||||||
PhotoConfig,
|
PhotoConfig,
|
||||||
PluginsConfig,
|
PluginsConfig,
|
||||||
|
RateLimitConfig,
|
||||||
ScanningConfig,
|
ScanningConfig,
|
||||||
ServerConfig,
|
ServerConfig,
|
||||||
SharingConfig,
|
SharingConfig,
|
||||||
|
|
@ -41,19 +42,19 @@ use pinakes_core::{
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower::ServiceExt;
|
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> {
|
fn test_addr() -> ConnectInfo<SocketAddr> {
|
||||||
ConnectInfo("127.0.0.1:9999".parse().unwrap())
|
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> {
|
fn get(uri: &str) -> Request<Body> {
|
||||||
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
|
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
|
||||||
req.extensions_mut().insert(test_addr());
|
req.extensions_mut().insert(test_addr());
|
||||||
req
|
req
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a POST request with ConnectInfo
|
/// Build a POST request with `ConnectInfo`
|
||||||
fn post_json(uri: &str, body: &str) -> Request<Body> {
|
fn post_json(uri: &str, body: &str) -> Request<Body> {
|
||||||
let mut req = Request::builder()
|
let mut req = Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
|
|
@ -69,7 +70,7 @@ fn post_json(uri: &str, body: &str) -> Request<Body> {
|
||||||
fn get_authed(uri: &str, token: &str) -> Request<Body> {
|
fn get_authed(uri: &str, token: &str) -> Request<Body> {
|
||||||
let mut req = Request::builder()
|
let mut req = Request::builder()
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("authorization", format!("Bearer {}", token))
|
.header("authorization", format!("Bearer {token}"))
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
req.extensions_mut().insert(test_addr());
|
req.extensions_mut().insert(test_addr());
|
||||||
|
|
@ -82,7 +83,7 @@ fn post_json_authed(uri: &str, body: &str, token: &str) -> Request<Body> {
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
.header("authorization", format!("Bearer {}", token))
|
.header("authorization", format!("Bearer {token}"))
|
||||||
.body(Body::from(body.to_string()))
|
.body(Body::from(body.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
req.extensions_mut().insert(test_addr());
|
req.extensions_mut().insert(test_addr());
|
||||||
|
|
@ -94,7 +95,7 @@ fn delete_authed(uri: &str, token: &str) -> Request<Body> {
|
||||||
let mut req = Request::builder()
|
let mut req = Request::builder()
|
||||||
.method("DELETE")
|
.method("DELETE")
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("authorization", format!("Bearer {}", token))
|
.header("authorization", format!("Bearer {token}"))
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
req.extensions_mut().insert(test_addr());
|
req.extensions_mut().insert(test_addr());
|
||||||
|
|
@ -107,7 +108,7 @@ fn patch_json_authed(uri: &str, body: &str, token: &str) -> Request<Body> {
|
||||||
.method("PATCH")
|
.method("PATCH")
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
.header("authorization", format!("Bearer {}", token))
|
.header("authorization", format!("Bearer {token}"))
|
||||||
.body(Body::from(body.to_string()))
|
.body(Body::from(body.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
req.extensions_mut().insert(test_addr());
|
req.extensions_mut().insert(test_addr());
|
||||||
|
|
@ -136,7 +137,10 @@ fn default_config() -> Config {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
tls: TlsConfig::default(),
|
tls: TlsConfig::default(),
|
||||||
authentication_disabled: true,
|
authentication_disabled: true,
|
||||||
|
cors_enabled: false,
|
||||||
|
cors_origins: vec![],
|
||||||
},
|
},
|
||||||
|
rate_limits: RateLimitConfig::default(),
|
||||||
ui: UiConfig::default(),
|
ui: UiConfig::default(),
|
||||||
accounts: AccountsConfig::default(),
|
accounts: AccountsConfig::default(),
|
||||||
jobs: JobsConfig::default(),
|
jobs: JobsConfig::default(),
|
||||||
|
|
@ -164,7 +168,7 @@ async fn setup_app() -> axum::Router {
|
||||||
let config = default_config();
|
let config = default_config();
|
||||||
|
|
||||||
let job_queue =
|
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 config = Arc::new(RwLock::new(config));
|
||||||
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
|
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
|
||||||
job_queue.clone(),
|
job_queue.clone(),
|
||||||
|
|
@ -186,9 +190,10 @@ async fn setup_app() -> axum::Router {
|
||||||
managed_storage: None,
|
managed_storage: None,
|
||||||
chunked_upload_manager: None,
|
chunked_upload_manager: None,
|
||||||
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
|
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
|
/// 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.
|
/// 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) {
|
async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
|
||||||
let backend = SqliteBackend::in_memory().expect("in-memory SQLite");
|
let backend = SqliteBackend::in_memory().expect("in-memory SQLite");
|
||||||
backend.run_migrations().await.expect("migrations");
|
backend.run_migrations().await.expect("migrations");
|
||||||
|
|
@ -239,7 +244,7 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
|
||||||
];
|
];
|
||||||
|
|
||||||
let job_queue =
|
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 config = Arc::new(RwLock::new(config));
|
||||||
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
|
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
|
||||||
job_queue.clone(),
|
job_queue.clone(),
|
||||||
|
|
@ -261,9 +266,11 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
|
||||||
managed_storage: None,
|
managed_storage: None,
|
||||||
chunked_upload_manager: None,
|
chunked_upload_manager: None,
|
||||||
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
|
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
|
// Login each user to get tokens
|
||||||
let admin_token = login_user(app.clone(), "admin", "adminpass").await;
|
let admin_token = login_user(app.clone(), "admin", "adminpass").await;
|
||||||
|
|
@ -278,8 +285,7 @@ async fn login_user(
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
let body =
|
let body = format!(r#"{{"username":"{username}","password":"{password}"}}"#);
|
||||||
format!(r#"{{"username":"{}","password":"{}"}}"#, username, password);
|
|
||||||
let response = app
|
let response = app
|
||||||
.oneshot(post_json("/api/v1/auth/login", &body))
|
.oneshot(post_json("/api/v1/auth/login", &body))
|
||||||
.await
|
.await
|
||||||
|
|
@ -287,8 +293,7 @@ async fn login_user(
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
response.status(),
|
response.status(),
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
"login failed for user {}",
|
"login failed for user {username}"
|
||||||
username
|
|
||||||
);
|
);
|
||||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
let result: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
let result: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
|
@ -449,7 +454,7 @@ async fn test_user_management_crud() {
|
||||||
// Get specific user
|
// Get specific user
|
||||||
let response = app
|
let response = app
|
||||||
.clone()
|
.clone()
|
||||||
.oneshot(get(&format!("/api/v1/users/{}", user_id)))
|
.oneshot(get(&format!("/api/v1/users/{user_id}")))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -462,7 +467,7 @@ async fn test_user_management_crud() {
|
||||||
// Delete user
|
// Delete user
|
||||||
let mut req = Request::builder()
|
let mut req = Request::builder()
|
||||||
.method("DELETE")
|
.method("DELETE")
|
||||||
.uri(&format!("/api/v1/users/{}", user_id))
|
.uri(format!("/api/v1/users/{user_id}"))
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
req.extensions_mut().insert(test_addr());
|
req.extensions_mut().insert(test_addr());
|
||||||
|
|
@ -472,7 +477,7 @@ async fn test_user_management_crud() {
|
||||||
|
|
||||||
// Verify user is deleted
|
// Verify user is deleted
|
||||||
let response = app
|
let response = app
|
||||||
.oneshot(get(&format!("/api/v1/users/{}", user_id)))
|
.oneshot(get(&format!("/api/v1/users/{user_id}")))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
@ -796,7 +801,7 @@ async fn test_playlist_crud() {
|
||||||
let response = app
|
let response = app
|
||||||
.clone()
|
.clone()
|
||||||
.oneshot(get_authed(
|
.oneshot(get_authed(
|
||||||
&format!("/api/v1/playlists/{}", playlist_id),
|
&format!("/api/v1/playlists/{playlist_id}"),
|
||||||
&editor_token,
|
&editor_token,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|
@ -807,7 +812,7 @@ async fn test_playlist_crud() {
|
||||||
let response = app
|
let response = app
|
||||||
.clone()
|
.clone()
|
||||||
.oneshot(patch_json_authed(
|
.oneshot(patch_json_authed(
|
||||||
&format!("/api/v1/playlists/{}", playlist_id),
|
&format!("/api/v1/playlists/{playlist_id}"),
|
||||||
r#"{"name":"Updated Playlist","description":"A test description"}"#,
|
r#"{"name":"Updated Playlist","description":"A test description"}"#,
|
||||||
&editor_token,
|
&editor_token,
|
||||||
))
|
))
|
||||||
|
|
@ -821,7 +826,7 @@ async fn test_playlist_crud() {
|
||||||
let response = app
|
let response = app
|
||||||
.clone()
|
.clone()
|
||||||
.oneshot(delete_authed(
|
.oneshot(delete_authed(
|
||||||
&format!("/api/v1/playlists/{}", playlist_id),
|
&format!("/api/v1/playlists/{playlist_id}"),
|
||||||
&editor_token,
|
&editor_token,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|
@ -972,7 +977,7 @@ async fn test_oversized_comment() {
|
||||||
let (app, _, editor_token, _) = setup_app_with_auth().await;
|
let (app, _, editor_token, _) = setup_app_with_auth().await;
|
||||||
|
|
||||||
let long_text: String = "x".repeat(10_001);
|
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
|
let response = app
|
||||||
.oneshot(post_json_authed(
|
.oneshot(post_json_authed(
|
||||||
"/api/v1/media/00000000-0000-0000-0000-000000000000/comments",
|
"/api/v1/media/00000000-0000-0000-0000-000000000000/comments",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use pinakes_core::{
|
||||||
ManagedStorageConfig,
|
ManagedStorageConfig,
|
||||||
PhotoConfig,
|
PhotoConfig,
|
||||||
PluginsConfig,
|
PluginsConfig,
|
||||||
|
RateLimitConfig,
|
||||||
ScanningConfig,
|
ScanningConfig,
|
||||||
ServerConfig,
|
ServerConfig,
|
||||||
SharingConfig,
|
SharingConfig,
|
||||||
|
|
@ -40,12 +41,12 @@ use pinakes_core::{
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower::ServiceExt;
|
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> {
|
fn test_addr() -> ConnectInfo<SocketAddr> {
|
||||||
ConnectInfo("127.0.0.1:9999".parse().unwrap())
|
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> {
|
fn get(uri: &str) -> Request<Body> {
|
||||||
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
|
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
|
||||||
req.extensions_mut().insert(test_addr());
|
req.extensions_mut().insert(test_addr());
|
||||||
|
|
@ -103,7 +104,10 @@ async fn setup_app_with_plugins()
|
||||||
api_key: None,
|
api_key: None,
|
||||||
tls: TlsConfig::default(),
|
tls: TlsConfig::default(),
|
||||||
authentication_disabled: true,
|
authentication_disabled: true,
|
||||||
|
cors_enabled: false,
|
||||||
|
cors_origins: vec![],
|
||||||
},
|
},
|
||||||
|
rate_limits: RateLimitConfig::default(),
|
||||||
ui: UiConfig::default(),
|
ui: UiConfig::default(),
|
||||||
accounts: AccountsConfig::default(),
|
accounts: AccountsConfig::default(),
|
||||||
jobs: JobsConfig::default(),
|
jobs: JobsConfig::default(),
|
||||||
|
|
@ -123,7 +127,7 @@ async fn setup_app_with_plugins()
|
||||||
};
|
};
|
||||||
|
|
||||||
let job_queue =
|
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 config = Arc::new(RwLock::new(config));
|
||||||
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
|
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
|
||||||
job_queue.clone(),
|
job_queue.clone(),
|
||||||
|
|
@ -145,9 +149,11 @@ async fn setup_app_with_plugins()
|
||||||
managed_storage: None,
|
managed_storage: None,
|
||||||
chunked_upload_manager: None,
|
chunked_upload_manager: None,
|
||||||
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
|
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)
|
(router, plugin_manager, temp_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue