treewide: fix as many Clippy warnings as I humanly can

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I3c99acd032679bb7a04505db1a712b906a6a6964
This commit is contained in:
raf 2026-05-20 21:52:31 +03:00
commit 602cfb68b7
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
65 changed files with 1191 additions and 540 deletions

View file

@ -3,10 +3,7 @@
use chrono::{DateTime, Utc};
use crate::{
error::Result,
model::{MediaId, MediaItem},
};
use crate::model::{MediaId, MediaItem};
/// Configuration for event detection
#[derive(Debug, Clone)]
@ -68,15 +65,16 @@ fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
}
/// Detect photo events from a list of media items
#[must_use]
pub fn detect_events(
mut items: Vec<MediaItem>,
config: &EventDetectionConfig,
) -> Result<Vec<DetectedEvent>> {
) -> Vec<DetectedEvent> {
// Filter to only photos with date_taken
items.retain(|item| item.date_taken.is_some());
if items.is_empty() {
return Ok(Vec::new());
return Vec::new();
}
// Sort by date_taken (None < Some, but all are Some after retain)
@ -84,7 +82,7 @@ pub fn detect_events(
let mut events: Vec<DetectedEvent> = Vec::new();
let Some(first_date) = items[0].date_taken else {
return Ok(Vec::new());
return Vec::new();
};
let mut current_event_items: Vec<MediaId> = vec![items[0].id];
let mut current_start_time = first_date;
@ -171,21 +169,22 @@ pub fn detect_events(
});
}
Ok(events)
events
}
/// Detect photo bursts (rapid sequences of photos)
/// Returns groups of media IDs that are likely burst sequences
#[must_use]
pub fn detect_bursts(
mut items: Vec<MediaItem>,
max_gap_secs: i64,
min_burst_size: usize,
) -> Result<Vec<Vec<MediaId>>> {
) -> Vec<Vec<MediaId>> {
// Filter to only photos with date_taken
items.retain(|item| item.date_taken.is_some());
if items.is_empty() {
return Ok(Vec::new());
return Vec::new();
}
// Sort by date_taken (None < Some, but all are Some after retain)
@ -193,7 +192,7 @@ pub fn detect_bursts(
let mut bursts: Vec<Vec<MediaId>> = Vec::new();
let Some(first_date) = items[0].date_taken else {
return Ok(Vec::new());
return Vec::new();
};
let mut current_burst: Vec<MediaId> = vec![items[0].id];
let mut last_time = first_date;
@ -221,5 +220,5 @@ pub fn detect_bursts(
bursts.push(current_burst);
}
Ok(bursts)
bursts
}

View file

@ -185,12 +185,12 @@ impl JobQueue {
};
{
let mut map = self.jobs.write().await;
map.insert(id, job);
// Prune old terminal jobs to prevent unbounded memory growth.
// Keep at most 500 completed/failed/cancelled entries, removing the
// oldest.
const MAX_TERMINAL_JOBS: usize = 500;
let mut map = self.jobs.write().await;
map.insert(id, job);
let mut terminal: Vec<(Uuid, chrono::DateTime<Utc>)> = map
.iter()
.filter(|(_, j)| {

View file

@ -3,6 +3,11 @@ use std::{path::Path, process::Command};
use crate::error::{PinakesError, Result};
pub trait Opener: Send + Sync {
/// Open the file at `path` with the system default application.
///
/// # Errors
///
/// Returns an error if the opener command fails to launch or exits non-zero.
fn open(&self, path: &Path) -> Result<()>;
}

View file

@ -20,6 +20,7 @@ use std::{
use pinakes_metadata::ExtractedMetadata;
use pinakes_plugin::{
CapabilityEnforcer,
PluginManager,
rpc::{
CanHandleRequest,
@ -131,7 +132,7 @@ impl PluginPipeline {
pub async fn discover_capabilities(&self) -> crate::error::Result<()> {
info!("discovering plugin capabilities");
let timeout = Duration::from_secs(self.timeouts.capability_query_secs);
let timeout = Duration::from_secs(self.timeouts.capability_query);
let mut caps = CachedCapabilities::new();
// Discover metadata extractors
@ -322,7 +323,7 @@ impl PluginPipeline {
/// Iterates `MediaTypeProvider` plugins in priority order, falling back to
/// the built-in resolver at implicit priority 100.
pub async fn resolve_media_type(&self, path: &Path) -> Option<MediaType> {
let timeout = Duration::from_secs(self.timeouts.processing_secs);
let timeout = Duration::from_secs(self.timeouts.processing);
let plugins = self.manager.get_enabled_by_kind_sorted("media_type").await;
let mut builtin_ran = false;
@ -341,11 +342,7 @@ impl PluginPipeline {
}
// Validate the call is allowed for this plugin kind
if !self
.manager
.enforcer()
.validate_function_call(kinds, "can_handle")
{
if !CapabilityEnforcer::validate_function_call(kinds, "can_handle") {
continue;
}
@ -441,7 +438,7 @@ impl PluginPipeline {
path: &Path,
media_type: &MediaType,
) -> crate::error::Result<ExtractedMetadata> {
let timeout = Duration::from_secs(self.timeouts.processing_secs);
let timeout = Duration::from_secs(self.timeouts.processing);
let plugins = self
.manager
.get_enabled_by_kind_sorted("metadata_extractor")
@ -475,10 +472,7 @@ impl PluginPipeline {
continue;
}
if !self
.manager
.enforcer()
.validate_function_call(kinds, "extract_metadata")
if !CapabilityEnforcer::validate_function_call(kinds, "extract_metadata")
{
continue;
}
@ -558,7 +552,7 @@ impl PluginPipeline {
media_type: &MediaType,
thumb_dir: &Path,
) -> crate::error::Result<Option<PathBuf>> {
let timeout = Duration::from_secs(self.timeouts.processing_secs);
let timeout = Duration::from_secs(self.timeouts.processing);
let plugins = self
.manager
.get_enabled_by_kind_sorted("thumbnail_generator")
@ -596,11 +590,10 @@ impl PluginPipeline {
continue;
}
if !self
.manager
.enforcer()
.validate_function_call(kinds, "generate_thumbnail")
{
if !CapabilityEnforcer::validate_function_call(
kinds,
"generate_thumbnail",
) {
continue;
}
@ -693,7 +686,7 @@ impl PluginPipeline {
event_type: &str,
payload: &serde_json::Value,
) {
let timeout = Duration::from_secs(self.timeouts.event_handler_secs);
let timeout = Duration::from_secs(self.timeouts.event_handler);
// Collect plugin IDs interested in this event
let interested_ids: Vec<String> = {
@ -725,11 +718,7 @@ impl PluginPipeline {
continue;
}
if !self
.manager
.enforcer()
.validate_function_call(kinds, "handle_event")
{
if !CapabilityEnforcer::validate_function_call(kinds, "handle_event") {
continue;
}
@ -789,7 +778,7 @@ impl PluginPipeline {
limit: usize,
offset: usize,
) -> Vec<SearchResultItem> {
let timeout = Duration::from_secs(self.timeouts.processing_secs);
let timeout = Duration::from_secs(self.timeouts.processing);
let plugins = self
.manager
.get_enabled_by_kind_sorted("search_backend")
@ -801,11 +790,7 @@ impl PluginPipeline {
if !self.is_healthy(id).await {
continue;
}
if !self
.manager
.enforcer()
.validate_function_call(kinds, "search")
{
if !CapabilityEnforcer::validate_function_call(kinds, "search") {
continue;
}
@ -862,7 +847,7 @@ impl PluginPipeline {
/// Index a media item in all search backend plugins (fan-out).
pub async fn index_item(&self, req: &IndexItemRequest) {
let timeout = Duration::from_secs(self.timeouts.processing_secs);
let timeout = Duration::from_secs(self.timeouts.processing);
let plugins = self
.manager
.get_enabled_by_kind_sorted("search_backend")
@ -872,11 +857,7 @@ impl PluginPipeline {
if !self.is_healthy(id).await {
continue;
}
if !self
.manager
.enforcer()
.validate_function_call(kinds, "index_item")
{
if !CapabilityEnforcer::validate_function_call(kinds, "index_item") {
continue;
}
@ -905,7 +886,7 @@ impl PluginPipeline {
/// Remove a media item from all search backend plugins (fan-out).
pub async fn remove_item(&self, media_id: &str) {
let timeout = Duration::from_secs(self.timeouts.processing_secs);
let timeout = Duration::from_secs(self.timeouts.processing);
let plugins = self
.manager
.get_enabled_by_kind_sorted("search_backend")
@ -919,11 +900,7 @@ impl PluginPipeline {
if !self.is_healthy(id).await {
continue;
}
if !self
.manager
.enforcer()
.validate_function_call(kinds, "remove_item")
{
if !CapabilityEnforcer::validate_function_call(kinds, "remove_item") {
continue;
}
@ -963,7 +940,7 @@ impl PluginPipeline {
/// Load a specific theme by ID from the provider that registered it.
pub async fn load_theme(&self, theme_id: &str) -> Option<LoadThemeResponse> {
let timeout = Duration::from_secs(self.timeouts.processing_secs);
let timeout = Duration::from_secs(self.timeouts.processing);
// Find which plugin owns this theme
let owner_id = {
@ -990,11 +967,7 @@ impl PluginPipeline {
let plugin = plugins.iter().find(|(id, ..)| id == &owner_id)?;
let (id, _priority, kinds, wasm) = plugin;
if !self
.manager
.enforcer()
.validate_function_call(kinds, "load_theme")
{
if !CapabilityEnforcer::validate_function_call(kinds, "load_theme") {
return None;
}

View file

@ -204,7 +204,7 @@ impl SharePermissions {
/// Merges two permission sets, taking the most permissive values.
#[must_use]
pub const fn merge(&self, other: &Self) -> Self {
pub const fn merge(self, other: Self) -> Self {
Self {
view: ShareViewPermissions {
can_view: self.view.can_view || other.view.can_view,

View file

@ -1,3 +1,6 @@
/// # Errors
///
/// Returns an error if migrations fail to apply.
#[cfg(feature = "sqlite")]
pub fn run_sqlite_migrations(
conn: &mut rusqlite::Connection,
@ -7,6 +10,9 @@ pub fn run_sqlite_migrations(
.map_err(|e| crate::error::PinakesError::Migration(e.to_string()))
}
/// # Errors
///
/// Returns an error if migrations fail to apply.
#[cfg(feature = "postgres")]
pub async fn run_postgres_migrations(
client: &mut tokio_postgres::Client,

View file

@ -423,10 +423,12 @@ pub trait StorageBackend: Send + Sync + 'static {
user_id: crate::users::UserId,
media_id: crate::model::MediaId,
) -> Result<bool> {
match self.check_library_access(user_id, media_id).await {
Ok(perm) => Ok(perm.can_read()),
Err(_) => Ok(false),
}
Ok(
self
.check_library_access(user_id, media_id)
.await
.is_ok_and(|_perm| crate::users::LibraryPermission::can_read()),
)
}
/// Check if a user has write access to a media item
@ -435,10 +437,12 @@ pub trait StorageBackend: Send + Sync + 'static {
user_id: crate::users::UserId,
media_id: crate::model::MediaId,
) -> Result<bool> {
match self.check_library_access(user_id, media_id).await {
Ok(perm) => Ok(perm.can_write()),
Err(_) => Ok(false),
}
Ok(
self
.check_library_access(user_id, media_id)
.await
.is_ok_and(crate::users::LibraryPermission::can_write),
)
}
/// Rate a media item (1-5 stars) with an optional text review.

View file

@ -255,7 +255,6 @@ fn row_to_audit_entry(row: &Row) -> rusqlite::Result<AuditEntry> {
let action = match action_str.as_str() {
"imported" => AuditAction::Imported,
"updated" => AuditAction::Updated,
"deleted" => AuditAction::Deleted,
"tagged" => AuditAction::Tagged,
"untagged" => AuditAction::Untagged,
@ -725,7 +724,6 @@ impl StorageBackend for SqliteBackend {
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(stmt);
drop(db);
rows
};
Ok(rows)
@ -863,7 +861,6 @@ impl StorageBackend for SqliteBackend {
drop(stmt);
item.custom_fields = load_custom_fields_sync(&db, item.id)
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(db);
item
};
Ok(item)
@ -902,10 +899,8 @@ impl StorageBackend for SqliteBackend {
if let Some(mut item) = result {
item.custom_fields = load_custom_fields_sync(&db, item.id)
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(db);
Some(item)
} else {
drop(db);
None
}
};
@ -945,10 +940,8 @@ impl StorageBackend for SqliteBackend {
if let Some(mut item) = result {
item.custom_fields = load_custom_fields_sync(&db, item.id)
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(db);
Some(item)
} else {
drop(db);
None
}
};
@ -1006,7 +999,6 @@ impl StorageBackend for SqliteBackend {
drop(stmt);
load_custom_fields_batch(&db, &mut rows)
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(db);
rows
};
Ok(rows)
@ -1062,7 +1054,6 @@ impl StorageBackend for SqliteBackend {
],
)
.map_err(db_ctx("update_media", &item.id))?;
drop(db);
if changed == 0 {
return Err(PinakesError::NotFound(format!(
"media item {}",
@ -1088,7 +1079,6 @@ impl StorageBackend for SqliteBackend {
id.0.to_string()
])
.map_err(db_ctx("delete_media", id))?;
drop(db);
if changed == 0 {
return Err(PinakesError::NotFound(format!("media item {id}")));
}
@ -1147,7 +1137,6 @@ impl StorageBackend for SqliteBackend {
],
)
.map_err(db_ctx("create_tag", &name))?;
drop(db);
Tag {
id,
name,
@ -1184,7 +1173,6 @@ impl StorageBackend for SqliteBackend {
}
})?;
drop(stmt);
drop(db);
tag
};
Ok(tag)
@ -1211,7 +1199,6 @@ impl StorageBackend for SqliteBackend {
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(stmt);
drop(db);
rows
};
Ok(rows)
@ -1230,7 +1217,6 @@ impl StorageBackend for SqliteBackend {
let changed = db
.execute("DELETE FROM tags WHERE id = ?1", params![id.to_string()])
.map_err(db_ctx("delete_tag", id))?;
drop(db);
if changed == 0 {
return Err(PinakesError::TagNotFound(id.to_string()));
}
@ -1299,7 +1285,6 @@ impl StorageBackend for SqliteBackend {
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(stmt);
drop(db);
rows
};
Ok(rows)
@ -1330,7 +1315,6 @@ impl StorageBackend for SqliteBackend {
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(stmt);
drop(db);
rows
};
Ok(rows)
@ -1372,7 +1356,6 @@ impl StorageBackend for SqliteBackend {
],
)
.map_err(db_ctx("create_collection", &name))?;
drop(db);
Collection {
id,
name,
@ -1413,7 +1396,6 @@ impl StorageBackend for SqliteBackend {
}
})?;
drop(stmt);
drop(db);
collection
};
Ok(collection)
@ -1441,7 +1423,6 @@ impl StorageBackend for SqliteBackend {
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(stmt);
drop(db);
rows
};
Ok(rows)
@ -1462,7 +1443,6 @@ impl StorageBackend for SqliteBackend {
id.to_string()
])
.map_err(db_ctx("delete_collection", id))?;
drop(db);
if changed == 0 {
return Err(PinakesError::CollectionNotFound(id.to_string()));
}
@ -1565,7 +1545,6 @@ impl StorageBackend for SqliteBackend {
drop(stmt);
load_custom_fields_batch(&db, &mut rows)
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(db);
rows
};
Ok(rows)
@ -1675,7 +1654,6 @@ impl StorageBackend for SqliteBackend {
let total_count: i64 = db
.query_row(&count_sql, count_param_refs.as_slice(), |row| row.get(0))
.map_err(|e| PinakesError::Database(e.to_string()))?;
drop(db);
SearchResults {
items,
@ -1777,7 +1755,6 @@ impl StorageBackend for SqliteBackend {
.map_err(|e| PinakesError::Database(e.to_string()))?
};
drop(stmt);
drop(db);
rows
};
@ -1854,7 +1831,6 @@ impl StorageBackend for SqliteBackend {
map.insert(name, field);
}
drop(stmt);
drop(db);
map
};
Ok(map)
@ -2907,7 +2883,7 @@ impl StorageBackend for SqliteBackend {
crate::users::UserProfile {
avatar_path: None,
bio: None,
preferences: Default::default(),
preferences: crate::users::UserPreferences::default(),
}
};
@ -3017,36 +2993,34 @@ impl StorageBackend for SqliteBackend {
.map_err(|e| PinakesError::Database(e.to_string()))?;
// Fetch updated user
Ok(
db.query_row(
"SELECT id, username, password_hash, role, created_at, updated_at \
FROM users WHERE id = ?",
[&id_str],
|row| {
let id_str: String = row.get(0)?;
let profile = load_user_profile_sync(&db, &id_str)?;
Ok(crate::users::User {
id: crate::users::UserId(parse_uuid(&id_str)?),
username: row.get(1)?,
password_hash: row.get(2)?,
role: serde_json::from_str(&row.get::<_, String>(3)?)
.unwrap_or(crate::config::UserRole::Viewer),
profile,
created_at: chrono::DateTime::parse_from_rfc3339(
&row.get::<_, String>(4)?,
)
.unwrap_or_else(|_| chrono::Utc::now().into())
.with_timezone(&chrono::Utc),
updated_at: chrono::DateTime::parse_from_rfc3339(
&row.get::<_, String>(5)?,
)
.unwrap_or_else(|_| chrono::Utc::now().into())
.with_timezone(&chrono::Utc),
})
},
)
.map_err(|e| PinakesError::Database(e.to_string()))?,
db.query_row(
"SELECT id, username, password_hash, role, created_at, updated_at \
FROM users WHERE id = ?",
[&id_str],
|row| {
let id_str: String = row.get(0)?;
let profile = load_user_profile_sync(&db, &id_str)?;
Ok(crate::users::User {
id: crate::users::UserId(parse_uuid(&id_str)?),
username: row.get(1)?,
password_hash: row.get(2)?,
role: serde_json::from_str(&row.get::<_, String>(3)?)
.unwrap_or(crate::config::UserRole::Viewer),
profile,
created_at: chrono::DateTime::parse_from_rfc3339(
&row.get::<_, String>(4)?,
)
.unwrap_or_else(|_| chrono::Utc::now().into())
.with_timezone(&chrono::Utc),
updated_at: chrono::DateTime::parse_from_rfc3339(
&row.get::<_, String>(5)?,
)
.unwrap_or_else(|_| chrono::Utc::now().into())
.with_timezone(&chrono::Utc),
})
},
)
.map_err(|e| PinakesError::Database(e.to_string()))
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
@ -5949,39 +5923,33 @@ impl StorageBackend for SqliteBackend {
.map_err(|e| PinakesError::Database(e.to_string()))?;
let mut results = Vec::new();
for row in rows {
match row {
Ok((item, current_page, total_pages)) => {
// Calculate status based on progress
let calculated_status = total_pages.map_or(
// No total pages known, assume reading
crate::model::ReadingStatus::Reading,
|total| {
if total > 0 {
let percent = (f64::from(current_page) / f64::from(total)
* 100.0)
.min(100.0);
if percent >= 100.0 {
crate::model::ReadingStatus::Completed
} else if percent > 0.0 {
crate::model::ReadingStatus::Reading
} else {
crate::model::ReadingStatus::ToRead
}
} else {
crate::model::ReadingStatus::Reading
}
},
);
// Filter by status if specified
match status {
None => results.push(item),
Some(s) if s == calculated_status => results.push(item),
_ => {},
for (item, current_page, total_pages) in rows.flatten() {
// Calculate status based on progress
let calculated_status = total_pages.map_or(
// No total pages known, assume reading
crate::model::ReadingStatus::Reading,
|total| {
if total > 0 {
let percent =
(f64::from(current_page) / f64::from(total) * 100.0).min(100.0);
if percent >= 100.0 {
crate::model::ReadingStatus::Completed
} else if percent > 0.0 {
crate::model::ReadingStatus::Reading
} else {
crate::model::ReadingStatus::ToRead
}
} else {
crate::model::ReadingStatus::Reading
}
},
Err(_) => continue,
);
// Filter by status if specified
match status {
None => results.push(item),
Some(s) if s == calculated_status => results.push(item),
_ => {},
}
}
Ok::<_, PinakesError>(results)
@ -7809,7 +7777,7 @@ impl StorageBackend for SqliteBackend {
) if *share_user == uid => {
return Ok(Some(share.permissions));
},
_ => continue,
_ => {},
}
}
@ -8216,7 +8184,7 @@ impl StorageBackend for SqliteBackend {
let new_name = new_name.to_string();
let (old_path, storage_mode) = tokio::task::spawn_blocking({
let conn = conn.clone();
let conn = Arc::clone(&conn);
let id_str = id_str.clone();
move || {
let conn = conn.lock().map_err(|e| {
@ -8239,7 +8207,7 @@ impl StorageBackend for SqliteBackend {
})??;
let old_path_buf = std::path::PathBuf::from(&old_path);
let parent = old_path_buf.parent().unwrap_or(std::path::Path::new(""));
let parent = old_path_buf.parent().unwrap_or_else(|| Path::new(""));
let new_path = parent.join(&new_name);
let new_path_str = new_path.to_string_lossy().to_string();
@ -8288,7 +8256,7 @@ impl StorageBackend for SqliteBackend {
let new_dir = new_directory.to_path_buf();
let (old_path, file_name, storage_mode) = tokio::task::spawn_blocking({
let conn = conn.clone();
let conn = Arc::clone(&conn);
let id_str = id_str.clone();
move || {
let conn = conn.lock().map_err(|e| {
@ -8788,10 +8756,8 @@ impl StorageBackend for SqliteBackend {
let conn = conn.lock().map_err(|e| PinakesError::Database(format!("connection mutex poisoned: {e}")))?;
let mut nodes = Vec::new();
let mut edges = Vec::new();
let mut node_ids = rustc_hash::FxHashSet::default();
// Get nodes - either all markdown files or those connected to center
if let Some(center_id) = center_id_str {
let node_ids = if let Some(center_id) = center_id_str {
// BFS to find connected nodes within depth
let mut frontier = vec![center_id.clone()];
let mut visited = rustc_hash::FxHashSet::default();
@ -8839,9 +8805,10 @@ impl StorageBackend for SqliteBackend {
frontier = next_frontier;
}
node_ids = visited;
visited
} else {
// Get all markdown files with links (limit to 500 for performance)
let mut ids = rustc_hash::FxHashSet::default();
let mut stmt = conn.prepare(
"SELECT DISTINCT id FROM media_items
WHERE media_type = 'markdown' AND deleted_at IS NULL
@ -8852,10 +8819,10 @@ impl StorageBackend for SqliteBackend {
Ok(id)
}).map_err(|e| PinakesError::Database(e.to_string()))?;
for row in rows {
node_ids.insert(row.map_err(|e| PinakesError::Database(e.to_string()))?);
ids.insert(row.map_err(|e| PinakesError::Database(e.to_string()))?);
}
}
ids
};
// Build nodes with metadata
for node_id in &node_ids {
@ -9108,11 +9075,6 @@ fn row_to_share(row: &Row) -> rusqlite::Result<crate::sharing::Share> {
let password_hash: Option<String> = row.get(7)?;
let target = match target_type.as_str() {
"media" => {
crate::sharing::ShareTarget::Media {
media_id: MediaId(parse_uuid(&target_id_str)?),
}
},
"collection" => {
crate::sharing::ShareTarget::Collection {
collection_id: parse_uuid(&target_id_str)?,
@ -9136,12 +9098,6 @@ fn row_to_share(row: &Row) -> rusqlite::Result<crate::sharing::Share> {
};
let recipient = match recipient_type.as_str() {
"public_link" => {
crate::sharing::ShareRecipient::PublicLink {
token: public_token.unwrap_or_default(),
password_hash,
}
},
"user" => {
crate::sharing::ShareRecipient::User {
user_id: crate::users::UserId(parse_uuid(

View file

@ -172,9 +172,8 @@ pub async fn list_embedded_tracks(
}
})?;
let streams = match json.get("streams").and_then(|s| s.as_array()) {
Some(s) => s,
None => return Ok(vec![]),
let Some(streams) = json.get("streams").and_then(|s| s.as_array()) else {
return Ok(vec![]);
};
let mut tracks = Vec::new();
@ -203,7 +202,7 @@ pub async fn list_embedded_tracks(
.map(str::to_owned);
tracks.push(SubtitleTrackInfo {
index: idx as u32,
index: u32::try_from(idx).unwrap_or(u32::MAX),
language,
format,
title,

View file

@ -367,7 +367,7 @@ pub enum CoverSize {
impl CoverSize {
#[must_use]
pub const fn dimensions(&self) -> Option<(u32, u32)> {
pub const fn dimensions(self) -> Option<(u32, u32)> {
match self {
Self::Tiny => Some((64, 64)),
Self::Grid => Some((320, 320)),
@ -377,7 +377,7 @@ impl CoverSize {
}
#[must_use]
pub const fn filename(&self) -> &'static str {
pub const fn filename(self) -> &'static str {
match self {
Self::Tiny => "tiny.jpg",
Self::Grid => "grid.jpg",
@ -541,7 +541,7 @@ pub enum ThumbnailSize {
impl ThumbnailSize {
/// Get the pixel size for this thumbnail variant
#[must_use]
pub const fn pixels(&self) -> u32 {
pub const fn pixels(self) -> u32 {
match self {
Self::Tiny => 64,
Self::Grid => 320,
@ -551,7 +551,7 @@ impl ThumbnailSize {
/// Get the subdirectory name for this size
#[must_use]
pub const fn subdir_name(&self) -> &'static str {
pub const fn subdir_name(self) -> &'static str {
match self {
Self::Tiny => "tiny",
Self::Grid => "grid",

View file

@ -103,9 +103,9 @@ impl TranscodeService {
pub fn new(config: TranscodingConfig) -> Self {
let max_concurrent = config.max_concurrent.max(1);
Self {
config,
sessions: Arc::new(RwLock::new(FxHashMap::default())),
semaphore: Arc::new(Semaphore::new(max_concurrent)),
config,
}
}

View file

@ -1,15 +1,15 @@
//! User management and authentication
use chrono::{DateTime, Utc};
pub use pinakes_types::model::UserId;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use crate::{
config::UserRole,
error::{PinakesError, Result},
};
pub use pinakes_types::model::UserId;
/// User account with profile information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
@ -67,24 +67,24 @@ pub enum LibraryPermission {
impl LibraryPermission {
/// Checks if read permission is granted.
#[must_use]
pub const fn can_read(&self) -> bool {
pub const fn can_read() -> bool {
true
}
/// Checks if write permission is granted.
#[must_use]
pub const fn can_write(&self) -> bool {
pub const fn can_write(self) -> bool {
matches!(self, Self::Write | Self::Admin)
}
/// Checks if admin permission is granted.
#[must_use]
pub const fn can_admin(&self) -> bool {
pub const fn can_admin(self) -> bool {
matches!(self, Self::Admin)
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
@ -132,6 +132,10 @@ pub mod auth {
use super::{PinakesError, Result};
/// Hash a password using Argon2
///
/// # Errors
///
/// Returns an error if password hashing fails.
pub fn hash_password(password: &str) -> Result<String> {
use argon2::{
Argon2,
@ -150,6 +154,10 @@ pub mod auth {
}
/// Verify a password against a hash
///
/// # Errors
///
/// Returns an error if the hash is invalid or cannot be parsed.
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
use argon2::{
Argon2,
@ -193,17 +201,17 @@ mod tests {
#[test]
fn test_library_permission_levels() {
let read = LibraryPermission::Read;
assert!(read.can_read());
assert!(LibraryPermission::can_read());
assert!(!read.can_write());
assert!(!read.can_admin());
let write = LibraryPermission::Write;
assert!(write.can_read());
assert!(LibraryPermission::can_read());
assert!(write.can_write());
assert!(!write.can_admin());
let admin = LibraryPermission::Admin;
assert!(admin.can_read());
assert!(LibraryPermission::can_read());
assert!(admin.can_write());
assert!(admin.can_admin());
}

View file

@ -69,7 +69,7 @@ impl WebhookDispatcher {
/// Dispatch an event to all matching webhooks.
/// This is fire-and-forget, errors are logged but not propagated.
pub fn dispatch(self: &Arc<Self>, event: WebhookEvent) {
let this = self.clone();
let this = Arc::clone(self);
tokio::spawn(async move {
this.dispatch_inner(&event).await;
});

View file

@ -74,7 +74,7 @@ impl BookEnricher {
})?;
Ok(Some(ExternalMetadata {
id: Uuid::new_v4(),
id: Uuid::now_v7(),
media_id: pinakes_types::model::MediaId(Uuid::nil()), /* Will be set by caller */
source: EnrichmentSourceType::OpenLibrary,
external_id: None,
@ -104,7 +104,7 @@ impl BookEnricher {
})?;
Ok(Some(ExternalMetadata {
id: Uuid::new_v4(),
id: Uuid::now_v7(),
media_id: pinakes_types::model::MediaId(Uuid::nil()), /* Will be set by caller */
source: EnrichmentSourceType::GoogleBooks,
external_id: Some(book.id.clone()),
@ -136,7 +136,7 @@ impl BookEnricher {
})?;
return Ok(Some(ExternalMetadata {
id: Uuid::new_v4(),
id: Uuid::now_v7(),
media_id: pinakes_types::model::MediaId(Uuid::nil()),
source: EnrichmentSourceType::OpenLibrary,
external_id: result.key.clone(),
@ -155,7 +155,7 @@ impl BookEnricher {
})?;
return Ok(Some(ExternalMetadata {
id: Uuid::new_v4(),
id: Uuid::now_v7(),
media_id: pinakes_types::model::MediaId(Uuid::nil()),
source: EnrichmentSourceType::GoogleBooks,
external_id: Some(book.id.clone()),

View file

@ -20,21 +20,21 @@ pub struct TmdbEnricher {
impl TmdbEnricher {
/// Create a new `TMDb` enricher.
///
/// # Panics
/// # Errors
///
/// Panics if the HTTP client cannot be built (programming error in client
/// configuration).
#[must_use]
pub fn new(api_key: String) -> Self {
Self {
client: reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(5))
.build()
.expect("failed to build HTTP client with configured timeouts"),
/// Returns an error if the HTTP client cannot be built (e.g. TLS
/// initialisation failure).
pub fn new(api_key: String) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(5))
.build()
.map_err(|e| PinakesError::External(e.to_string()))?;
Ok(Self {
client,
api_key,
base_url: "https://api.themoviedb.org/3".to_string(),
}
})
}
}

View file

@ -5,6 +5,7 @@ mod postgres_migrations {
embed_migrations!("migrations/postgres");
}
#[must_use]
pub fn sqlite_migrations() -> Migrations<'static> {
Migrations::new(vec![
M::up(include_str!("../migrations/sqlite/V1__initial_schema.sql")),
@ -49,6 +50,7 @@ pub fn sqlite_migrations() -> Migrations<'static> {
])
}
#[must_use]
pub fn postgres_runner() -> refinery::Runner {
postgres_migrations::migrations::runner()
}

View file

@ -21,11 +21,7 @@ impl PluginLoader {
}
/// Discover all plugins in configured directories
///
/// # Errors
///
/// Returns an error if a plugin directory cannot be searched.
pub fn discover_plugins(&self) -> Result<Vec<PluginManifest>> {
pub fn discover_plugins(&self) -> Vec<PluginManifest> {
let mut manifests = Vec::new();
for dir in &self.plugin_dirs {
@ -41,7 +37,7 @@ impl PluginLoader {
manifests.extend(found);
}
Ok(manifests)
manifests
}
/// Discover plugins in a specific directory
@ -271,7 +267,7 @@ impl PluginLoader {
///
/// Returns an error if the path does not exist, is missing `plugin.toml`,
/// the WASM binary is not found, or the WASM file is invalid.
pub fn validate_plugin_package(&self, path: &Path) -> Result<()> {
pub fn validate_plugin_package(path: &Path) -> Result<()> {
// Check that the path exists
if !path.exists() {
return Err(anyhow!("Plugin path does not exist: {}", path.display()));
@ -339,7 +335,7 @@ mod tests {
let temp_dir = TempDir::new().unwrap();
let loader = PluginLoader::new(vec![temp_dir.path().to_path_buf()]);
let manifests = loader.discover_plugins().unwrap();
let manifests = loader.discover_plugins();
assert_eq!(manifests.len(), 0);
}
@ -367,7 +363,7 @@ wasm = "plugin.wasm"
.unwrap();
let loader = PluginLoader::new(vec![temp_dir.path().to_path_buf()]);
let manifests = loader.discover_plugins().unwrap();
let manifests = loader.discover_plugins();
assert_eq!(manifests.len(), 1);
assert_eq!(manifests[0].plugin.name, "test-plugin");
@ -392,17 +388,15 @@ wasm = "plugin.wasm"
"#;
std::fs::write(plugin_dir.join("plugin.toml"), manifest_content).unwrap();
let loader = PluginLoader::new(vec![]);
// Should fail without WASM file
assert!(loader.validate_plugin_package(&plugin_dir).is_err());
assert!(PluginLoader::validate_plugin_package(&plugin_dir).is_err());
// Create valid WASM file (magic number only)
std::fs::write(plugin_dir.join("plugin.wasm"), b"\0asm\x01\x00\x00\x00")
.unwrap();
// Should succeed now
assert!(loader.validate_plugin_package(&plugin_dir).is_ok());
assert!(PluginLoader::validate_plugin_package(&plugin_dir).is_ok());
}
#[test]
@ -426,7 +420,6 @@ wasm = "plugin.wasm"
// Create invalid WASM file
std::fs::write(plugin_dir.join("plugin.wasm"), b"not wasm").unwrap();
let loader = PluginLoader::new(vec![]);
assert!(loader.validate_plugin_package(&plugin_dir).is_err());
assert!(PluginLoader::validate_plugin_package(&plugin_dir).is_err());
}
}

View file

@ -144,7 +144,7 @@ impl PluginManager {
pub async fn discover_and_load_all(&self) -> Result<Vec<String>> {
info!("Discovering plugins from {:?}", self.config.plugin_dirs);
let manifests = self.loader.discover_plugins()?;
let manifests = self.loader.discover_plugins();
let ordered = Self::resolve_load_order(&manifests);
let mut loaded_plugins = Vec::new();
@ -645,6 +645,7 @@ impl PluginManager {
},
}
}
drop(registry);
pages
}
@ -666,6 +667,7 @@ impl PluginManager {
merged.insert(k.clone(), v.clone());
}
}
drop(registry);
merged
}
@ -686,6 +688,7 @@ impl PluginManager {
widgets.push((plugin.id.clone(), widget.clone()));
}
}
drop(registry);
widgets
}

View file

@ -561,9 +561,7 @@ impl HostFunctions {
if let Some(ref allowed) =
caller.data().context.capabilities.network.allowed_domains
{
let parsed = if let Ok(u) = url::Url::parse(&url_str) {
u
} else {
let Ok(parsed) = url::Url::parse(&url_str) else {
tracing::warn!(url = %url_str, "plugin provided invalid URL");
return -1;
};
@ -717,15 +715,12 @@ impl HostFunctions {
return -2;
}
match std::env::var(&key_str) {
Ok(value) => {
let bytes = value.into_bytes();
let len = i32::try_from(bytes.len()).unwrap_or(i32::MAX);
caller.data_mut().exchange_buffer = bytes;
len
},
Err(_) => -1,
}
std::env::var(&key_str).map_or(-1, |value| {
let bytes = value.into_bytes();
let len = i32::try_from(bytes.len()).unwrap_or(i32::MAX);
caller.data_mut().exchange_buffer = bytes;
len
})
},
)?;

View file

@ -242,7 +242,6 @@ impl CapabilityEnforcer {
/// bugs from calling wrong functions on plugins. Returns `true` if allowed.
#[must_use]
pub fn validate_function_call(
&self,
plugin_kinds: &[String],
function_name: &str,
) -> bool {
@ -423,51 +422,91 @@ mod tests {
#[test]
fn test_validate_function_call_lifecycle_always_allowed() {
let enforcer = CapabilityEnforcer::new();
let kinds = vec!["metadata_extractor".to_string()];
assert!(enforcer.validate_function_call(&kinds, "initialize"));
assert!(enforcer.validate_function_call(&kinds, "shutdown"));
assert!(enforcer.validate_function_call(&kinds, "health_check"));
assert!(CapabilityEnforcer::validate_function_call(
&kinds,
"initialize"
));
assert!(CapabilityEnforcer::validate_function_call(
&kinds, "shutdown"
));
assert!(CapabilityEnforcer::validate_function_call(
&kinds,
"health_check"
));
}
#[test]
fn test_validate_function_call_metadata_extractor() {
let enforcer = CapabilityEnforcer::new();
let kinds = vec!["metadata_extractor".to_string()];
assert!(enforcer.validate_function_call(&kinds, "extract_metadata"));
assert!(enforcer.validate_function_call(&kinds, "supported_types"));
assert!(!enforcer.validate_function_call(&kinds, "search"));
assert!(!enforcer.validate_function_call(&kinds, "generate_thumbnail"));
assert!(!enforcer.validate_function_call(&kinds, "can_handle"));
assert!(CapabilityEnforcer::validate_function_call(
&kinds,
"extract_metadata"
));
assert!(CapabilityEnforcer::validate_function_call(
&kinds,
"supported_types"
));
assert!(!CapabilityEnforcer::validate_function_call(
&kinds, "search"
));
assert!(!CapabilityEnforcer::validate_function_call(
&kinds,
"generate_thumbnail"
));
assert!(!CapabilityEnforcer::validate_function_call(
&kinds,
"can_handle"
));
}
#[test]
fn test_validate_function_call_multi_kind() {
let enforcer = CapabilityEnforcer::new();
let kinds =
vec!["media_type".to_string(), "metadata_extractor".to_string()];
assert!(enforcer.validate_function_call(&kinds, "can_handle"));
assert!(enforcer.validate_function_call(&kinds, "supported_media_types"));
assert!(enforcer.validate_function_call(&kinds, "extract_metadata"));
assert!(!enforcer.validate_function_call(&kinds, "search"));
assert!(CapabilityEnforcer::validate_function_call(
&kinds,
"can_handle"
));
assert!(CapabilityEnforcer::validate_function_call(
&kinds,
"supported_media_types"
));
assert!(CapabilityEnforcer::validate_function_call(
&kinds,
"extract_metadata"
));
assert!(!CapabilityEnforcer::validate_function_call(
&kinds, "search"
));
}
#[test]
fn test_validate_function_call_unknown_function() {
let enforcer = CapabilityEnforcer::new();
let kinds = vec!["metadata_extractor".to_string()];
assert!(!enforcer.validate_function_call(&kinds, "unknown_func"));
assert!(!enforcer.validate_function_call(&kinds, ""));
assert!(!CapabilityEnforcer::validate_function_call(
&kinds,
"unknown_func"
));
assert!(!CapabilityEnforcer::validate_function_call(&kinds, ""));
}
#[test]
fn test_validate_function_call_shared_supported_types() {
let enforcer = CapabilityEnforcer::new();
let extractor = vec!["metadata_extractor".to_string()];
let generator = vec!["thumbnail_generator".to_string()];
let search = vec!["search_backend".to_string()];
assert!(enforcer.validate_function_call(&extractor, "supported_types"));
assert!(enforcer.validate_function_call(&generator, "supported_types"));
assert!(!enforcer.validate_function_call(&search, "supported_types"));
assert!(CapabilityEnforcer::validate_function_call(
&extractor,
"supported_types"
));
assert!(CapabilityEnforcer::validate_function_call(
&generator,
"supported_types"
));
assert!(!CapabilityEnforcer::validate_function_call(
&search,
"supported_types"
));
}
}

View file

@ -94,8 +94,7 @@ pub const fn resolve_by_mtime(conflict: &ConflictInfo) -> ConflictOutcome {
}
},
(Some(_), None) => ConflictOutcome::UseLocal,
(None, Some(_)) => ConflictOutcome::UseServer,
(None, None) => ConflictOutcome::UseServer, // Default to server
(None, Some(_) | None) => ConflictOutcome::UseServer, // Default to server
}
}

View file

@ -364,7 +364,7 @@ impl UploadSession {
chunk_count,
status: UploadStatus::Pending,
created_at: now,
expires_at: now + chrono::Duration::hours(timeout_hours as i64),
expires_at: now + chrono::Duration::hours(timeout_hours.cast_signed()),
last_activity: now,
}
}

View file

@ -504,7 +504,7 @@ pub enum UserRole {
impl UserRole {
#[must_use]
pub const fn can_read(self) -> bool {
pub const fn can_read() -> bool {
true
}
@ -533,14 +533,20 @@ impl std::fmt::Display for UserRole {
pub struct PluginTimeoutConfig {
/// Timeout for capability discovery queries (`supported_types`,
/// `interested_events`)
#[serde(default = "default_capability_query_timeout")]
pub capability_query_secs: u64,
#[serde(
default = "default_capability_query_timeout",
rename = "capability_query_secs"
)]
pub capability_query: u64,
/// Timeout for processing calls (`extract_metadata`, `generate_thumbnail`)
#[serde(default = "default_processing_timeout")]
pub processing_secs: u64,
#[serde(default = "default_processing_timeout", rename = "processing_secs")]
pub processing: u64,
/// Timeout for event handler calls
#[serde(default = "default_event_handler_timeout")]
pub event_handler_secs: u64,
#[serde(
default = "default_event_handler_timeout",
rename = "event_handler_secs"
)]
pub event_handler: u64,
}
const fn default_capability_query_timeout() -> u64 {
@ -558,9 +564,9 @@ const fn default_event_handler_timeout() -> u64 {
impl Default for PluginTimeoutConfig {
fn default() -> Self {
Self {
capability_query_secs: default_capability_query_timeout(),
processing_secs: default_processing_timeout(),
event_handler_secs: default_event_handler_timeout(),
capability_query: default_capability_query_timeout(),
processing: default_processing_timeout(),
event_handler: default_event_handler_timeout(),
}
}
}
@ -1138,7 +1144,7 @@ pub enum StorageBackendType {
impl StorageBackendType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Sqlite => "sqlite",
Self::Postgres => "postgres",

View file

@ -63,7 +63,7 @@ pub enum MediaCategory {
impl BuiltinMediaType {
/// Get the unique, stable ID for this media type.
#[must_use]
pub const fn id(&self) -> &'static str {
pub const fn id(self) -> &'static str {
match self {
Self::Mp3 => "mp3",
Self::Flac => "flac",
@ -100,7 +100,7 @@ impl BuiltinMediaType {
/// Get the display name for this media type
#[must_use]
pub fn name(&self) -> String {
pub fn name(self) -> String {
match self {
Self::Mp3 => "MP3 Audio".to_string(),
Self::Flac => "FLAC Audio".to_string(),
@ -180,7 +180,7 @@ impl BuiltinMediaType {
}
#[must_use]
pub const fn mime_type(&self) -> &'static str {
pub const fn mime_type(self) -> &'static str {
match self {
Self::Mp3 => "audio/mpeg",
Self::Flac => "audio/flac",
@ -216,7 +216,7 @@ impl BuiltinMediaType {
}
#[must_use]
pub const fn category(&self) -> MediaCategory {
pub const fn category(self) -> MediaCategory {
match self {
Self::Mp3
| Self::Flac
@ -246,7 +246,7 @@ impl BuiltinMediaType {
}
#[must_use]
pub const fn extensions(&self) -> &'static [&'static str] {
pub const fn extensions(self) -> &'static [&'static str] {
match self {
Self::Mp3 => &["mp3"],
Self::Flac => &["flac"],
@ -283,7 +283,7 @@ impl BuiltinMediaType {
/// Returns true if this is a RAW image format.
#[must_use]
pub const fn is_raw(&self) -> bool {
pub const fn is_raw(self) -> bool {
matches!(
self,
Self::Cr2 | Self::Nef | Self::Arw | Self::Dng | Self::Orf | Self::Rw2

View file

@ -49,6 +49,10 @@ impl MediaTypeRegistry {
}
/// Register a new media type
///
/// # Errors
///
/// Returns an error if a media type with the same ID is already registered.
pub fn register(&mut self, descriptor: MediaTypeDescriptor) -> Result<()> {
// Check if ID is already registered
if self.types.contains_key(&descriptor.id) {
@ -74,6 +78,10 @@ impl MediaTypeRegistry {
}
/// Unregister a media type
///
/// # Errors
///
/// Returns an error if no media type with the given ID is registered.
pub fn unregister(&mut self, id: &str) -> Result<()> {
let descriptor = self
.types
@ -146,6 +154,10 @@ impl MediaTypeRegistry {
}
/// Unregister all types from a specific plugin
///
/// # Errors
///
/// Returns an error if unregistering any individual type fails.
pub fn unregister_plugin(&mut self, plugin_id: &str) -> Result<usize> {
let type_ids: Vec<String> = self
.types

View file

@ -215,7 +215,7 @@ pub enum CustomFieldType {
impl CustomFieldType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::Number => "number",
@ -262,7 +262,7 @@ pub enum CollectionKind {
impl CollectionKind {
#[must_use]
pub const fn as_str(&self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Manual => "manual",
Self::Virtual => "virtual",