pinakes/crates/pinakes-core/src/storage/mod.rs
NotAShelf 5b817e0b3e
pinakes-core: fix hasher usage in tests
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ied03277d450e39299470667ef479c3526a6a6964
2026-03-22 22:04:41 +03:00

1204 lines
35 KiB
Rust

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<String>,
pub username: String,
pub role: String,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
}
#[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).
/// 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,
kind: CollectionKind,
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>,
pagination: &Pagination,
) -> 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<FxHashMap<String, CustomField>>;
/// 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<u64>;
/// 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<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)>>;
/// 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<i32>,
description: Option<&str>,
) -> Result<u64>;
// 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<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
/// List all media IDs, optionally filtering to those missing thumbnails.
async fn list_media_ids_for_thumbnails(
&self,
only_missing: bool,
) -> 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,
password_hash: &str,
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,
password_hash: Option<&str>,
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,
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<crate::users::LibraryPermission> {
// 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<bool> {
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<bool> {
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<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,
media_id: MediaId,
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,
created_by: UserId,
token: &str,
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,
name: &str,
description: Option<&str>,
is_public: bool,
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,
name: Option<&str>,
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,
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<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>,
) -> Result<u64>;
/// 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<Option<SessionData>>;
/// 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)
async fn delete_expired_sessions(&self) -> Result<u64>;
/// List all active sessions (optionally filtered by username)
async fn list_active_sessions(
&self,
username: Option<&str>,
) -> Result<Vec<SessionData>>;
// 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<Option<crate::model::BookMetadata>>;
/// 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<Vec<crate::model::AuthorInfo>>;
/// List all distinct authors with book counts
async fn list_all_authors(
&self,
pagination: &Pagination,
) -> Result<Vec<(String, u64)>>;
/// 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`
async fn get_series_books(&self, series_name: &str)
-> Result<Vec<MediaItem>>;
/// 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<Option<crate::model::ReadingProgress>>;
/// Get reading list for a user filtered by status
async fn get_reading_list(
&self,
user_id: uuid::Uuid,
status: Option<crate::model::ReadingStatus>,
) -> Result<Vec<MediaItem>>;
/// 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<Vec<MediaItem>>;
/// 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<ManagedBlob>;
/// Get a managed blob by its content hash
async fn get_blob(&self, hash: &ContentHash) -> Result<Option<ManagedBlob>>;
/// 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<bool>;
/// 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<Vec<ManagedBlob>>;
/// Delete a blob record
async fn delete_blob(&self, hash: &ContentHash) -> Result<()>;
/// Get managed storage statistics
async fn managed_storage_stats(&self) -> Result<ManagedStorageStats>;
/// Register a new sync device
async fn register_device(
&self,
device: &crate::sync::SyncDevice,
token_hash: &str,
) -> Result<crate::sync::SyncDevice>;
/// Get a sync device by ID
async fn get_device(
&self,
id: crate::sync::DeviceId,
) -> Result<crate::sync::SyncDevice>;
/// Get a sync device by its token hash
async fn get_device_by_token(
&self,
token_hash: &str,
) -> Result<Option<crate::sync::SyncDevice>>;
/// List all devices for a user
async fn list_user_devices(
&self,
user_id: UserId,
) -> Result<Vec<crate::sync::SyncDevice>>;
/// 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<Vec<crate::sync::SyncLogEntry>>;
/// Get the current sync cursor (highest sequence number)
async fn get_current_sync_cursor(&self) -> Result<i64>;
/// Clean up old sync log entries
async fn cleanup_old_sync_log(&self, before: DateTime<Utc>) -> Result<u64>;
/// Get sync state for a device and path
async fn get_device_sync_state(
&self,
device_id: crate::sync::DeviceId,
path: &str,
) -> Result<Option<crate::sync::DeviceSyncState>>;
/// 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<Vec<crate::sync::DeviceSyncState>>;
/// 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<crate::sync::UploadSession>;
/// 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<Vec<crate::sync::ChunkInfo>>;
/// Clean up expired upload sessions
async fn cleanup_expired_uploads(&self) -> Result<u64>;
/// 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<Vec<crate::sync::SyncConflict>>;
/// 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<crate::sharing::Share>;
/// Get a share by ID
async fn get_share(
&self,
id: crate::sharing::ShareId,
) -> Result<crate::sharing::Share>;
/// Get a share by its public token
async fn get_share_by_token(
&self,
token: &str,
) -> Result<crate::sharing::Share>;
/// List shares created by a user
async fn list_shares_by_owner(
&self,
owner_id: UserId,
pagination: &Pagination,
) -> Result<Vec<crate::sharing::Share>>;
/// List shares received by a user
async fn list_shares_for_user(
&self,
user_id: UserId,
pagination: &Pagination,
) -> Result<Vec<crate::sharing::Share>>;
/// List all shares for a specific target
async fn list_shares_for_target(
&self,
target: &crate::sharing::ShareTarget,
) -> Result<Vec<crate::sharing::Share>>;
/// Update a share
async fn update_share(
&self,
share: &crate::sharing::Share,
) -> Result<crate::sharing::Share>;
/// 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<UserId>,
target: &crate::sharing::ShareTarget,
) -> Result<Option<crate::sharing::SharePermissions>>;
/// Get effective permissions for a media item (considering inheritance)
async fn get_effective_share_permissions(
&self,
user_id: Option<UserId>,
media_id: MediaId,
) -> Result<Option<crate::sharing::SharePermissions>>;
/// Batch delete shares
async fn batch_delete_shares(
&self,
ids: &[crate::sharing::ShareId],
) -> Result<u64>;
/// Clean up expired shares
async fn cleanup_expired_shares(&self) -> Result<u64>;
/// 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<Vec<crate::sharing::ShareActivity>>;
/// 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<Vec<crate::sharing::ShareNotification>>;
/// 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<String>;
/// 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<String>;
/// Batch move multiple media items to a new directory.
async fn batch_move_media(
&self,
ids: &[MediaId],
new_directory: &std::path::Path,
) -> Result<Vec<(MediaId, String)>> {
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<Vec<MediaItem>>;
/// Permanently delete all items in trash.
async fn empty_trash(&self) -> Result<u64>;
/// Permanently delete items in trash older than the specified date.
async fn purge_old_trash(&self, before: DateTime<Utc>) -> Result<u64>;
/// Count items in trash.
async fn count_trash(&self) -> Result<u64>;
/// 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<Vec<crate::model::MarkdownLink>>;
/// Get backlinks (incoming links) to a media item.
async fn get_backlinks(
&self,
media_id: MediaId,
) -> Result<Vec<crate::model::BacklinkInfo>>;
/// 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<MediaId>,
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.
async fn resolve_links(&self) -> Result<u64>;
/// 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<u64>;
/// 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<String>,
pub oldest_item: Option<String>,
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<dyn StorageBackend>;