initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 34856 additions and 0 deletions
26
crates/pinakes-core/src/storage/migrations.rs
Normal file
26
crates/pinakes-core/src/storage/migrations.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::error::{PinakesError, Result};
|
||||
|
||||
mod sqlite_migrations {
|
||||
use refinery::embed_migrations;
|
||||
embed_migrations!("../../migrations/sqlite");
|
||||
}
|
||||
|
||||
mod postgres_migrations {
|
||||
use refinery::embed_migrations;
|
||||
embed_migrations!("../../migrations/postgres");
|
||||
}
|
||||
|
||||
pub fn run_sqlite_migrations(conn: &mut rusqlite::Connection) -> Result<()> {
|
||||
sqlite_migrations::migrations::runner()
|
||||
.run(conn)
|
||||
.map_err(|e| PinakesError::Migration(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_postgres_migrations(client: &mut tokio_postgres::Client) -> Result<()> {
|
||||
postgres_migrations::migrations::runner()
|
||||
.run_async(client)
|
||||
.await
|
||||
.map_err(|e| PinakesError::Migration(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
209
crates/pinakes-core/src/storage/mod.rs
Normal file
209
crates/pinakes-core/src/storage/mod.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
pub mod migrations;
|
||||
pub mod postgres;
|
||||
pub mod sqlite;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::model::*;
|
||||
use crate::search::{SearchRequest, SearchResults};
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait StorageBackend: Send + Sync + 'static {
|
||||
// Migrations
|
||||
async fn run_migrations(&self) -> Result<()>;
|
||||
|
||||
// Root directories
|
||||
async fn add_root_dir(&self, path: PathBuf) -> Result<()>;
|
||||
async fn list_root_dirs(&self) -> Result<Vec<PathBuf>>;
|
||||
async fn remove_root_dir(&self, path: &std::path::Path) -> Result<()>;
|
||||
|
||||
// Media CRUD
|
||||
async fn insert_media(&self, item: &MediaItem) -> Result<()>;
|
||||
async fn get_media(&self, id: MediaId) -> Result<MediaItem>;
|
||||
async fn count_media(&self) -> Result<u64>;
|
||||
async fn get_media_by_hash(&self, hash: &ContentHash) -> Result<Option<MediaItem>>;
|
||||
async fn list_media(&self, pagination: &Pagination) -> Result<Vec<MediaItem>>;
|
||||
async fn update_media(&self, item: &MediaItem) -> Result<()>;
|
||||
async fn delete_media(&self, id: MediaId) -> Result<()>;
|
||||
async fn delete_all_media(&self) -> Result<u64>;
|
||||
|
||||
// Tags
|
||||
async fn create_tag(&self, name: &str, parent_id: Option<Uuid>) -> Result<Tag>;
|
||||
async fn get_tag(&self, id: Uuid) -> Result<Tag>;
|
||||
async fn list_tags(&self) -> Result<Vec<Tag>>;
|
||||
async fn delete_tag(&self, id: Uuid) -> Result<()>;
|
||||
async fn tag_media(&self, media_id: MediaId, tag_id: Uuid) -> Result<()>;
|
||||
async fn untag_media(&self, media_id: MediaId, tag_id: Uuid) -> Result<()>;
|
||||
async fn get_media_tags(&self, media_id: MediaId) -> Result<Vec<Tag>>;
|
||||
async fn get_tag_descendants(&self, tag_id: Uuid) -> Result<Vec<Tag>>;
|
||||
|
||||
// Collections
|
||||
async fn create_collection(
|
||||
&self,
|
||||
name: &str,
|
||||
kind: CollectionKind,
|
||||
description: Option<&str>,
|
||||
filter_query: Option<&str>,
|
||||
) -> Result<Collection>;
|
||||
async fn get_collection(&self, id: Uuid) -> Result<Collection>;
|
||||
async fn list_collections(&self) -> Result<Vec<Collection>>;
|
||||
async fn delete_collection(&self, id: Uuid) -> Result<()>;
|
||||
async fn add_to_collection(
|
||||
&self,
|
||||
collection_id: Uuid,
|
||||
media_id: MediaId,
|
||||
position: i32,
|
||||
) -> Result<()>;
|
||||
async fn remove_from_collection(&self, collection_id: Uuid, media_id: MediaId) -> Result<()>;
|
||||
async fn get_collection_members(&self, collection_id: Uuid) -> Result<Vec<MediaItem>>;
|
||||
|
||||
// Search
|
||||
async fn search(&self, request: &SearchRequest) -> Result<SearchResults>;
|
||||
|
||||
// Audit
|
||||
async fn record_audit(&self, entry: &AuditEntry) -> Result<()>;
|
||||
async fn list_audit_entries(
|
||||
&self,
|
||||
media_id: Option<MediaId>,
|
||||
pagination: &Pagination,
|
||||
) -> Result<Vec<AuditEntry>>;
|
||||
|
||||
// Custom fields
|
||||
async fn set_custom_field(
|
||||
&self,
|
||||
media_id: MediaId,
|
||||
name: &str,
|
||||
field: &CustomField,
|
||||
) -> Result<()>;
|
||||
async fn get_custom_fields(
|
||||
&self,
|
||||
media_id: MediaId,
|
||||
) -> Result<std::collections::HashMap<String, CustomField>>;
|
||||
async fn delete_custom_field(&self, media_id: MediaId, name: &str) -> Result<()>;
|
||||
|
||||
// Batch operations (transactional where supported)
|
||||
async fn batch_delete_media(&self, ids: &[MediaId]) -> Result<u64> {
|
||||
let mut count = 0u64;
|
||||
for id in ids {
|
||||
self.delete_media(*id).await?;
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn batch_tag_media(&self, media_ids: &[MediaId], tag_ids: &[Uuid]) -> Result<u64> {
|
||||
let mut count = 0u64;
|
||||
for media_id in media_ids {
|
||||
for tag_id in tag_ids {
|
||||
self.tag_media(*media_id, *tag_id).await?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// Integrity
|
||||
async fn list_media_paths(&self) -> Result<Vec<(MediaId, std::path::PathBuf, ContentHash)>>;
|
||||
|
||||
// Batch metadata update
|
||||
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> {
|
||||
let mut count = 0u64;
|
||||
for id in ids {
|
||||
let mut item = self.get_media(*id).await?;
|
||||
if let Some(v) = title {
|
||||
item.title = Some(v.to_string());
|
||||
}
|
||||
if let Some(v) = artist {
|
||||
item.artist = Some(v.to_string());
|
||||
}
|
||||
if let Some(v) = album {
|
||||
item.album = Some(v.to_string());
|
||||
}
|
||||
if let Some(v) = genre {
|
||||
item.genre = Some(v.to_string());
|
||||
}
|
||||
if let Some(v) = &year {
|
||||
item.year = Some(*v);
|
||||
}
|
||||
if let Some(v) = description {
|
||||
item.description = Some(v.to_string());
|
||||
}
|
||||
item.updated_at = chrono::Utc::now();
|
||||
self.update_media(&item).await?;
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// Saved searches
|
||||
async fn save_search(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
name: &str,
|
||||
query: &str,
|
||||
sort_order: Option<&str>,
|
||||
) -> Result<()>;
|
||||
async fn list_saved_searches(&self) -> Result<Vec<crate::model::SavedSearch>>;
|
||||
async fn delete_saved_search(&self, id: uuid::Uuid) -> Result<()>;
|
||||
|
||||
// Duplicates
|
||||
async fn find_duplicates(&self) -> Result<Vec<Vec<MediaItem>>>;
|
||||
|
||||
// Database management
|
||||
async fn database_stats(&self) -> Result<DatabaseStats>;
|
||||
async fn vacuum(&self) -> Result<()>;
|
||||
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
|
||||
async fn library_statistics(&self) -> Result<LibraryStatistics>;
|
||||
}
|
||||
|
||||
/// 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>;
|
||||
1847
crates/pinakes-core/src/storage/postgres.rs
Normal file
1847
crates/pinakes-core/src/storage/postgres.rs
Normal file
File diff suppressed because it is too large
Load diff
1649
crates/pinakes-core/src/storage/sqlite.rs
Normal file
1649
crates/pinakes-core/src/storage/sqlite.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue