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:
parent
672e11b592
commit
4e91cb6679
4 changed files with 3096 additions and 2300 deletions
|
|
@ -20,6 +20,7 @@ pub struct ShareId(pub Uuid);
|
||||||
|
|
||||||
impl ShareId {
|
impl ShareId {
|
||||||
/// Creates a new share ID.
|
/// Creates a new share ID.
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self(Uuid::now_v7())
|
Self(Uuid::now_v7())
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +50,8 @@ pub enum ShareTarget {
|
||||||
|
|
||||||
impl ShareTarget {
|
impl ShareTarget {
|
||||||
/// Returns the type of target being shared.
|
/// 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 {
|
match self {
|
||||||
Self::Media { .. } => "media",
|
Self::Media { .. } => "media",
|
||||||
Self::Collection { .. } => "collection",
|
Self::Collection { .. } => "collection",
|
||||||
|
|
@ -59,7 +61,8 @@ impl ShareTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the ID of the target being shared.
|
/// 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 {
|
match self {
|
||||||
Self::Media { media_id } => media_id.0,
|
Self::Media { media_id } => media_id.0,
|
||||||
Self::Collection { collection_id } => *collection_id,
|
Self::Collection { collection_id } => *collection_id,
|
||||||
|
|
@ -91,7 +94,8 @@ pub enum ShareRecipient {
|
||||||
|
|
||||||
impl ShareRecipient {
|
impl ShareRecipient {
|
||||||
/// Returns the type of recipient.
|
/// Returns the type of recipient.
|
||||||
pub fn recipient_type(&self) -> &'static str {
|
#[must_use]
|
||||||
|
pub const fn recipient_type(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::PublicLink { .. } => "public_link",
|
Self::PublicLink { .. } => "public_link",
|
||||||
Self::User { .. } => "user",
|
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.
|
/// Permissions granted by a share.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize,
|
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||||
)]
|
)]
|
||||||
pub struct SharePermissions {
|
pub struct SharePermissions {
|
||||||
/// Can view the content
|
#[serde(flatten)]
|
||||||
pub can_view: bool,
|
pub view: ShareViewPermissions,
|
||||||
/// Can download the content
|
#[serde(flatten)]
|
||||||
pub can_download: bool,
|
pub mutate: ShareMutatePermissions,
|
||||||
/// 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharePermissions {
|
impl SharePermissions {
|
||||||
/// Creates a new share with view-only permissions.
|
/// Creates a new share with view-only permissions.
|
||||||
|
#[must_use]
|
||||||
pub fn view_only() -> Self {
|
pub fn view_only() -> Self {
|
||||||
Self {
|
Self {
|
||||||
can_view: true,
|
view: ShareViewPermissions {
|
||||||
..Default::default()
|
can_view: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
mutate: ShareMutatePermissions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new share with download permissions.
|
/// Creates a new share with download permissions.
|
||||||
|
#[must_use]
|
||||||
pub fn download() -> Self {
|
pub fn download() -> Self {
|
||||||
Self {
|
Self {
|
||||||
can_view: true,
|
view: ShareViewPermissions {
|
||||||
can_download: true,
|
can_view: true,
|
||||||
..Default::default()
|
can_download: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
mutate: ShareMutatePermissions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new share with edit permissions.
|
/// Creates a new share with edit permissions.
|
||||||
|
#[must_use]
|
||||||
pub fn edit() -> Self {
|
pub fn edit() -> Self {
|
||||||
Self {
|
Self {
|
||||||
can_view: true,
|
view: ShareViewPermissions {
|
||||||
can_download: true,
|
can_view: true,
|
||||||
can_edit: true,
|
can_download: true,
|
||||||
can_add: true,
|
..Default::default()
|
||||||
..Default::default()
|
},
|
||||||
|
mutate: ShareMutatePermissions {
|
||||||
|
can_edit: true,
|
||||||
|
can_add: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new share with full permissions.
|
/// Creates a new share with full permissions.
|
||||||
pub fn full() -> Self {
|
#[must_use]
|
||||||
|
pub const fn full() -> Self {
|
||||||
Self {
|
Self {
|
||||||
can_view: true,
|
view: ShareViewPermissions {
|
||||||
can_download: true,
|
can_view: true,
|
||||||
can_edit: true,
|
can_download: true,
|
||||||
can_delete: true,
|
can_reshare: true,
|
||||||
can_reshare: true,
|
},
|
||||||
can_add: true,
|
mutate: ShareMutatePermissions {
|
||||||
|
can_edit: true,
|
||||||
|
can_delete: true,
|
||||||
|
can_add: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merges two permission sets, taking the most permissive values.
|
/// 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 {
|
Self {
|
||||||
can_view: self.can_view || other.can_view,
|
view: ShareViewPermissions {
|
||||||
can_download: self.can_download || other.can_download,
|
can_view: self.view.can_view || other.view.can_view,
|
||||||
can_edit: self.can_edit || other.can_edit,
|
can_download: self.view.can_download || other.view.can_download,
|
||||||
can_delete: self.can_delete || other.can_delete,
|
can_reshare: self.view.can_reshare || other.view.can_reshare,
|
||||||
can_reshare: self.can_reshare || other.can_reshare,
|
},
|
||||||
can_add: self.can_add || other.can_add,
|
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 {
|
impl Share {
|
||||||
/// Create a new public link share.
|
/// Create a new public link share.
|
||||||
|
#[must_use]
|
||||||
pub fn new_public_link(
|
pub fn new_public_link(
|
||||||
owner_id: UserId,
|
owner_id: UserId,
|
||||||
target: ShareTarget,
|
target: ShareTarget,
|
||||||
|
|
@ -224,6 +271,7 @@ impl Share {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new user share.
|
/// Create a new user share.
|
||||||
|
#[must_use]
|
||||||
pub fn new_user_share(
|
pub fn new_user_share(
|
||||||
owner_id: UserId,
|
owner_id: UserId,
|
||||||
target: ShareTarget,
|
target: ShareTarget,
|
||||||
|
|
@ -251,16 +299,19 @@ impl Share {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the share has expired.
|
/// Checks if the share has expired.
|
||||||
|
#[must_use]
|
||||||
pub fn is_expired(&self) -> bool {
|
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.
|
/// 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 { .. })
|
matches!(self.recipient, ShareRecipient::PublicLink { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the public token if this is a public link share.
|
/// Returns the public token if this is a public link share.
|
||||||
|
#[must_use]
|
||||||
pub fn public_token(&self) -> Option<&str> {
|
pub fn public_token(&self) -> Option<&str> {
|
||||||
match &self.recipient {
|
match &self.recipient {
|
||||||
ShareRecipient::PublicLink { token, .. } => Some(token),
|
ShareRecipient::PublicLink { token, .. } => Some(token),
|
||||||
|
|
@ -308,7 +359,7 @@ impl std::str::FromStr for ShareActivityAction {
|
||||||
"revoked" => Ok(Self::Revoked),
|
"revoked" => Ok(Self::Revoked),
|
||||||
"expired" => Ok(Self::Expired),
|
"expired" => Ok(Self::Expired),
|
||||||
"password_failed" => Ok(Self::PasswordFailed),
|
"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 {
|
impl ShareActivity {
|
||||||
/// Creates a new share activity entry.
|
/// Creates a new share activity entry.
|
||||||
|
#[must_use]
|
||||||
pub fn new(share_id: ShareId, action: ShareActivityAction) -> Self {
|
pub fn new(share_id: ShareId, action: ShareActivityAction) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::now_v7(),
|
id: Uuid::now_v7(),
|
||||||
|
|
@ -340,18 +392,21 @@ impl ShareActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the actor who performed the activity.
|
/// 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.actor_id = Some(actor_id);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the IP address of the actor.
|
/// Sets the IP address of the actor.
|
||||||
|
#[must_use]
|
||||||
pub fn with_ip(mut self, ip: &str) -> Self {
|
pub fn with_ip(mut self, ip: &str) -> Self {
|
||||||
self.actor_ip = Some(ip.to_string());
|
self.actor_ip = Some(ip.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets additional details about the activity.
|
/// Sets additional details about the activity.
|
||||||
|
#[must_use]
|
||||||
pub fn with_details(mut self, details: &str) -> Self {
|
pub fn with_details(mut self, details: &str) -> Self {
|
||||||
self.details = Some(details.to_string());
|
self.details = Some(details.to_string());
|
||||||
self
|
self
|
||||||
|
|
@ -391,7 +446,7 @@ impl std::str::FromStr for ShareNotificationType {
|
||||||
"share_revoked" => Ok(Self::ShareRevoked),
|
"share_revoked" => Ok(Self::ShareRevoked),
|
||||||
"share_expiring" => Ok(Self::ShareExpiring),
|
"share_expiring" => Ok(Self::ShareExpiring),
|
||||||
"share_accessed" => Ok(Self::ShareAccessed),
|
"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 {
|
impl ShareNotification {
|
||||||
/// Creates a new share notification.
|
/// Creates a new share notification.
|
||||||
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
share_id: ShareId,
|
share_id: ShareId,
|
||||||
|
|
@ -426,17 +482,23 @@ impl ShareNotification {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a random share token.
|
/// Generates a random share token.
|
||||||
|
#[must_use]
|
||||||
pub fn generate_share_token() -> String {
|
pub fn generate_share_token() -> String {
|
||||||
// Use UUIDv4 for random tokens - simple string representation
|
// Use UUIDv4 for random tokens - simple string representation
|
||||||
Uuid::new_v4().simple().to_string()
|
Uuid::new_v4().simple().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hashes a share password using Argon2id.
|
/// Hashes a share password using Argon2id.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if hashing fails.
|
||||||
pub fn hash_share_password(password: &str) -> Result<String, PinakesError> {
|
pub fn hash_share_password(password: &str) -> Result<String, PinakesError> {
|
||||||
crate::users::auth::hash_password(password)
|
crate::users::auth::hash_password(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies a share password against an Argon2id hash.
|
/// Verifies a share password against an Argon2id hash.
|
||||||
|
#[must_use]
|
||||||
pub fn verify_share_password(password: &str, hash: &str) -> bool {
|
pub fn verify_share_password(password: &str, hash: &str) -> bool {
|
||||||
crate::users::auth::verify_password(password, hash).unwrap_or(false)
|
crate::users::auth::verify_password(password, hash).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,19 @@ use crate::{
|
||||||
analytics::UsageEvent,
|
analytics::UsageEvent,
|
||||||
enrichment::ExternalMetadata,
|
enrichment::ExternalMetadata,
|
||||||
error::Result,
|
error::Result,
|
||||||
model::*,
|
model::{
|
||||||
|
AuditEntry,
|
||||||
|
Collection,
|
||||||
|
CollectionKind,
|
||||||
|
ContentHash,
|
||||||
|
CustomField,
|
||||||
|
ManagedBlob,
|
||||||
|
ManagedStorageStats,
|
||||||
|
MediaId,
|
||||||
|
MediaItem,
|
||||||
|
Pagination,
|
||||||
|
Tag,
|
||||||
|
},
|
||||||
playlists::Playlist,
|
playlists::Playlist,
|
||||||
search::{SearchRequest, SearchResults},
|
search::{SearchRequest, SearchResults},
|
||||||
social::{Comment, Rating, ShareLink},
|
social::{Comment, Rating, ShareLink},
|
||||||
|
|
@ -46,47 +58,99 @@ pub struct SessionData {
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait StorageBackend: Send + Sync + 'static {
|
pub trait StorageBackend: Send + Sync + 'static {
|
||||||
// Migrations
|
// Migrations
|
||||||
|
|
||||||
|
/// Apply all pending database migrations.
|
||||||
|
/// Called on server startup to ensure the schema is up to date.
|
||||||
async fn run_migrations(&self) -> Result<()>;
|
async fn run_migrations(&self) -> Result<()>;
|
||||||
|
|
||||||
// Root directories
|
// Root directories
|
||||||
|
|
||||||
|
/// Register a root directory for media scanning.
|
||||||
async fn add_root_dir(&self, path: PathBuf) -> Result<()>;
|
async fn add_root_dir(&self, path: PathBuf) -> Result<()>;
|
||||||
|
|
||||||
|
/// List all registered root directories.
|
||||||
async fn list_root_dirs(&self) -> Result<Vec<PathBuf>>;
|
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<()>;
|
async fn remove_root_dir(&self, path: &std::path::Path) -> Result<()>;
|
||||||
|
|
||||||
// Media CRUD
|
// 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<()>;
|
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>;
|
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>;
|
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(
|
async fn get_media_by_hash(
|
||||||
&self,
|
&self,
|
||||||
hash: &ContentHash,
|
hash: &ContentHash,
|
||||||
) -> Result<Option<MediaItem>>;
|
) -> 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(
|
async fn get_media_by_path(
|
||||||
&self,
|
&self,
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
) -> Result<Option<MediaItem>>;
|
) -> Result<Option<MediaItem>>;
|
||||||
|
|
||||||
|
/// List media items with pagination (offset and limit).
|
||||||
async fn list_media(&self, pagination: &Pagination)
|
async fn list_media(&self, pagination: &Pagination)
|
||||||
-> Result<Vec<MediaItem>>;
|
-> 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<()>;
|
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<()>;
|
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>;
|
async fn delete_all_media(&self) -> Result<u64>;
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
|
|
||||||
|
/// Create a new tag with an optional parent for hierarchical tagging.
|
||||||
async fn create_tag(
|
async fn create_tag(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
parent_id: Option<Uuid>,
|
parent_id: Option<Uuid>,
|
||||||
) -> Result<Tag>;
|
) -> Result<Tag>;
|
||||||
|
|
||||||
|
/// Retrieve a tag by its ID. Returns `NotFound` if it does not exist.
|
||||||
async fn get_tag(&self, id: Uuid) -> Result<Tag>;
|
async fn get_tag(&self, id: Uuid) -> Result<Tag>;
|
||||||
|
|
||||||
|
/// List all tags in the database.
|
||||||
async fn list_tags(&self) -> Result<Vec<Tag>>;
|
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<()>;
|
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<()>;
|
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<()>;
|
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>>;
|
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>>;
|
async fn get_tag_descendants(&self, tag_id: Uuid) -> Result<Vec<Tag>>;
|
||||||
|
|
||||||
// Collections
|
// Collections
|
||||||
|
|
||||||
|
/// Create a new collection. Smart collections use `filter_query` for
|
||||||
|
/// automatic membership; manual collections use explicit add/remove.
|
||||||
async fn create_collection(
|
async fn create_collection(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|
@ -94,30 +158,49 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
description: Option<&str>,
|
description: Option<&str>,
|
||||||
filter_query: Option<&str>,
|
filter_query: Option<&str>,
|
||||||
) -> Result<Collection>;
|
) -> Result<Collection>;
|
||||||
|
|
||||||
|
/// Retrieve a collection by ID. Returns `NotFound` if it does not exist.
|
||||||
async fn get_collection(&self, id: Uuid) -> Result<Collection>;
|
async fn get_collection(&self, id: Uuid) -> Result<Collection>;
|
||||||
|
|
||||||
|
/// List all collections.
|
||||||
async fn list_collections(&self) -> Result<Vec<Collection>>;
|
async fn list_collections(&self) -> Result<Vec<Collection>>;
|
||||||
|
|
||||||
|
/// Delete a collection and all its membership entries.
|
||||||
async fn delete_collection(&self, id: Uuid) -> Result<()>;
|
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(
|
async fn add_to_collection(
|
||||||
&self,
|
&self,
|
||||||
collection_id: Uuid,
|
collection_id: Uuid,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
position: i32,
|
position: i32,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Remove a media item from a collection.
|
||||||
async fn remove_from_collection(
|
async fn remove_from_collection(
|
||||||
&self,
|
&self,
|
||||||
collection_id: Uuid,
|
collection_id: Uuid,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get all media items in a collection, ordered by position.
|
||||||
async fn get_collection_members(
|
async fn get_collection_members(
|
||||||
&self,
|
&self,
|
||||||
collection_id: Uuid,
|
collection_id: Uuid,
|
||||||
) -> Result<Vec<MediaItem>>;
|
) -> Result<Vec<MediaItem>>;
|
||||||
|
|
||||||
// Search
|
// 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>;
|
async fn search(&self, request: &SearchRequest) -> Result<SearchResults>;
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
|
|
||||||
|
/// Record an audit log entry for an action on a media item.
|
||||||
async fn record_audit(&self, entry: &AuditEntry) -> Result<()>;
|
async fn record_audit(&self, entry: &AuditEntry) -> Result<()>;
|
||||||
|
|
||||||
|
/// List audit entries, optionally filtered to a specific media item.
|
||||||
async fn list_audit_entries(
|
async fn list_audit_entries(
|
||||||
&self,
|
&self,
|
||||||
media_id: Option<MediaId>,
|
media_id: Option<MediaId>,
|
||||||
|
|
@ -125,16 +208,22 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
) -> Result<Vec<AuditEntry>>;
|
) -> Result<Vec<AuditEntry>>;
|
||||||
|
|
||||||
// Custom fields
|
// Custom fields
|
||||||
|
|
||||||
|
/// Set a custom field on a media item (upserts if the field name exists).
|
||||||
async fn set_custom_field(
|
async fn set_custom_field(
|
||||||
&self,
|
&self,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
name: &str,
|
name: &str,
|
||||||
field: &CustomField,
|
field: &CustomField,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get all custom fields for a media item, keyed by field name.
|
||||||
async fn get_custom_fields(
|
async fn get_custom_fields(
|
||||||
&self,
|
&self,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<std::collections::HashMap<String, CustomField>>;
|
) -> Result<std::collections::HashMap<String, CustomField>>;
|
||||||
|
|
||||||
|
/// Delete a custom field from a media item by name.
|
||||||
async fn delete_custom_field(
|
async fn delete_custom_field(
|
||||||
&self,
|
&self,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
|
|
@ -142,8 +231,13 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
// Batch operations (transactional where supported)
|
// 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>;
|
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(
|
async fn batch_tag_media(
|
||||||
&self,
|
&self,
|
||||||
media_ids: &[MediaId],
|
media_ids: &[MediaId],
|
||||||
|
|
@ -151,11 +245,15 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
) -> Result<u64>;
|
) -> Result<u64>;
|
||||||
|
|
||||||
// Integrity
|
// 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(
|
async fn list_media_paths(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<(MediaId, std::path::PathBuf, ContentHash)>>;
|
) -> 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn batch_update_media(
|
async fn batch_update_media(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -169,6 +267,8 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
) -> Result<u64>;
|
) -> Result<u64>;
|
||||||
|
|
||||||
// Saved searches
|
// Saved searches
|
||||||
|
|
||||||
|
/// Persist a search query so it can be re-executed later.
|
||||||
async fn save_search(
|
async fn save_search(
|
||||||
&self,
|
&self,
|
||||||
id: uuid::Uuid,
|
id: uuid::Uuid,
|
||||||
|
|
@ -176,20 +276,41 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
query: &str,
|
query: &str,
|
||||||
sort_order: Option<&str>,
|
sort_order: Option<&str>,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// List all saved searches.
|
||||||
async fn list_saved_searches(&self)
|
async fn list_saved_searches(&self)
|
||||||
-> Result<Vec<crate::model::SavedSearch>>;
|
-> 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<()>;
|
async fn delete_saved_search(&self, id: uuid::Uuid) -> Result<()>;
|
||||||
|
|
||||||
// Duplicates
|
// Duplicates
|
||||||
|
|
||||||
|
/// Find groups of media items with identical content hashes.
|
||||||
async fn find_duplicates(&self) -> Result<Vec<Vec<MediaItem>>>;
|
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(
|
async fn find_perceptual_duplicates(
|
||||||
&self,
|
&self,
|
||||||
threshold: u32,
|
threshold: u32,
|
||||||
) -> Result<Vec<Vec<MediaItem>>>;
|
) -> Result<Vec<Vec<MediaItem>>>;
|
||||||
|
|
||||||
// Database management
|
// Database management
|
||||||
|
|
||||||
|
/// Collect aggregate database statistics (counts, size, backend name).
|
||||||
async fn database_stats(&self) -> Result<DatabaseStats>;
|
async fn database_stats(&self) -> Result<DatabaseStats>;
|
||||||
|
|
||||||
|
/// Reclaim unused disk space (VACUUM for `SQLite`, VACUUM FULL for Postgres).
|
||||||
async fn vacuum(&self) -> Result<()>;
|
async fn vacuum(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// Delete all data from all tables. Destructive; used in tests.
|
||||||
async fn clear_all_data(&self) -> Result<()>;
|
async fn clear_all_data(&self) -> Result<()>;
|
||||||
|
|
||||||
// Thumbnail helpers
|
// Thumbnail helpers
|
||||||
|
|
@ -200,18 +321,29 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
) -> Result<Vec<crate::model::MediaId>>;
|
) -> Result<Vec<crate::model::MediaId>>;
|
||||||
|
|
||||||
// Library statistics
|
// Library statistics
|
||||||
|
|
||||||
|
/// Compute comprehensive library statistics (sizes, counts by type,
|
||||||
|
/// top tags/collections, duplicates).
|
||||||
async fn library_statistics(&self) -> Result<LibraryStatistics>;
|
async fn library_statistics(&self) -> Result<LibraryStatistics>;
|
||||||
|
|
||||||
// User Management
|
// User Management
|
||||||
|
|
||||||
|
/// List all registered users.
|
||||||
async fn list_users(&self) -> Result<Vec<crate::users::User>>;
|
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(
|
async fn get_user(
|
||||||
&self,
|
&self,
|
||||||
id: crate::users::UserId,
|
id: crate::users::UserId,
|
||||||
) -> Result<crate::users::User>;
|
) -> Result<crate::users::User>;
|
||||||
|
|
||||||
|
/// Look up a user by username. Returns `NotFound` if not found.
|
||||||
async fn get_user_by_username(
|
async fn get_user_by_username(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
) -> Result<crate::users::User>;
|
) -> Result<crate::users::User>;
|
||||||
|
|
||||||
|
/// Create a new user with the given credentials and role.
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
|
|
@ -219,6 +351,9 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
role: crate::config::UserRole,
|
role: crate::config::UserRole,
|
||||||
profile: Option<crate::users::UserProfile>,
|
profile: Option<crate::users::UserProfile>,
|
||||||
) -> Result<crate::users::User>;
|
) -> Result<crate::users::User>;
|
||||||
|
|
||||||
|
/// Update a user's password, role, or profile. Only non-`None` fields
|
||||||
|
/// are applied.
|
||||||
async fn update_user(
|
async fn update_user(
|
||||||
&self,
|
&self,
|
||||||
id: crate::users::UserId,
|
id: crate::users::UserId,
|
||||||
|
|
@ -226,17 +361,25 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
role: Option<crate::config::UserRole>,
|
role: Option<crate::config::UserRole>,
|
||||||
profile: Option<crate::users::UserProfile>,
|
profile: Option<crate::users::UserProfile>,
|
||||||
) -> Result<crate::users::User>;
|
) -> Result<crate::users::User>;
|
||||||
|
|
||||||
|
/// Delete a user and all associated sessions.
|
||||||
async fn delete_user(&self, id: crate::users::UserId) -> Result<()>;
|
async fn delete_user(&self, id: crate::users::UserId) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get the library access grants for a user.
|
||||||
async fn get_user_libraries(
|
async fn get_user_libraries(
|
||||||
&self,
|
&self,
|
||||||
user_id: crate::users::UserId,
|
user_id: crate::users::UserId,
|
||||||
) -> Result<Vec<crate::users::UserLibraryAccess>>;
|
) -> Result<Vec<crate::users::UserLibraryAccess>>;
|
||||||
|
|
||||||
|
/// Grant a user access to a library root path with the given permission.
|
||||||
async fn grant_library_access(
|
async fn grant_library_access(
|
||||||
&self,
|
&self,
|
||||||
user_id: crate::users::UserId,
|
user_id: crate::users::UserId,
|
||||||
root_path: &str,
|
root_path: &str,
|
||||||
permission: crate::users::LibraryPermission,
|
permission: crate::users::LibraryPermission,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Revoke a user's access to a library root path.
|
||||||
async fn revoke_library_access(
|
async fn revoke_library_access(
|
||||||
&self,
|
&self,
|
||||||
user_id: crate::users::UserId,
|
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
|
// Default implementation: get the media item's path and check against
|
||||||
// user's library access
|
// user's library access
|
||||||
let media = self.get_media(media_id).await?;
|
let media = self.get_media(media_id).await?;
|
||||||
let path_str = media.path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// Get user's library permissions
|
// Get user's library permissions
|
||||||
let libraries = self.get_user_libraries(user_id).await?;
|
let libraries = self.get_user_libraries(user_id).await?;
|
||||||
|
|
||||||
// If user has no library restrictions, they have no access (unless they're
|
// If user has no library restrictions, they have no access (unless they're
|
||||||
// admin) This default impl requires at least one matching library
|
// 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 {
|
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);
|
return Ok(lib.permission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(crate::error::PinakesError::Authorization(format!(
|
Err(crate::error::PinakesError::Authorization(format!(
|
||||||
"user {} has no access to media {}",
|
"user {user_id} has no access to media {media_id}"
|
||||||
user_id, 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(
|
async fn rate_media(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
|
|
@ -306,14 +449,21 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
stars: u8,
|
stars: u8,
|
||||||
review: Option<&str>,
|
review: Option<&str>,
|
||||||
) -> Result<Rating>;
|
) -> Result<Rating>;
|
||||||
|
|
||||||
|
/// Get all ratings for a media item.
|
||||||
async fn get_media_ratings(&self, media_id: MediaId) -> Result<Vec<Rating>>;
|
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(
|
async fn get_user_rating(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<Option<Rating>>;
|
) -> Result<Option<Rating>>;
|
||||||
|
|
||||||
|
/// Delete a rating by ID.
|
||||||
async fn delete_rating(&self, id: Uuid) -> Result<()>;
|
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(
|
async fn add_comment(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
|
|
@ -321,31 +471,44 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
text: &str,
|
text: &str,
|
||||||
parent_id: Option<Uuid>,
|
parent_id: Option<Uuid>,
|
||||||
) -> Result<Comment>;
|
) -> Result<Comment>;
|
||||||
|
|
||||||
|
/// Get all comments for a media item.
|
||||||
async fn get_media_comments(&self, media_id: MediaId)
|
async fn get_media_comments(&self, media_id: MediaId)
|
||||||
-> Result<Vec<Comment>>;
|
-> Result<Vec<Comment>>;
|
||||||
|
|
||||||
|
/// Delete a comment by ID.
|
||||||
async fn delete_comment(&self, id: Uuid) -> Result<()>;
|
async fn delete_comment(&self, id: Uuid) -> Result<()>;
|
||||||
|
|
||||||
|
/// Mark a media item as a favorite for a user.
|
||||||
async fn add_favorite(
|
async fn add_favorite(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Remove a media item from a user's favorites.
|
||||||
async fn remove_favorite(
|
async fn remove_favorite(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get a user's favorited media items with pagination.
|
||||||
async fn get_user_favorites(
|
async fn get_user_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
pagination: &Pagination,
|
pagination: &Pagination,
|
||||||
) -> Result<Vec<MediaItem>>;
|
) -> Result<Vec<MediaItem>>;
|
||||||
|
|
||||||
|
/// Check whether a media item is in a user's favorites.
|
||||||
async fn is_favorite(
|
async fn is_favorite(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<bool>;
|
) -> Result<bool>;
|
||||||
|
|
||||||
|
/// Create a public share link for a media item with optional password
|
||||||
|
/// and expiry.
|
||||||
async fn create_share_link(
|
async fn create_share_link(
|
||||||
&self,
|
&self,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
|
|
@ -354,10 +517,17 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
password_hash: Option<&str>,
|
password_hash: Option<&str>,
|
||||||
expires_at: Option<DateTime<Utc>>,
|
expires_at: Option<DateTime<Utc>>,
|
||||||
) -> Result<ShareLink>;
|
) -> Result<ShareLink>;
|
||||||
|
|
||||||
|
/// Look up a share link by its token. Returns `NotFound` if invalid.
|
||||||
async fn get_share_link(&self, token: &str) -> Result<ShareLink>;
|
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<()>;
|
async fn increment_share_views(&self, token: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// Delete a share link by ID.
|
||||||
async fn delete_share_link(&self, id: Uuid) -> Result<()>;
|
async fn delete_share_link(&self, id: Uuid) -> Result<()>;
|
||||||
|
|
||||||
|
/// Create a new playlist. Smart playlists auto-populate via `filter_query`.
|
||||||
async fn create_playlist(
|
async fn create_playlist(
|
||||||
&self,
|
&self,
|
||||||
owner_id: UserId,
|
owner_id: UserId,
|
||||||
|
|
@ -367,11 +537,18 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
is_smart: bool,
|
is_smart: bool,
|
||||||
filter_query: Option<&str>,
|
filter_query: Option<&str>,
|
||||||
) -> Result<Playlist>;
|
) -> Result<Playlist>;
|
||||||
|
|
||||||
|
/// Get a playlist by ID. Returns `NotFound` if it does not exist.
|
||||||
async fn get_playlist(&self, id: Uuid) -> Result<Playlist>;
|
async fn get_playlist(&self, id: Uuid) -> Result<Playlist>;
|
||||||
|
|
||||||
|
/// List playlists, optionally filtered to a specific owner.
|
||||||
async fn list_playlists(
|
async fn list_playlists(
|
||||||
&self,
|
&self,
|
||||||
owner_id: Option<UserId>,
|
owner_id: Option<UserId>,
|
||||||
) -> Result<Vec<Playlist>>;
|
) -> Result<Vec<Playlist>>;
|
||||||
|
|
||||||
|
/// Update a playlist's name, description, or visibility.
|
||||||
|
/// Only non-`None` fields are applied.
|
||||||
async fn update_playlist(
|
async fn update_playlist(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
@ -379,22 +556,32 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
description: Option<&str>,
|
description: Option<&str>,
|
||||||
is_public: Option<bool>,
|
is_public: Option<bool>,
|
||||||
) -> Result<Playlist>;
|
) -> Result<Playlist>;
|
||||||
|
|
||||||
|
/// Delete a playlist and all its item associations.
|
||||||
async fn delete_playlist(&self, id: Uuid) -> Result<()>;
|
async fn delete_playlist(&self, id: Uuid) -> Result<()>;
|
||||||
|
|
||||||
|
/// Add a media item to a playlist at the given position.
|
||||||
async fn add_to_playlist(
|
async fn add_to_playlist(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: Uuid,
|
playlist_id: Uuid,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
position: i32,
|
position: i32,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Remove a media item from a playlist.
|
||||||
async fn remove_from_playlist(
|
async fn remove_from_playlist(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: Uuid,
|
playlist_id: Uuid,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get all media items in a playlist, ordered by position.
|
||||||
async fn get_playlist_items(
|
async fn get_playlist_items(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: Uuid,
|
playlist_id: Uuid,
|
||||||
) -> Result<Vec<MediaItem>>;
|
) -> Result<Vec<MediaItem>>;
|
||||||
|
|
||||||
|
/// Move a media item to a new position within a playlist.
|
||||||
async fn reorder_playlist(
|
async fn reorder_playlist(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: Uuid,
|
playlist_id: Uuid,
|
||||||
|
|
@ -402,69 +589,106 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
new_position: i32,
|
new_position: i32,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Record a usage/analytics event (play, view, download, etc.).
|
||||||
async fn record_usage_event(&self, event: &UsageEvent) -> Result<()>;
|
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(
|
async fn get_usage_events(
|
||||||
&self,
|
&self,
|
||||||
media_id: Option<MediaId>,
|
media_id: Option<MediaId>,
|
||||||
user_id: Option<UserId>,
|
user_id: Option<UserId>,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
) -> Result<Vec<UsageEvent>>;
|
) -> 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)>>;
|
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(
|
async fn get_recently_viewed(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
) -> Result<Vec<MediaItem>>;
|
) -> Result<Vec<MediaItem>>;
|
||||||
|
|
||||||
|
/// Update playback/watch progress for a user on a media item (in seconds).
|
||||||
async fn update_watch_progress(
|
async fn update_watch_progress(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
progress_secs: f64,
|
progress_secs: f64,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get the stored watch progress (in seconds) for a user/media pair.
|
||||||
async fn get_watch_progress(
|
async fn get_watch_progress(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<Option<f64>>;
|
) -> 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>;
|
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<()>;
|
async fn add_subtitle(&self, subtitle: &Subtitle) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get all subtitle tracks for a media item.
|
||||||
async fn get_media_subtitles(
|
async fn get_media_subtitles(
|
||||||
&self,
|
&self,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<Vec<Subtitle>>;
|
) -> Result<Vec<Subtitle>>;
|
||||||
|
|
||||||
|
/// Delete a subtitle track by ID.
|
||||||
async fn delete_subtitle(&self, id: Uuid) -> Result<()>;
|
async fn delete_subtitle(&self, id: Uuid) -> Result<()>;
|
||||||
|
|
||||||
|
/// Adjust the timing offset (in milliseconds) for a subtitle track.
|
||||||
async fn update_subtitle_offset(
|
async fn update_subtitle_offset(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
offset_ms: i64,
|
offset_ms: i64,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Store metadata fetched from an external source (e.g., `MusicBrainz`,
|
||||||
|
/// `TMDb`).
|
||||||
async fn store_external_metadata(
|
async fn store_external_metadata(
|
||||||
&self,
|
&self,
|
||||||
meta: &ExternalMetadata,
|
meta: &ExternalMetadata,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get all external metadata records for a media item.
|
||||||
async fn get_external_metadata(
|
async fn get_external_metadata(
|
||||||
&self,
|
&self,
|
||||||
media_id: MediaId,
|
media_id: MediaId,
|
||||||
) -> Result<Vec<ExternalMetadata>>;
|
) -> Result<Vec<ExternalMetadata>>;
|
||||||
|
|
||||||
|
/// Delete an external metadata record by ID.
|
||||||
async fn delete_external_metadata(&self, id: Uuid) -> Result<()>;
|
async fn delete_external_metadata(&self, id: Uuid) -> Result<()>;
|
||||||
|
|
||||||
|
/// Create a new transcoding session for a media item.
|
||||||
async fn create_transcode_session(
|
async fn create_transcode_session(
|
||||||
&self,
|
&self,
|
||||||
session: &TranscodeSession,
|
session: &TranscodeSession,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get a transcoding session by ID.
|
||||||
async fn get_transcode_session(&self, id: Uuid) -> Result<TranscodeSession>;
|
async fn get_transcode_session(&self, id: Uuid) -> Result<TranscodeSession>;
|
||||||
|
|
||||||
|
/// List transcoding sessions, optionally filtered to a media item.
|
||||||
async fn list_transcode_sessions(
|
async fn list_transcode_sessions(
|
||||||
&self,
|
&self,
|
||||||
media_id: Option<MediaId>,
|
media_id: Option<MediaId>,
|
||||||
) -> Result<Vec<TranscodeSession>>;
|
) -> Result<Vec<TranscodeSession>>;
|
||||||
|
|
||||||
|
/// Update the status and progress of a transcoding session.
|
||||||
async fn update_transcode_status(
|
async fn update_transcode_status(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
status: TranscodeStatus,
|
status: TranscodeStatus,
|
||||||
progress: f32,
|
progress: f32,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Delete transcode sessions that expired before the given timestamp.
|
||||||
|
/// Returns the number of sessions cleaned up.
|
||||||
async fn cleanup_expired_transcodes(
|
async fn cleanup_expired_transcodes(
|
||||||
&self,
|
&self,
|
||||||
before: DateTime<Utc>,
|
before: DateTime<Utc>,
|
||||||
|
|
@ -479,16 +703,26 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
session_token: &str,
|
session_token: &str,
|
||||||
) -> Result<Option<SessionData>>;
|
) -> 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<()>;
|
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
|
/// Delete a specific session
|
||||||
async fn delete_session(&self, session_token: &str) -> Result<()>;
|
async fn delete_session(&self, session_token: &str) -> Result<()>;
|
||||||
|
|
||||||
/// Delete all sessions for a specific user
|
/// Delete all sessions for a specific user
|
||||||
async fn delete_user_sessions(&self, username: &str) -> Result<u64>;
|
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>;
|
async fn delete_expired_sessions(&self) -> Result<u64>;
|
||||||
|
|
||||||
/// List all active sessions (optionally filtered by username)
|
/// List all active sessions (optionally filtered by username)
|
||||||
|
|
@ -533,7 +767,7 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
/// List all series with book counts
|
/// List all series with book counts
|
||||||
async fn list_series(&self) -> Result<Vec<(String, u64)>>;
|
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)
|
async fn get_series_books(&self, series_name: &str)
|
||||||
-> Result<Vec<MediaItem>>;
|
-> Result<Vec<MediaItem>>;
|
||||||
|
|
||||||
|
|
@ -591,10 +825,10 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
/// deleted.
|
/// deleted.
|
||||||
async fn decrement_blob_ref(&self, hash: &ContentHash) -> Result<bool>;
|
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<()>;
|
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>>;
|
async fn list_orphaned_blobs(&self) -> Result<Vec<ManagedBlob>>;
|
||||||
|
|
||||||
/// Delete a blob record
|
/// Delete a blob record
|
||||||
|
|
@ -635,7 +869,7 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
/// Delete a sync device
|
/// Delete a sync device
|
||||||
async fn delete_device(&self, id: crate::sync::DeviceId) -> Result<()>;
|
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<()>;
|
async fn touch_device(&self, id: crate::sync::DeviceId) -> Result<()>;
|
||||||
|
|
||||||
/// Record a change in the sync log
|
/// Record a change in the sync log
|
||||||
|
|
@ -830,13 +1064,19 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
) -> Result<Vec<crate::sharing::ShareNotification>>;
|
) -> Result<Vec<crate::sharing::ShareNotification>>;
|
||||||
|
|
||||||
/// Mark a notification as read
|
/// Mark a notification as read. Scoped to `user_id` to prevent cross-user
|
||||||
async fn mark_notification_read(&self, id: Uuid) -> Result<()>;
|
/// 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
|
/// Mark all notifications as read for a user
|
||||||
async fn mark_all_notifications_read(&self, user_id: UserId) -> Result<()>;
|
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 external storage, this actually renames the file on disk.
|
||||||
/// For managed storage, this only updates the metadata.
|
/// For managed storage, this only updates the metadata.
|
||||||
/// Returns the old path for sync log recording.
|
/// Returns the old path for sync log recording.
|
||||||
|
|
@ -866,7 +1106,7 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
Ok(results)
|
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<()>;
|
async fn soft_delete_media(&self, id: MediaId) -> Result<()>;
|
||||||
|
|
||||||
/// Restore a soft-deleted media item.
|
/// Restore a soft-deleted media item.
|
||||||
|
|
@ -919,15 +1159,24 @@ pub trait StorageBackend: Send + Sync + 'static {
|
||||||
depth: u32,
|
depth: u32,
|
||||||
) -> Result<crate::model::GraphData>;
|
) -> Result<crate::model::GraphData>;
|
||||||
|
|
||||||
/// Resolve unresolved links by matching target_path against media item paths.
|
/// Resolve unresolved links by matching `target_path` against media item
|
||||||
/// Returns the number of links that were resolved.
|
/// paths. Returns the number of links that were resolved.
|
||||||
async fn resolve_links(&self) -> Result<u64>;
|
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<()>;
|
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>;
|
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.
|
/// Comprehensive library statistics.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue