pub mod migrations; pub mod postgres; pub mod sqlite; use std::{path::PathBuf, sync::Arc}; use chrono::{DateTime, Utc}; use rustc_hash::FxHashMap; use uuid::Uuid; use crate::{ analytics::UsageEvent, enrichment::ExternalMetadata, error::Result, model::{ AuditEntry, Collection, CollectionKind, ContentHash, CustomField, ManagedBlob, ManagedStorageStats, MediaId, MediaItem, Pagination, Tag, }, playlists::Playlist, search::{SearchRequest, SearchResults}, social::{Comment, Rating, ShareLink}, subtitles::Subtitle, transcode::{TranscodeSession, TranscodeStatus}, users::UserId, }; /// Statistics about the database. #[derive(Debug, Clone, Default)] pub struct DatabaseStats { pub media_count: u64, pub tag_count: u64, pub collection_count: u64, pub audit_count: u64, pub database_size_bytes: u64, pub backend_name: String, } /// Session data for database-backed session storage. #[derive(Debug, Clone)] pub struct SessionData { pub session_token: String, pub user_id: Option, pub username: String, pub role: String, pub created_at: DateTime, pub expires_at: DateTime, pub last_accessed: DateTime, } #[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>; /// 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; /// Return the total number of media items in the database. async fn count_media(&self) -> Result; /// 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>; /// 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>; /// List media items with pagination (offset and limit). async fn list_media(&self, pagination: &Pagination) -> Result>; /// 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; // Tags /// Create a new tag with an optional parent for hierarchical tagging. async fn create_tag( &self, name: &str, parent_id: Option, ) -> Result; /// Retrieve a tag by its ID. Returns `NotFound` if it does not exist. async fn get_tag(&self, id: Uuid) -> Result; /// List all tags in the database. async fn list_tags(&self) -> Result>; /// 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>; /// Get all descendant tags of a parent tag (recursive). async fn get_tag_descendants(&self, tag_id: Uuid) -> Result>; // 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, kind: CollectionKind, description: Option<&str>, filter_query: Option<&str>, ) -> Result; /// Retrieve a collection by ID. Returns `NotFound` if it does not exist. async fn get_collection(&self, id: Uuid) -> Result; /// List all collections. async fn list_collections(&self) -> Result>; /// 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>; // 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; // 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, pagination: &Pagination, ) -> Result>; // 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>; /// Delete a custom field from a media item by name. async fn delete_custom_field( &self, media_id: MediaId, name: &str, ) -> 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; /// Apply multiple tags to multiple media items. /// Returns the number of new associations created. async fn batch_tag_media( &self, media_ids: &[MediaId], tag_ids: &[Uuid], ) -> Result; // 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>; /// 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, ids: &[MediaId], title: Option<&str>, artist: Option<&str>, album: Option<&str>, genre: Option<&str>, year: Option, description: Option<&str>, ) -> Result; // Saved searches /// Persist a search query so it can be re-executed later. async fn save_search( &self, id: uuid::Uuid, name: &str, query: &str, sort_order: Option<&str>, ) -> Result<()>; /// List all saved searches. async fn list_saved_searches(&self) -> Result>; /// Get a single saved search by ID. async fn get_saved_search( &self, id: uuid::Uuid, ) -> Result; /// 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>>; /// 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>>; // Database management /// Collect aggregate database statistics (counts, size, backend name). async fn database_stats(&self) -> Result; /// 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 /// List all media IDs, optionally filtering to those missing thumbnails. async fn list_media_ids_for_thumbnails( &self, only_missing: bool, ) -> Result>; // Library statistics /// Compute comprehensive library statistics (sizes, counts by type, /// top tags/collections, duplicates). async fn library_statistics(&self) -> Result; // User Management /// List all registered users. async fn list_users(&self) -> Result>; /// Get a user by ID. Returns `NotFound` if no such user exists. async fn get_user( &self, id: crate::users::UserId, ) -> Result; /// Look up a user by username. Returns `NotFound` if not found. async fn get_user_by_username( &self, username: &str, ) -> Result; /// Create a new user with the given credentials and role. async fn create_user( &self, username: &str, password_hash: &str, role: crate::config::UserRole, profile: Option, ) -> Result; /// Update a user's password, role, or profile. Only non-`None` fields /// are applied. async fn update_user( &self, id: crate::users::UserId, password_hash: Option<&str>, role: Option, profile: Option, ) -> Result; /// 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>; /// 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, root_path: &str, ) -> Result<()>; /// Check if a user has access to a specific media item based on library /// permissions. Returns the permission level if access is granted, or an /// error if denied. Admin users (role=admin) bypass library checks and have /// full access. async fn check_library_access( &self, user_id: crate::users::UserId, media_id: crate::model::MediaId, ) -> Result { // Default implementation: get the media item's path and check against // user's library access let media = self.get_media(media_id).await?; // 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. 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 media.path.starts_with(std::path::Path::new(&lib.root_path)) { return Ok(lib.permission); } } Err(crate::error::PinakesError::Authorization(format!( "user {user_id} has no access to media {media_id}" ))) } /// Check if a user has at least read access to a media item async fn has_media_read_access( &self, user_id: crate::users::UserId, media_id: crate::model::MediaId, ) -> Result { match self.check_library_access(user_id, media_id).await { Ok(perm) => Ok(perm.can_read()), Err(_) => Ok(false), } } /// Check if a user has write access to a media item async fn has_media_write_access( &self, user_id: crate::users::UserId, media_id: crate::model::MediaId, ) -> Result { match self.check_library_access(user_id, media_id).await { Ok(perm) => Ok(perm.can_write()), Err(_) => Ok(false), } } /// 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, media_id: MediaId, stars: u8, review: Option<&str>, ) -> Result; /// Get all ratings for a media item. async fn get_media_ratings(&self, media_id: MediaId) -> Result>; /// 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>; /// 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, media_id: MediaId, text: &str, parent_id: Option, ) -> Result; /// Get all comments for a media item. async fn get_media_comments(&self, media_id: MediaId) -> Result>; /// 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>; /// Check whether a media item is in a user's favorites. async fn is_favorite( &self, user_id: UserId, media_id: MediaId, ) -> Result; /// Create a public share link for a media item with optional password /// and expiry. async fn create_share_link( &self, media_id: MediaId, created_by: UserId, token: &str, password_hash: Option<&str>, expires_at: Option>, ) -> Result; /// Look up a share link by its token. Returns `NotFound` if invalid. async fn get_share_link(&self, token: &str) -> Result; /// 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, name: &str, description: Option<&str>, is_public: bool, is_smart: bool, filter_query: Option<&str>, ) -> Result; /// Get a playlist by ID. Returns `NotFound` if it does not exist. async fn get_playlist(&self, id: Uuid) -> Result; /// List playlists, optionally filtered to a specific owner. async fn list_playlists( &self, owner_id: Option, ) -> Result>; /// Update a playlist's name, description, or visibility. /// Only non-`None` fields are applied. async fn update_playlist( &self, id: Uuid, name: Option<&str>, description: Option<&str>, is_public: Option, ) -> Result; /// 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>; /// Move a media item to a new position within a playlist. async fn reorder_playlist( &self, playlist_id: Uuid, media_id: MediaId, 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, user_id: Option, limit: u64, ) -> Result>; /// Get the most-viewed media items with their view counts. async fn get_most_viewed(&self, limit: u64) -> Result>; /// Get media items recently viewed by a user, most recent first. async fn get_recently_viewed( &self, user_id: UserId, limit: u64, ) -> Result>; /// 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>; /// Delete usage events older than the given timestamp. /// Returns the number of events deleted. async fn cleanup_old_events(&self, before: DateTime) -> Result; /// 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>; /// 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>; /// 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; /// List transcoding sessions, optionally filtered to a media item. async fn list_transcode_sessions( &self, media_id: Option, ) -> Result>; /// 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, ) -> Result; /// Create a new session in the database async fn create_session(&self, session: &SessionData) -> Result<()>; /// Get a session by its token, returns None if not found or expired async fn get_session( &self, session_token: &str, ) -> Result>; /// 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, ) -> Result>>; /// 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; /// Delete all expired sessions (where `expires_at` < now) async fn delete_expired_sessions(&self) -> Result; /// List all active sessions (optionally filtered by username) async fn list_active_sessions( &self, username: Option<&str>, ) -> Result>; // Book Management Methods /// Upsert book metadata for a media item async fn upsert_book_metadata( &self, metadata: &crate::model::BookMetadata, ) -> Result<()>; /// Get book metadata for a media item async fn get_book_metadata( &self, media_id: MediaId, ) -> Result>; /// Add an author to a book async fn add_book_author( &self, media_id: MediaId, author: &crate::model::AuthorInfo, ) -> Result<()>; /// Get all authors for a book async fn get_book_authors( &self, media_id: MediaId, ) -> Result>; /// List all distinct authors with book counts async fn list_all_authors( &self, pagination: &Pagination, ) -> Result>; /// List all series with book counts async fn list_series(&self) -> Result>; /// Get all books in a series, ordered by `series_index` async fn get_series_books(&self, series_name: &str) -> Result>; /// Update reading progress for a user and book async fn update_reading_progress( &self, user_id: uuid::Uuid, media_id: MediaId, current_page: i32, ) -> Result<()>; /// Get reading progress for a user and book async fn get_reading_progress( &self, user_id: uuid::Uuid, media_id: MediaId, ) -> Result>; /// Get reading list for a user filtered by status async fn get_reading_list( &self, user_id: uuid::Uuid, status: Option, ) -> Result>; /// Search books with book-specific criteria async fn search_books( &self, isbn: Option<&str>, author: Option<&str>, series: Option<&str>, publisher: Option<&str>, language: Option<&str>, pagination: &Pagination, ) -> Result>; /// Insert a media item that uses managed storage async fn insert_managed_media(&self, item: &MediaItem) -> Result<()>; /// Get or create a managed blob record (for deduplication tracking) async fn get_or_create_blob( &self, hash: &ContentHash, size: u64, mime_type: &str, ) -> Result; /// Get a managed blob by its content hash async fn get_blob(&self, hash: &ContentHash) -> Result>; /// Increment the reference count for a blob async fn increment_blob_ref(&self, hash: &ContentHash) -> Result<()>; /// Decrement the reference count for a blob. Returns true if blob should be /// deleted. async fn decrement_blob_ref(&self, hash: &ContentHash) -> Result; /// Update the `last_verified` timestamp for a blob async fn update_blob_verified(&self, hash: &ContentHash) -> Result<()>; /// List orphaned blobs (`reference_count` = 0) async fn list_orphaned_blobs(&self) -> Result>; /// Delete a blob record async fn delete_blob(&self, hash: &ContentHash) -> Result<()>; /// Get managed storage statistics async fn managed_storage_stats(&self) -> Result; /// Register a new sync device async fn register_device( &self, device: &crate::sync::SyncDevice, token_hash: &str, ) -> Result; /// Get a sync device by ID async fn get_device( &self, id: crate::sync::DeviceId, ) -> Result; /// Get a sync device by its token hash async fn get_device_by_token( &self, token_hash: &str, ) -> Result>; /// List all devices for a user async fn list_user_devices( &self, user_id: UserId, ) -> Result>; /// Update a sync device async fn update_device(&self, device: &crate::sync::SyncDevice) -> Result<()>; /// Delete a sync device async fn delete_device(&self, id: crate::sync::DeviceId) -> Result<()>; /// 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 async fn record_sync_change( &self, change: &crate::sync::SyncLogEntry, ) -> Result<()>; /// Get changes since a cursor position async fn get_changes_since( &self, cursor: i64, limit: u64, ) -> Result>; /// Get the current sync cursor (highest sequence number) async fn get_current_sync_cursor(&self) -> Result; /// Clean up old sync log entries async fn cleanup_old_sync_log(&self, before: DateTime) -> Result; /// Get sync state for a device and path async fn get_device_sync_state( &self, device_id: crate::sync::DeviceId, path: &str, ) -> Result>; /// Insert or update device sync state async fn upsert_device_sync_state( &self, state: &crate::sync::DeviceSyncState, ) -> Result<()>; /// List all pending sync items for a device async fn list_pending_sync( &self, device_id: crate::sync::DeviceId, ) -> Result>; /// Create a new upload session async fn create_upload_session( &self, session: &crate::sync::UploadSession, ) -> Result<()>; /// Get an upload session by ID async fn get_upload_session( &self, id: Uuid, ) -> Result; /// Update an upload session async fn update_upload_session( &self, session: &crate::sync::UploadSession, ) -> Result<()>; /// Record a received chunk async fn record_chunk( &self, upload_id: Uuid, chunk: &crate::sync::ChunkInfo, ) -> Result<()>; /// Get all chunks for an upload async fn get_upload_chunks( &self, upload_id: Uuid, ) -> Result>; /// Clean up expired upload sessions async fn cleanup_expired_uploads(&self) -> Result; /// Record a sync conflict async fn record_conflict( &self, conflict: &crate::sync::SyncConflict, ) -> Result<()>; /// Get unresolved conflicts for a device async fn get_unresolved_conflicts( &self, device_id: crate::sync::DeviceId, ) -> Result>; /// Resolve a conflict async fn resolve_conflict( &self, id: Uuid, resolution: crate::config::ConflictResolution, ) -> Result<()>; /// Create a new share async fn create_share( &self, share: &crate::sharing::Share, ) -> Result; /// Get a share by ID async fn get_share( &self, id: crate::sharing::ShareId, ) -> Result; /// Get a share by its public token async fn get_share_by_token( &self, token: &str, ) -> Result; /// List shares created by a user async fn list_shares_by_owner( &self, owner_id: UserId, pagination: &Pagination, ) -> Result>; /// List shares received by a user async fn list_shares_for_user( &self, user_id: UserId, pagination: &Pagination, ) -> Result>; /// List all shares for a specific target async fn list_shares_for_target( &self, target: &crate::sharing::ShareTarget, ) -> Result>; /// Update a share async fn update_share( &self, share: &crate::sharing::Share, ) -> Result; /// Delete a share async fn delete_share(&self, id: crate::sharing::ShareId) -> Result<()>; /// Record that a share was accessed async fn record_share_access( &self, id: crate::sharing::ShareId, ) -> Result<()>; /// Check share access for a user and target async fn check_share_access( &self, user_id: Option, target: &crate::sharing::ShareTarget, ) -> Result>; /// Get effective permissions for a media item (considering inheritance) async fn get_effective_share_permissions( &self, user_id: Option, media_id: MediaId, ) -> Result>; /// Batch delete shares async fn batch_delete_shares( &self, ids: &[crate::sharing::ShareId], ) -> Result; /// Clean up expired shares async fn cleanup_expired_shares(&self) -> Result; /// Record share activity async fn record_share_activity( &self, activity: &crate::sharing::ShareActivity, ) -> Result<()>; /// Get activity for a share async fn get_share_activity( &self, share_id: crate::sharing::ShareId, pagination: &Pagination, ) -> Result>; /// Create a share notification async fn create_share_notification( &self, notification: &crate::sharing::ShareNotification, ) -> Result<()>; /// Get unread notifications for a user async fn get_unread_notifications( &self, user_id: UserId, ) -> 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). /// 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. async fn rename_media(&self, id: MediaId, new_name: &str) -> Result; /// Move a media item to a new directory. /// For external storage, this actually moves the file on disk. /// For managed storage, this only updates the path in metadata. /// Returns the old path for sync log recording. async fn move_media( &self, id: MediaId, new_directory: &std::path::Path, ) -> Result; /// Batch move multiple media items to a new directory. async fn batch_move_media( &self, ids: &[MediaId], new_directory: &std::path::Path, ) -> Result> { let mut results = Vec::new(); for id in ids { let old_path = self.move_media(*id, new_directory).await?; results.push((*id, old_path)); } Ok(results) } /// Soft delete a media item (set `deleted_at` timestamp). async fn soft_delete_media(&self, id: MediaId) -> Result<()>; /// Restore a soft-deleted media item. async fn restore_media(&self, id: MediaId) -> Result<()>; /// List all soft-deleted media items. async fn list_trash(&self, pagination: &Pagination) -> Result>; /// Permanently delete all items in trash. async fn empty_trash(&self) -> Result; /// Permanently delete items in trash older than the specified date. async fn purge_old_trash(&self, before: DateTime) -> Result; /// Count items in trash. async fn count_trash(&self) -> Result; /// Save extracted markdown links for a media item. /// This replaces any existing links for the source media. async fn save_markdown_links( &self, media_id: MediaId, links: &[crate::model::MarkdownLink], ) -> Result<()>; /// Get outgoing links from a media item. async fn get_outgoing_links( &self, media_id: MediaId, ) -> Result>; /// Get backlinks (incoming links) to a media item. async fn get_backlinks( &self, media_id: MediaId, ) -> Result>; /// Clear all links for a media item. async fn clear_links_for_media(&self, media_id: MediaId) -> Result<()>; /// Get graph data for visualization. /// /// If `center_id` is provided, returns nodes within `depth` hops of that /// node. If `center_id` is None, returns the entire graph (limited by /// internal max). async fn get_graph_data( &self, center_id: Option, depth: u32, ) -> Result; /// 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; /// 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). async fn count_unresolved_links(&self) -> Result; /// Create a backup of the database to the specified path. /// /// Only supported for `SQLite` (uses VACUUM INTO). `PostgreSQL` /// deployments should use `pg_dump` directly; this method returns /// `PinakesError::InvalidOperation` for unsupported backends. async fn backup(&self, _dest: &std::path::Path) -> Result<()> { Err(crate::error::PinakesError::InvalidOperation( "backup not supported for this storage backend; use pg_dump for \ PostgreSQL" .to_string(), )) } } /// Comprehensive library statistics. #[derive(Debug, Clone, Default)] pub struct LibraryStatistics { pub total_media: u64, pub total_size_bytes: u64, pub avg_file_size_bytes: u64, pub media_by_type: Vec<(String, u64)>, pub storage_by_type: Vec<(String, u64)>, pub newest_item: Option, pub oldest_item: Option, pub top_tags: Vec<(String, u64)>, pub top_collections: Vec<(String, u64)>, pub total_tags: u64, pub total_collections: u64, pub total_duplicates: u64, } pub type DynStorageBackend = Arc;