Seralize index into DB

This commit is contained in:
Antoine Gersant 2024-07-29 22:56:03 -07:00
parent 1f3cc1ea26
commit b42c6d39e8
9 changed files with 105 additions and 42 deletions

37
Cargo.lock generated
View file

@ -60,6 +60,12 @@ dependencies = [
"byteorder", "byteorder",
] ]
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.81" version = "0.1.81"
@ -258,6 +264,30 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitcode"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee1bce7608560cd4bf0296a4262d0dbf13e6bcec5ff2105724c8ab88cc7fc784"
dependencies = [
"arrayvec",
"bitcode_derive",
"bytemuck",
"glam",
"serde",
]
[[package]]
name = "bitcode_derive"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.71",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -810,6 +840,12 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "glam"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.0" version = "0.14.0"
@ -1556,6 +1592,7 @@ dependencies = [
"axum-range", "axum-range",
"axum-test", "axum-test",
"base64 0.22.1", "base64 0.22.1",
"bitcode",
"branca", "branca",
"bytes", "bytes",
"crossbeam-channel", "crossbeam-channel",

View file

@ -13,6 +13,7 @@ ape = "0.5"
axum-extra = { version = "0.9.3", features = ["typed-header"] } axum-extra = { version = "0.9.3", features = ["typed-header"] }
axum-range = "0.4.0" axum-range = "0.4.0"
base64 = "0.22.1" base64 = "0.22.1"
bitcode = { version = "0.6.3", features = ["serde"] }
branca = "0.10.1" branca = "0.10.1"
crossbeam-channel = "0.5.13" crossbeam-channel = "0.5.13"
futures-util = { version = "0.3.30" } futures-util = { version = "0.3.30" }

View file

@ -67,7 +67,7 @@ impl App {
let auth_secret = settings_manager.get_auth_secret().await?; let auth_secret = settings_manager.get_auth_secret().await?;
let ddns_manager = ddns::Manager::new(db.clone()); let ddns_manager = ddns::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone(), auth_secret); let user_manager = user::Manager::new(db.clone(), auth_secret);
let index_manager = collection::IndexManager::new(); let index_manager = collection::IndexManager::new(db.clone()).await;
let browser = collection::Browser::new(db.clone(), vfs_manager.clone()); let browser = collection::Browser::new(db.clone(), vfs_manager.clone());
let updater = collection::Updater::new( let updater = collection::Updater::new(
db.clone(), db.clone(),

View file

@ -4,21 +4,29 @@ use std::{
sync::Arc, sync::Arc,
}; };
use log::{error, info};
use rand::{rngs::ThreadRng, seq::IteratorRandom}; use rand::{rngs::ThreadRng, seq::IteratorRandom};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::app::collection; use crate::{app::collection, db::DB};
#[derive(Clone)] #[derive(Clone)]
pub struct IndexManager { pub struct IndexManager {
db: DB,
index: Arc<RwLock<Index>>, index: Arc<RwLock<Index>>,
} }
impl IndexManager { impl IndexManager {
pub fn new() -> Self { pub async fn new(db: DB) -> Self {
Self { let mut index_manager = Self {
db,
index: Arc::default(), index: Arc::default(),
};
if let Err(e) = index_manager.try_restore_index().await {
error!("Failed to restore index: {}", e);
} }
index_manager
} }
pub(super) async fn replace_index(&mut self, new_index: Index) { pub(super) async fn replace_index(&mut self, new_index: Index) {
@ -26,6 +34,37 @@ impl IndexManager {
*lock = new_index; *lock = new_index;
} }
pub(super) async fn persist_index(&mut self, index: &Index) -> Result<(), collection::Error> {
let serialized = match bitcode::serialize(index) {
Ok(s) => s,
Err(_) => return Err(collection::Error::IndexSerializationError),
};
sqlx::query!("UPDATE collection_index SET content = $1", serialized)
.execute(self.db.connect().await?.as_mut())
.await?;
Ok(())
}
async fn try_restore_index(&mut self) -> Result<bool, collection::Error> {
let serialized = sqlx::query_scalar!("SELECT content FROM collection_index")
.fetch_one(self.db.connect().await?.as_mut())
.await?;
let Some(serialized) = serialized else {
info!("Database did not contain a collection to restore");
return Ok(false);
};
let index = match bitcode::deserialize(&serialized[..]) {
Ok(i) => i,
Err(_) => return Err(collection::Error::IndexDeserializationError),
};
self.replace_index(index).await;
Ok(true)
}
pub async fn get_random_albums( pub async fn get_random_albums(
&self, &self,
count: usize, count: usize,
@ -119,7 +158,7 @@ impl IndexBuilder {
} }
} }
#[derive(Default)] #[derive(Default, Serialize, Deserialize)]
pub(super) struct Index { pub(super) struct Index {
songs: HashMap<SongID, collection::Song>, songs: HashMap<SongID, collection::Song>,
albums: HashMap<AlbumID, Album>, albums: HashMap<AlbumID, Album>,
@ -148,10 +187,10 @@ impl Index {
} }
} }
#[derive(Clone, Copy, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct SongID(u64); pub struct SongID(u64);
#[derive(Clone, Eq, Hash, PartialEq)] #[derive(Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct SongKey { pub struct SongKey {
pub virtual_path: String, pub virtual_path: String,
} }
@ -177,7 +216,7 @@ impl collection::Song {
} }
} }
#[derive(Default)] #[derive(Default, Serialize, Deserialize)]
struct Album { struct Album {
pub name: Option<String>, pub name: Option<String>,
pub artwork: Option<String>, pub artwork: Option<String>,
@ -187,7 +226,7 @@ struct Album {
pub songs: HashSet<SongID>, pub songs: HashSet<SongID>,
} }
#[derive(Clone, Copy, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct AlbumID(u64); pub struct AlbumID(u64);
#[derive(Clone, Eq, Hash, PartialEq)] #[derive(Clone, Eq, Hash, PartialEq)]

View file

@ -1,5 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow; use sqlx::prelude::FromRow;
use crate::{ use crate::{
@ -7,7 +8,7 @@ use crate::{
db, db,
}; };
#[derive(Clone, Debug, FromRow, PartialEq, Eq)] #[derive(Clone, Debug, FromRow, PartialEq, Eq, Serialize, Deserialize)]
pub struct MultiString(pub Vec<String>); pub struct MultiString(pub Vec<String>);
impl MultiString { impl MultiString {
@ -33,6 +34,10 @@ pub enum Error {
DatabaseConnection(#[from] db::Error), DatabaseConnection(#[from] db::Error),
#[error(transparent)] #[error(transparent)]
Vfs(#[from] vfs::Error), Vfs(#[from] vfs::Error),
#[error("Could not serialize collection")]
IndexDeserializationError,
#[error("Could not deserialize collection")]
IndexSerializationError,
#[error(transparent)] #[error(transparent)]
ThreadPoolBuilder(#[from] rayon::ThreadPoolBuildError), ThreadPoolBuilder(#[from] rayon::ThreadPoolBuildError),
#[error(transparent)] #[error(transparent)]
@ -45,7 +50,7 @@ pub enum File {
Song(Song), Song(Song),
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Song { pub struct Song {
pub id: i64, pub id: i64,
pub path: String, pub path: String,

View file

@ -27,7 +27,7 @@ impl Updater {
settings_manager: settings::Manager, settings_manager: settings::Manager,
vfs_manager: vfs::Manager, vfs_manager: vfs::Manager,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut updater = Self { let updater = Self {
db, db,
index_manager, index_manager,
vfs_manager, vfs_manager,
@ -47,8 +47,6 @@ impl Updater {
} }
}); });
updater.rebuild_index().await?;
Ok(updater) Ok(updater)
} }
@ -76,33 +74,6 @@ impl Updater {
}); });
} }
async fn rebuild_index(&mut self) -> Result<(), Error> {
let start = Instant::now();
info!("Rebuilding index from disk database");
let mut index_builder = IndexBuilder::default();
let mut connection = self.db.connect().await?;
let songs = sqlx::query_as!(Song, "SELECT * FROM songs")
.fetch_all(connection.as_mut())
.await?;
for song in songs {
index_builder.add_song(song);
}
self.index_manager
.replace_index(index_builder.build())
.await;
info!(
"Index rebuild took {} seconds",
start.elapsed().as_millis() as f32 / 1000.0
);
Ok(())
}
pub async fn update(&mut self) -> Result<(), Error> { pub async fn update(&mut self) -> Result<(), Error> {
let start = Instant::now(); let start = Instant::now();
info!("Beginning collection scan"); info!("Beginning collection scan");
@ -172,6 +143,7 @@ impl Updater {
}); });
let index = tokio::join!(scanner.scan(), directory_task, song_task).2?; let index = tokio::join!(scanner.scan(), directory_task, song_task).2?;
self.index_manager.persist_index(&index).await?;
self.index_manager.replace_index(index).await; self.index_manager.replace_index(index).await;
info!( info!(

View file

@ -68,7 +68,7 @@ impl ContextBuilder {
ddns_manager.clone(), ddns_manager.clone(),
); );
let browser = collection::Browser::new(db.clone(), vfs_manager.clone()); let browser = collection::Browser::new(db.clone(), vfs_manager.clone());
let index_manager = collection::IndexManager::new(); let index_manager = collection::IndexManager::new(db.clone()).await;
let updater = collection::Updater::new( let updater = collection::Updater::new(
db.clone(), db.clone(),
index_manager.clone(), index_manager.clone(),

View file

@ -75,6 +75,13 @@ CREATE TABLE songs (
UNIQUE(path) ON CONFLICT REPLACE UNIQUE(path) ON CONFLICT REPLACE
); );
CREATE TABLE collection_index (
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
content BLOB
);
INSERT INTO collection_index (id, content) VALUES (0, NULL);
CREATE TABLE playlists ( CREATE TABLE playlists (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
owner INTEGER NOT NULL, owner INTEGER NOT NULL,

View file

@ -89,6 +89,8 @@ impl From<collection::Error> for APIError {
collection::Error::Database(e) => APIError::Database(e), collection::Error::Database(e) => APIError::Database(e),
collection::Error::DatabaseConnection(e) => e.into(), collection::Error::DatabaseConnection(e) => e.into(),
collection::Error::Vfs(e) => e.into(), collection::Error::Vfs(e) => e.into(),
collection::Error::IndexDeserializationError => APIError::Internal,
collection::Error::IndexSerializationError => APIError::Internal,
collection::Error::ThreadPoolBuilder(_) => APIError::Internal, collection::Error::ThreadPoolBuilder(_) => APIError::Internal,
collection::Error::ThreadJoining(_) => APIError::Internal, collection::Error::ThreadJoining(_) => APIError::Internal,
} }