pinakes-core: add backup, session refresh, share permissions restructure, and fix integrity

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I17da1cf8403bd11d2a6ea31138f97e776a6a6964
This commit is contained in:
raf 2026-03-08 00:42:10 +03:00
commit 4e91cb6679
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 3096 additions and 2300 deletions

View file

@ -20,6 +20,7 @@ pub struct ShareId(pub Uuid);
impl ShareId {
/// Creates a new share ID.
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
@ -49,7 +50,8 @@ pub enum ShareTarget {
impl ShareTarget {
/// Returns the type of target being shared.
pub fn target_type(&self) -> &'static str {
#[must_use]
pub const fn target_type(&self) -> &'static str {
match self {
Self::Media { .. } => "media",
Self::Collection { .. } => "collection",
@ -59,7 +61,8 @@ impl ShareTarget {
}
/// Returns the ID of the target being shared.
pub fn target_id(&self) -> Uuid {
#[must_use]
pub const fn target_id(&self) -> Uuid {
match self {
Self::Media { media_id } => media_id.0,
Self::Collection { collection_id } => *collection_id,
@ -91,7 +94,8 @@ pub enum ShareRecipient {
impl ShareRecipient {
/// Returns the type of recipient.
pub fn recipient_type(&self) -> &'static str {
#[must_use]
pub const fn recipient_type(&self) -> &'static str {
match self {
Self::PublicLink { .. } => "public_link",
Self::User { .. } => "user",
@ -101,75 +105,117 @@ impl ShareRecipient {
}
}
/// Read-access permissions granted by a share.
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub struct ShareViewPermissions {
/// Can view the content
pub can_view: bool,
/// Can download the content
pub can_download: bool,
/// Can reshare with others
pub can_reshare: bool,
}
/// Write-access permissions granted by a share.
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub struct ShareMutatePermissions {
/// Can edit the content/metadata
pub can_edit: bool,
/// Can delete the content
pub can_delete: bool,
/// Can add new items (for collections)
pub can_add: bool,
}
/// Permissions granted by a share.
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub struct SharePermissions {
/// Can view the content
pub can_view: bool,
/// Can download the content
pub can_download: bool,
/// Can edit the content/metadata
pub can_edit: bool,
/// Can delete the content
pub can_delete: bool,
/// Can reshare with others
pub can_reshare: bool,
/// Can add new items (for collections)
pub can_add: bool,
#[serde(flatten)]
pub view: ShareViewPermissions,
#[serde(flatten)]
pub mutate: ShareMutatePermissions,
}
impl SharePermissions {
/// Creates a new share with view-only permissions.
#[must_use]
pub fn view_only() -> Self {
Self {
can_view: true,
..Default::default()
view: ShareViewPermissions {
can_view: true,
..Default::default()
},
mutate: ShareMutatePermissions::default(),
}
}
/// Creates a new share with download permissions.
#[must_use]
pub fn download() -> Self {
Self {
can_view: true,
can_download: true,
..Default::default()
view: ShareViewPermissions {
can_view: true,
can_download: true,
..Default::default()
},
mutate: ShareMutatePermissions::default(),
}
}
/// Creates a new share with edit permissions.
#[must_use]
pub fn edit() -> Self {
Self {
can_view: true,
can_download: true,
can_edit: true,
can_add: true,
..Default::default()
view: ShareViewPermissions {
can_view: true,
can_download: true,
..Default::default()
},
mutate: ShareMutatePermissions {
can_edit: true,
can_add: true,
..Default::default()
},
}
}
/// Creates a new share with full permissions.
pub fn full() -> Self {
#[must_use]
pub const fn full() -> Self {
Self {
can_view: true,
can_download: true,
can_edit: true,
can_delete: true,
can_reshare: true,
can_add: true,
view: ShareViewPermissions {
can_view: true,
can_download: true,
can_reshare: true,
},
mutate: ShareMutatePermissions {
can_edit: true,
can_delete: true,
can_add: true,
},
}
}
/// Merges two permission sets, taking the most permissive values.
pub fn merge(&self, other: &Self) -> Self {
#[must_use]
pub const fn merge(&self, other: &Self) -> Self {
Self {
can_view: self.can_view || other.can_view,
can_download: self.can_download || other.can_download,
can_edit: self.can_edit || other.can_edit,
can_delete: self.can_delete || other.can_delete,
can_reshare: self.can_reshare || other.can_reshare,
can_add: self.can_add || other.can_add,
view: ShareViewPermissions {
can_view: self.view.can_view || other.view.can_view,
can_download: self.view.can_download || other.view.can_download,
can_reshare: self.view.can_reshare || other.view.can_reshare,
},
mutate: ShareMutatePermissions {
can_edit: self.mutate.can_edit || other.mutate.can_edit,
can_delete: self.mutate.can_delete || other.mutate.can_delete,
can_add: self.mutate.can_add || other.mutate.can_add,
},
}
}
}
@ -196,6 +242,7 @@ pub struct Share {
impl Share {
/// Create a new public link share.
#[must_use]
pub fn new_public_link(
owner_id: UserId,
target: ShareTarget,
@ -224,6 +271,7 @@ impl Share {
}
/// Create a new user share.
#[must_use]
pub fn new_user_share(
owner_id: UserId,
target: ShareTarget,
@ -251,16 +299,19 @@ impl Share {
}
/// Checks if the share has expired.
#[must_use]
pub fn is_expired(&self) -> bool {
self.expires_at.map(|exp| exp < Utc::now()).unwrap_or(false)
self.expires_at.is_some_and(|exp| exp < Utc::now())
}
/// Checks if this is a public link share.
pub fn is_public(&self) -> bool {
#[must_use]
pub const fn is_public(&self) -> bool {
matches!(self.recipient, ShareRecipient::PublicLink { .. })
}
/// Returns the public token if this is a public link share.
#[must_use]
pub fn public_token(&self) -> Option<&str> {
match &self.recipient {
ShareRecipient::PublicLink { token, .. } => Some(token),
@ -308,7 +359,7 @@ impl std::str::FromStr for ShareActivityAction {
"revoked" => Ok(Self::Revoked),
"expired" => Ok(Self::Expired),
"password_failed" => Ok(Self::PasswordFailed),
_ => Err(format!("unknown share activity action: {}", s)),
_ => Err(format!("unknown share activity action: {s}")),
}
}
}
@ -327,6 +378,7 @@ pub struct ShareActivity {
impl ShareActivity {
/// Creates a new share activity entry.
#[must_use]
pub fn new(share_id: ShareId, action: ShareActivityAction) -> Self {
Self {
id: Uuid::now_v7(),
@ -340,18 +392,21 @@ impl ShareActivity {
}
/// Sets the actor who performed the activity.
pub fn with_actor(mut self, actor_id: UserId) -> Self {
#[must_use]
pub const fn with_actor(mut self, actor_id: UserId) -> Self {
self.actor_id = Some(actor_id);
self
}
/// Sets the IP address of the actor.
#[must_use]
pub fn with_ip(mut self, ip: &str) -> Self {
self.actor_ip = Some(ip.to_string());
self
}
/// Sets additional details about the activity.
#[must_use]
pub fn with_details(mut self, details: &str) -> Self {
self.details = Some(details.to_string());
self
@ -391,7 +446,7 @@ impl std::str::FromStr for ShareNotificationType {
"share_revoked" => Ok(Self::ShareRevoked),
"share_expiring" => Ok(Self::ShareExpiring),
"share_accessed" => Ok(Self::ShareAccessed),
_ => Err(format!("unknown share notification type: {}", s)),
_ => Err(format!("unknown share notification type: {s}")),
}
}
}
@ -409,6 +464,7 @@ pub struct ShareNotification {
impl ShareNotification {
/// Creates a new share notification.
#[must_use]
pub fn new(
user_id: UserId,
share_id: ShareId,
@ -426,17 +482,23 @@ impl ShareNotification {
}
/// Generates a random share token.
#[must_use]
pub fn generate_share_token() -> String {
// Use UUIDv4 for random tokens - simple string representation
Uuid::new_v4().simple().to_string()
}
/// Hashes a share password using Argon2id.
///
/// # Errors
///
/// Returns an error if hashing fails.
pub fn hash_share_password(password: &str) -> Result<String, PinakesError> {
crate::users::auth::hash_password(password)
}
/// Verifies a share password against an Argon2id hash.
#[must_use]
pub fn verify_share_password(password: &str, hash: &str) -> bool {
crate::users::auth::verify_password(password, hash).unwrap_or(false)
}

View file

@ -11,7 +11,19 @@ use crate::{
analytics::UsageEvent,
enrichment::ExternalMetadata,
error::Result,
model::*,
model::{
AuditEntry,
Collection,
CollectionKind,
ContentHash,
CustomField,
ManagedBlob,
ManagedStorageStats,
MediaId,
MediaItem,
Pagination,
Tag,
},
playlists::Playlist,
search::{SearchRequest, SearchResults},
social::{Comment, Rating, ShareLink},
@ -46,47 +58,99 @@ pub struct SessionData {
#[async_trait::async_trait]
pub trait StorageBackend: Send + Sync + 'static {
// Migrations
/// Apply all pending database migrations.
/// Called on server startup to ensure the schema is up to date.
async fn run_migrations(&self) -> Result<()>;
// Root directories
/// Register a root directory for media scanning.
async fn add_root_dir(&self, path: PathBuf) -> Result<()>;
/// List all registered root directories.
async fn list_root_dirs(&self) -> Result<Vec<PathBuf>>;
/// Remove a root directory registration.
/// Does not delete any media items found under this directory.
async fn remove_root_dir(&self, path: &std::path::Path) -> Result<()>;
// Media CRUD
/// Insert a new media item into the database.
/// Returns `Database` error if an item with the same ID already exists.
async fn insert_media(&self, item: &MediaItem) -> Result<()>;
/// Retrieve a media item by its ID.
/// Returns `NotFound` if no item exists with the given ID.
async fn get_media(&self, id: MediaId) -> Result<MediaItem>;
/// Return the total number of media items in the database.
async fn count_media(&self) -> Result<u64>;
/// Look up a media item by its content hash.
/// Returns `None` if no item matches the hash.
async fn get_media_by_hash(
&self,
hash: &ContentHash,
) -> Result<Option<MediaItem>>;
/// Get a media item by its file path (used for incremental scanning)
/// Get a media item by its file path (used for incremental scanning).
/// Returns `None` if no item exists at the given path.
async fn get_media_by_path(
&self,
path: &std::path::Path,
) -> Result<Option<MediaItem>>;
/// List media items with pagination (offset and limit).
async fn list_media(&self, pagination: &Pagination)
-> Result<Vec<MediaItem>>;
/// Update an existing media item's metadata.
/// Returns `NotFound` if the item does not exist.
async fn update_media(&self, item: &MediaItem) -> Result<()>;
/// Permanently delete a media item by ID.
/// Returns `NotFound` if the item does not exist.
async fn delete_media(&self, id: MediaId) -> Result<()>;
/// Delete all media items from the database. Returns the number deleted.
async fn delete_all_media(&self) -> Result<u64>;
// Tags
/// Create a new tag with an optional parent for hierarchical tagging.
async fn create_tag(
&self,
name: &str,
parent_id: Option<Uuid>,
) -> Result<Tag>;
/// Retrieve a tag by its ID. Returns `NotFound` if it does not exist.
async fn get_tag(&self, id: Uuid) -> Result<Tag>;
/// List all tags in the database.
async fn list_tags(&self) -> Result<Vec<Tag>>;
/// Delete a tag by ID. Also removes all media-tag associations.
async fn delete_tag(&self, id: Uuid) -> Result<()>;
/// Associate a tag with a media item. No-op if already tagged.
async fn tag_media(&self, media_id: MediaId, tag_id: Uuid) -> Result<()>;
/// Remove a tag association from a media item.
async fn untag_media(&self, media_id: MediaId, tag_id: Uuid) -> Result<()>;
/// Get all tags associated with a media item.
async fn get_media_tags(&self, media_id: MediaId) -> Result<Vec<Tag>>;
/// Get all descendant tags of a parent tag (recursive).
async fn get_tag_descendants(&self, tag_id: Uuid) -> Result<Vec<Tag>>;
// Collections
/// Create a new collection. Smart collections use `filter_query` for
/// automatic membership; manual collections use explicit add/remove.
async fn create_collection(
&self,
name: &str,
@ -94,30 +158,49 @@ pub trait StorageBackend: Send + Sync + 'static {
description: Option<&str>,
filter_query: Option<&str>,
) -> Result<Collection>;
/// Retrieve a collection by ID. Returns `NotFound` if it does not exist.
async fn get_collection(&self, id: Uuid) -> Result<Collection>;
/// List all collections.
async fn list_collections(&self) -> Result<Vec<Collection>>;
/// Delete a collection and all its membership entries.
async fn delete_collection(&self, id: Uuid) -> Result<()>;
/// Add a media item to a manual collection at the given position.
async fn add_to_collection(
&self,
collection_id: Uuid,
media_id: MediaId,
position: i32,
) -> Result<()>;
/// Remove a media item from a collection.
async fn remove_from_collection(
&self,
collection_id: Uuid,
media_id: MediaId,
) -> Result<()>;
/// Get all media items in a collection, ordered by position.
async fn get_collection_members(
&self,
collection_id: Uuid,
) -> Result<Vec<MediaItem>>;
// Search
/// Execute a full-text search with filters, sorting, and pagination.
/// Uses FTS5 on `SQLite` and tsvector/trigram on `PostgreSQL`.
async fn search(&self, request: &SearchRequest) -> Result<SearchResults>;
// Audit
/// Record an audit log entry for an action on a media item.
async fn record_audit(&self, entry: &AuditEntry) -> Result<()>;
/// List audit entries, optionally filtered to a specific media item.
async fn list_audit_entries(
&self,
media_id: Option<MediaId>,
@ -125,16 +208,22 @@ pub trait StorageBackend: Send + Sync + 'static {
) -> Result<Vec<AuditEntry>>;
// Custom fields
/// Set a custom field on a media item (upserts if the field name exists).
async fn set_custom_field(
&self,
media_id: MediaId,
name: &str,
field: &CustomField,
) -> Result<()>;
/// Get all custom fields for a media item, keyed by field name.
async fn get_custom_fields(
&self,
media_id: MediaId,
) -> Result<std::collections::HashMap<String, CustomField>>;
/// Delete a custom field from a media item by name.
async fn delete_custom_field(
&self,
media_id: MediaId,
@ -142,8 +231,13 @@ pub trait StorageBackend: Send + Sync + 'static {
) -> Result<()>;
// Batch operations (transactional where supported)
/// Delete multiple media items in a single transaction.
/// Returns the number of items actually deleted.
async fn batch_delete_media(&self, ids: &[MediaId]) -> Result<u64>;
/// Apply multiple tags to multiple media items.
/// Returns the number of new associations created.
async fn batch_tag_media(
&self,
media_ids: &[MediaId],
@ -151,11 +245,15 @@ pub trait StorageBackend: Send + Sync + 'static {
) -> Result<u64>;
// Integrity
/// List all media item IDs, paths, and content hashes.
/// Used by integrity checks to verify files still exist on disk.
async fn list_media_paths(
&self,
) -> Result<Vec<(MediaId, std::path::PathBuf, ContentHash)>>;
// Batch metadata update (must be implemented per backend for bulk SQL)
/// Update metadata fields on multiple media items at once.
/// Only non-`None` fields are applied. Returns the number updated.
#[allow(clippy::too_many_arguments)]
async fn batch_update_media(
&self,
@ -169,6 +267,8 @@ pub trait StorageBackend: Send + Sync + 'static {
) -> Result<u64>;
// Saved searches
/// Persist a search query so it can be re-executed later.
async fn save_search(
&self,
id: uuid::Uuid,
@ -176,20 +276,41 @@ pub trait StorageBackend: Send + Sync + 'static {
query: &str,
sort_order: Option<&str>,
) -> Result<()>;
/// List all saved searches.
async fn list_saved_searches(&self)
-> Result<Vec<crate::model::SavedSearch>>;
/// Get a single saved search by ID.
async fn get_saved_search(
&self,
id: uuid::Uuid,
) -> Result<crate::model::SavedSearch>;
/// Delete a saved search by ID.
async fn delete_saved_search(&self, id: uuid::Uuid) -> Result<()>;
// Duplicates
/// Find groups of media items with identical content hashes.
async fn find_duplicates(&self) -> Result<Vec<Vec<MediaItem>>>;
/// Find groups of visually similar media using perceptual hashing.
/// `threshold` is the maximum Hamming distance to consider a match.
async fn find_perceptual_duplicates(
&self,
threshold: u32,
) -> Result<Vec<Vec<MediaItem>>>;
// Database management
/// Collect aggregate database statistics (counts, size, backend name).
async fn database_stats(&self) -> Result<DatabaseStats>;
/// Reclaim unused disk space (VACUUM for `SQLite`, VACUUM FULL for Postgres).
async fn vacuum(&self) -> Result<()>;
/// Delete all data from all tables. Destructive; used in tests.
async fn clear_all_data(&self) -> Result<()>;
// Thumbnail helpers
@ -200,18 +321,29 @@ pub trait StorageBackend: Send + Sync + 'static {
) -> Result<Vec<crate::model::MediaId>>;
// Library statistics
/// Compute comprehensive library statistics (sizes, counts by type,
/// top tags/collections, duplicates).
async fn library_statistics(&self) -> Result<LibraryStatistics>;
// User Management
/// List all registered users.
async fn list_users(&self) -> Result<Vec<crate::users::User>>;
/// Get a user by ID. Returns `NotFound` if no such user exists.
async fn get_user(
&self,
id: crate::users::UserId,
) -> Result<crate::users::User>;
/// Look up a user by username. Returns `NotFound` if not found.
async fn get_user_by_username(
&self,
username: &str,
) -> Result<crate::users::User>;
/// Create a new user with the given credentials and role.
async fn create_user(
&self,
username: &str,
@ -219,6 +351,9 @@ pub trait StorageBackend: Send + Sync + 'static {
role: crate::config::UserRole,
profile: Option<crate::users::UserProfile>,
) -> Result<crate::users::User>;
/// Update a user's password, role, or profile. Only non-`None` fields
/// are applied.
async fn update_user(
&self,
id: crate::users::UserId,
@ -226,17 +361,25 @@ pub trait StorageBackend: Send + Sync + 'static {
role: Option<crate::config::UserRole>,
profile: Option<crate::users::UserProfile>,
) -> Result<crate::users::User>;
/// Delete a user and all associated sessions.
async fn delete_user(&self, id: crate::users::UserId) -> Result<()>;
/// Get the library access grants for a user.
async fn get_user_libraries(
&self,
user_id: crate::users::UserId,
) -> Result<Vec<crate::users::UserLibraryAccess>>;
/// Grant a user access to a library root path with the given permission.
async fn grant_library_access(
&self,
user_id: crate::users::UserId,
root_path: &str,
permission: crate::users::LibraryPermission,
) -> Result<()>;
/// Revoke a user's access to a library root path.
async fn revoke_library_access(
&self,
user_id: crate::users::UserId,
@ -255,23 +398,21 @@ pub trait StorageBackend: Send + Sync + 'static {
// Default implementation: get the media item's path and check against
// user's library access
let media = self.get_media(media_id).await?;
let path_str = media.path.to_string_lossy().to_string();
// Get user's library permissions
let libraries = self.get_user_libraries(user_id).await?;
// If user has no library restrictions, they have no access (unless they're
// admin) This default impl requires at least one matching library
// permission
// permission. Use Path::starts_with for component-wise matching to prevent
// prefix collisions (e.g. /data/user1 vs /data/user1-other).
for lib in &libraries {
if path_str.starts_with(&lib.root_path) {
if media.path.starts_with(std::path::Path::new(&lib.root_path)) {
return Ok(lib.permission);
}
}
Err(crate::error::PinakesError::Authorization(format!(
"user {} has no access to media {}",
user_id, media_id
"user {user_id} has no access to media {media_id}"
)))
}
@ -299,6 +440,8 @@ pub trait StorageBackend: Send + Sync + 'static {
}
}
/// Rate a media item (1-5 stars) with an optional text review.
/// Upserts: replaces any existing rating by the same user.
async fn rate_media(
&self,
user_id: UserId,
@ -306,14 +449,21 @@ pub trait StorageBackend: Send + Sync + 'static {
stars: u8,
review: Option<&str>,
) -> Result<Rating>;
/// Get all ratings for a media item.
async fn get_media_ratings(&self, media_id: MediaId) -> Result<Vec<Rating>>;
/// Get a specific user's rating for a media item, if any.
async fn get_user_rating(
&self,
user_id: UserId,
media_id: MediaId,
) -> Result<Option<Rating>>;
/// Delete a rating by ID.
async fn delete_rating(&self, id: Uuid) -> Result<()>;
/// Add a comment on a media item, optionally as a reply to another comment.
async fn add_comment(
&self,
user_id: UserId,
@ -321,31 +471,44 @@ pub trait StorageBackend: Send + Sync + 'static {
text: &str,
parent_id: Option<Uuid>,
) -> Result<Comment>;
/// Get all comments for a media item.
async fn get_media_comments(&self, media_id: MediaId)
-> Result<Vec<Comment>>;
/// Delete a comment by ID.
async fn delete_comment(&self, id: Uuid) -> Result<()>;
/// Mark a media item as a favorite for a user.
async fn add_favorite(
&self,
user_id: UserId,
media_id: MediaId,
) -> Result<()>;
/// Remove a media item from a user's favorites.
async fn remove_favorite(
&self,
user_id: UserId,
media_id: MediaId,
) -> Result<()>;
/// Get a user's favorited media items with pagination.
async fn get_user_favorites(
&self,
user_id: UserId,
pagination: &Pagination,
) -> Result<Vec<MediaItem>>;
/// Check whether a media item is in a user's favorites.
async fn is_favorite(
&self,
user_id: UserId,
media_id: MediaId,
) -> Result<bool>;
/// Create a public share link for a media item with optional password
/// and expiry.
async fn create_share_link(
&self,
media_id: MediaId,
@ -354,10 +517,17 @@ pub trait StorageBackend: Send + Sync + 'static {
password_hash: Option<&str>,
expires_at: Option<DateTime<Utc>>,
) -> Result<ShareLink>;
/// Look up a share link by its token. Returns `NotFound` if invalid.
async fn get_share_link(&self, token: &str) -> Result<ShareLink>;
/// Increment the view counter for a share link.
async fn increment_share_views(&self, token: &str) -> Result<()>;
/// Delete a share link by ID.
async fn delete_share_link(&self, id: Uuid) -> Result<()>;
/// Create a new playlist. Smart playlists auto-populate via `filter_query`.
async fn create_playlist(
&self,
owner_id: UserId,
@ -367,11 +537,18 @@ pub trait StorageBackend: Send + Sync + 'static {
is_smart: bool,
filter_query: Option<&str>,
) -> Result<Playlist>;
/// Get a playlist by ID. Returns `NotFound` if it does not exist.
async fn get_playlist(&self, id: Uuid) -> Result<Playlist>;
/// List playlists, optionally filtered to a specific owner.
async fn list_playlists(
&self,
owner_id: Option<UserId>,
) -> Result<Vec<Playlist>>;
/// Update a playlist's name, description, or visibility.
/// Only non-`None` fields are applied.
async fn update_playlist(
&self,
id: Uuid,
@ -379,22 +556,32 @@ pub trait StorageBackend: Send + Sync + 'static {
description: Option<&str>,
is_public: Option<bool>,
) -> Result<Playlist>;
/// Delete a playlist and all its item associations.
async fn delete_playlist(&self, id: Uuid) -> Result<()>;
/// Add a media item to a playlist at the given position.
async fn add_to_playlist(
&self,
playlist_id: Uuid,
media_id: MediaId,
position: i32,
) -> Result<()>;
/// Remove a media item from a playlist.
async fn remove_from_playlist(
&self,
playlist_id: Uuid,
media_id: MediaId,
) -> Result<()>;
/// Get all media items in a playlist, ordered by position.
async fn get_playlist_items(
&self,
playlist_id: Uuid,
) -> Result<Vec<MediaItem>>;
/// Move a media item to a new position within a playlist.
async fn reorder_playlist(
&self,
playlist_id: Uuid,
@ -402,69 +589,106 @@ pub trait StorageBackend: Send + Sync + 'static {
new_position: i32,
) -> Result<()>;
/// Record a usage/analytics event (play, view, download, etc.).
async fn record_usage_event(&self, event: &UsageEvent) -> Result<()>;
/// Query usage events, optionally filtered by media item and/or user.
async fn get_usage_events(
&self,
media_id: Option<MediaId>,
user_id: Option<UserId>,
limit: u64,
) -> Result<Vec<UsageEvent>>;
/// Get the most-viewed media items with their view counts.
async fn get_most_viewed(&self, limit: u64) -> Result<Vec<(MediaItem, u64)>>;
/// Get media items recently viewed by a user, most recent first.
async fn get_recently_viewed(
&self,
user_id: UserId,
limit: u64,
) -> Result<Vec<MediaItem>>;
/// Update playback/watch progress for a user on a media item (in seconds).
async fn update_watch_progress(
&self,
user_id: UserId,
media_id: MediaId,
progress_secs: f64,
) -> Result<()>;
/// Get the stored watch progress (in seconds) for a user/media pair.
async fn get_watch_progress(
&self,
user_id: UserId,
media_id: MediaId,
) -> Result<Option<f64>>;
/// Delete usage events older than the given timestamp.
/// Returns the number of events deleted.
async fn cleanup_old_events(&self, before: DateTime<Utc>) -> Result<u64>;
/// Add a subtitle track for a media item.
async fn add_subtitle(&self, subtitle: &Subtitle) -> Result<()>;
/// Get all subtitle tracks for a media item.
async fn get_media_subtitles(
&self,
media_id: MediaId,
) -> Result<Vec<Subtitle>>;
/// Delete a subtitle track by ID.
async fn delete_subtitle(&self, id: Uuid) -> Result<()>;
/// Adjust the timing offset (in milliseconds) for a subtitle track.
async fn update_subtitle_offset(
&self,
id: Uuid,
offset_ms: i64,
) -> Result<()>;
/// Store metadata fetched from an external source (e.g., `MusicBrainz`,
/// `TMDb`).
async fn store_external_metadata(
&self,
meta: &ExternalMetadata,
) -> Result<()>;
/// Get all external metadata records for a media item.
async fn get_external_metadata(
&self,
media_id: MediaId,
) -> Result<Vec<ExternalMetadata>>;
/// Delete an external metadata record by ID.
async fn delete_external_metadata(&self, id: Uuid) -> Result<()>;
/// Create a new transcoding session for a media item.
async fn create_transcode_session(
&self,
session: &TranscodeSession,
) -> Result<()>;
/// Get a transcoding session by ID.
async fn get_transcode_session(&self, id: Uuid) -> Result<TranscodeSession>;
/// List transcoding sessions, optionally filtered to a media item.
async fn list_transcode_sessions(
&self,
media_id: Option<MediaId>,
) -> Result<Vec<TranscodeSession>>;
/// Update the status and progress of a transcoding session.
async fn update_transcode_status(
&self,
id: Uuid,
status: TranscodeStatus,
progress: f32,
) -> Result<()>;
/// Delete transcode sessions that expired before the given timestamp.
/// Returns the number of sessions cleaned up.
async fn cleanup_expired_transcodes(
&self,
before: DateTime<Utc>,
@ -479,16 +703,26 @@ pub trait StorageBackend: Send + Sync + 'static {
session_token: &str,
) -> Result<Option<SessionData>>;
/// Update the last_accessed timestamp for a session
/// Update the `last_accessed` timestamp for a session
async fn touch_session(&self, session_token: &str) -> Result<()>;
/// Extend a session's expiry time.
/// Only extends sessions that have not already expired.
/// Returns the new expiry time, or None if the session was not found or
/// already expired.
async fn extend_session(
&self,
session_token: &str,
new_expires_at: DateTime<Utc>,
) -> Result<Option<DateTime<Utc>>>;
/// Delete a specific session
async fn delete_session(&self, session_token: &str) -> Result<()>;
/// Delete all sessions for a specific user
async fn delete_user_sessions(&self, username: &str) -> Result<u64>;
/// Delete all expired sessions (where expires_at < now)
/// Delete all expired sessions (where `expires_at` < now)
async fn delete_expired_sessions(&self) -> Result<u64>;
/// List all active sessions (optionally filtered by username)
@ -533,7 +767,7 @@ pub trait StorageBackend: Send + Sync + 'static {
/// List all series with book counts
async fn list_series(&self) -> Result<Vec<(String, u64)>>;
/// Get all books in a series, ordered by series_index
/// Get all books in a series, ordered by `series_index`
async fn get_series_books(&self, series_name: &str)
-> Result<Vec<MediaItem>>;
@ -591,10 +825,10 @@ pub trait StorageBackend: Send + Sync + 'static {
/// deleted.
async fn decrement_blob_ref(&self, hash: &ContentHash) -> Result<bool>;
/// Update the last_verified timestamp for a blob
/// Update the `last_verified` timestamp for a blob
async fn update_blob_verified(&self, hash: &ContentHash) -> Result<()>;
/// List orphaned blobs (reference_count = 0)
/// List orphaned blobs (`reference_count` = 0)
async fn list_orphaned_blobs(&self) -> Result<Vec<ManagedBlob>>;
/// Delete a blob record
@ -635,7 +869,7 @@ pub trait StorageBackend: Send + Sync + 'static {
/// Delete a sync device
async fn delete_device(&self, id: crate::sync::DeviceId) -> Result<()>;
/// Update the last_seen_at timestamp for a device
/// Update the `last_seen_at` timestamp for a device
async fn touch_device(&self, id: crate::sync::DeviceId) -> Result<()>;
/// Record a change in the sync log
@ -830,13 +1064,19 @@ pub trait StorageBackend: Send + Sync + 'static {
user_id: UserId,
) -> Result<Vec<crate::sharing::ShareNotification>>;
/// Mark a notification as read
async fn mark_notification_read(&self, id: Uuid) -> Result<()>;
/// Mark a notification as read. Scoped to `user_id` to prevent cross-user
/// modification. Silently no-ops if the notification belongs to a different
/// user, which avoids leaking notification existence via error responses.
async fn mark_notification_read(
&self,
id: Uuid,
user_id: UserId,
) -> Result<()>;
/// Mark all notifications as read for a user
async fn mark_all_notifications_read(&self, user_id: UserId) -> Result<()>;
/// Rename a media item (changes file_name and updates path accordingly).
/// Rename a media item (changes `file_name` and updates path accordingly).
/// For external storage, this actually renames the file on disk.
/// For managed storage, this only updates the metadata.
/// Returns the old path for sync log recording.
@ -866,7 +1106,7 @@ pub trait StorageBackend: Send + Sync + 'static {
Ok(results)
}
/// Soft delete a media item (set deleted_at timestamp).
/// Soft delete a media item (set `deleted_at` timestamp).
async fn soft_delete_media(&self, id: MediaId) -> Result<()>;
/// Restore a soft-deleted media item.
@ -919,15 +1159,24 @@ pub trait StorageBackend: Send + Sync + 'static {
depth: u32,
) -> Result<crate::model::GraphData>;
/// Resolve unresolved links by matching target_path against media item paths.
/// Returns the number of links that were resolved.
/// Resolve unresolved links by matching `target_path` against media item
/// paths. Returns the number of links that were resolved.
async fn resolve_links(&self) -> Result<u64>;
/// Update the links_extracted_at timestamp for a media item.
/// Update the `links_extracted_at` timestamp for a media item.
async fn mark_links_extracted(&self, media_id: MediaId) -> Result<()>;
/// Get count of unresolved links (links where target_media_id is NULL).
/// Get count of unresolved links (links where `target_media_id` is NULL).
async fn count_unresolved_links(&self) -> Result<u64>;
/// Create a backup of the database to the specified path.
/// Default implementation returns unsupported; `SQLite` overrides with
/// VACUUM INTO.
async fn backup(&self, _dest: &std::path::Path) -> Result<()> {
Err(crate::error::PinakesError::InvalidOperation(
"backup not supported for this storage backend".to_string(),
))
}
}
/// Comprehensive library statistics.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff