diff --git a/Cargo.lock b/Cargo.lock index 43877a2..ad774c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,6 +620,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.1.0" @@ -840,6 +852,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + [[package]] name = "headers" version = "0.4.0" @@ -1268,6 +1289,17 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1695,6 +1727,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "png" version = "0.17.13" @@ -1750,6 +1788,7 @@ dependencies = [ "rand", "rayon", "regex", + "rusqlite", "sd-notify", "serde", "serde_derive", @@ -1981,6 +2020,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.6.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust-multipart-rfc7578_2" version = "0.6.1" @@ -2920,6 +2973,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 133113e..9e7f816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ pbkdf2 = "0.11" rand = "0.8" rayon = "1.10.0" regex = "1.10.5" +rusqlite = { version = "0.32.0", features = ["bundled"] } serde = { version = "1.0.147", features = ["derive"] } serde_derive = "1.0.147" serde_json = "1.0.122" diff --git a/src/app.rs b/src/app.rs index d2479aa..8500ed4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,6 +6,8 @@ use std::time::Duration; use log::info; use rand::rngs::OsRng; use rand::RngCore; +use tokio::fs::try_exists; +use tokio::task::spawn_blocking; use crate::app::legacy::*; use crate::paths::Paths; @@ -37,6 +39,8 @@ pub enum Error { #[error(transparent)] FileWatch(#[from] notify::Error), #[error(transparent)] + SQL(#[from] rusqlite::Error), + #[error(transparent)] Ape(#[from] ape::Error), #[error("ID3 error in `{0}`: `{1}`")] Id3(PathBuf, id3::Error), @@ -101,6 +105,8 @@ pub enum Error { #[error("Could not serialize collection")] IndexSerializationError, + #[error("Invalid Directory")] + InvalidDirectory(String), #[error("The following virtual path could not be mapped to a real path: `{0}`")] CouldNotMapToRealPath(PathBuf), #[error("User not found")] @@ -177,6 +183,7 @@ impl App { .map_err(|e| Error::Io(thumbnails_dir_path.clone(), e))?; let auth_secret_file_path = paths.data_dir_path.join("auth.secret"); + Self::migrate_legacy_auth_secret(&paths.db_file_path, &auth_secret_file_path).await?; let auth_secret = Self::get_or_create_auth_secret(&auth_secret_file_path).await?; let config_manager = config::Manager::new(&paths.config_file_path, auth_secret).await?; @@ -206,13 +213,61 @@ impl App { Ok(app) } + async fn migrate_legacy_auth_secret( + db_file_path: &PathBuf, + secret_file_path: &PathBuf, + ) -> Result<(), Error> { + if !try_exists(db_file_path) + .await + .map_err(|e| Error::Io(db_file_path.clone(), e))? + { + return Ok(()); + } + + if try_exists(secret_file_path) + .await + .map_err(|e| Error::Io(secret_file_path.clone(), e))? + { + return Ok(()); + } + + info!( + "Migrating auth secret from database at `{}`", + db_file_path.to_string_lossy() + ); + + let secret = spawn_blocking({ + let db_file_path = db_file_path.clone(); + move || read_legacy_auth_secret(&db_file_path) + }) + .await??; + + tokio::fs::write(secret_file_path, &secret) + .await + .map_err(|e| Error::Io(secret_file_path.clone(), e))?; + + Ok(()) + } + async fn migrate_legacy_db(&self, db_file_path: &PathBuf) -> Result<(), Error> { - let Some(config) = read_legacy_config(db_file_path)? else { + if !try_exists(db_file_path) + .await + .map_err(|e| Error::Io(db_file_path.clone(), e))? + { + return Ok(()); + } + + let Some(config) = tokio::task::spawn_blocking({ + let db_file_path = db_file_path.clone(); + move || read_legacy_config(&db_file_path) + }) + .await?? + else { return Ok(()); }; info!( - "Found usable data in legacy database at `{}`, beginning migration process", + "Found usable config in legacy database at `{}`, beginning migration process", db_file_path.to_string_lossy() ); diff --git a/src/app/legacy.rs b/src/app/legacy.rs index aec274b..ae06b58 100644 --- a/src/app/legacy.rs +++ b/src/app/legacy.rs @@ -1,11 +1,68 @@ -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; + +use rusqlite::Connection; use crate::app::{config, index, Error}; +pub fn read_legacy_auth_secret(db_file_path: &PathBuf) -> Result<[u8; 32], Error> { + let connection = Connection::open(db_file_path)?; + let auth_secret: [u8; 32] = + connection.query_row("SELECT auth_secret FROM misc_settings", [], |row| { + row.get(0) + })?; + Ok(auth_secret) +} + pub fn read_legacy_config( db_file_path: &PathBuf, ) -> Result<Option<config::storage::Config>, Error> { - Ok(None) + let connection = Connection::open(db_file_path)?; + + // Album art pattern + let album_art_pattern: String = connection.query_row( + "SELECT index_album_art_pattern FROM misc_settings", + [], + |row| row.get(0), + )?; + + // Mount directories + let mut mount_dirs_statement = connection.prepare("SELECT source, name FROM mount_points")?; + let mount_dirs_rows = mount_dirs_statement.query_and_then([], |row| { + let source_string = row.get::<_, String>(0)?; + let Ok(source) = PathBuf::from_str(&source_string) else { + return Err(Error::InvalidDirectory(source_string)); + }; + Ok(config::storage::MountDir { + source, + name: row.get::<_, String>(1)?, + }) + })?; + let mut mount_dirs = vec![]; + for mount_dir_result in mount_dirs_rows { + mount_dirs.push(mount_dir_result?); + } + + // Users + let mut users_statement = connection.prepare("SELECT name, password_hash, admin FROM users")?; + let users_rows = users_statement.query_map([], |row| { + Ok(config::storage::User { + name: row.get(0)?, + admin: row.get(2)?, + initial_password: None, + hashed_password: row.get(1)?, + }) + })?; + let mut users = vec![]; + for user_result in users_rows { + users.push(user_result?); + } + + Ok(Some(config::storage::Config { + album_art_pattern: Some(album_art_pattern), + mount_dirs, + ddns_update_url: None, + users, + })) } pub fn read_legacy_playlists( @@ -22,3 +79,70 @@ pub async fn delete_legacy_db(db_file_path: &PathBuf) -> Result<(), Error> { .map_err(|e| Error::Io(db_file_path.clone(), e))?; Ok(()) } + +#[cfg(test)] +mod test { + use std::path::PathBuf; + + use super::*; + use crate::app::config; + + #[test] + fn can_read_auth_secret() { + let secret = + read_legacy_auth_secret(&PathBuf::from_iter(["test-data", "legacy_db_blank.sqlite"])) + .unwrap(); + assert_eq!( + secret, + [ + 0x8b as u8, 0x88, 0x50, 0x17, 0x20, 0x09, 0x7e, 0x60, 0x31, 0x80, 0xCE, 0xE3, 0xF0, + 0x5A, 0x00, 0xBC, 0x3A, 0xF4, 0xDC, 0xFD, 0x2E, 0xB7, 0x5D, 0x33, 0x5D, 0x81, 0x2F, + 0x9A, 0xB4, 0x3A, 0x27, 0x2D + ] + ); + } + + #[test] + fn can_read_blank_config() { + let actual = + read_legacy_config(&PathBuf::from_iter(["test-data", "legacy_db_blank.sqlite"])) + .unwrap() + .unwrap(); + + let expected = config::storage::Config { + album_art_pattern: Some("Folder.(jpeg|jpg|png)".to_owned()), + mount_dirs: vec![], + ddns_update_url: None, + users: vec![], + }; + + assert_eq!(actual, expected); + } + + #[test] + fn can_read_populated_config() { + let actual = read_legacy_config(&PathBuf::from_iter([ + "test-data", + "legacy_db_populated.sqlite", + ])) + .unwrap() + .unwrap(); + + let expected = config::storage::Config { + album_art_pattern: Some("Folder.(jpeg|jpg|png)".to_owned()), + mount_dirs: vec![config::storage::MountDir { + source: PathBuf::from_iter(["/", "home", "agersant", "music", "Electronic", "Bitpop"]), + name: "root".to_owned(), + }], + ddns_update_url: None, + users: vec![config::storage::User { + name: "example_user".to_owned(), + admin: Some(true), + initial_password: None, + hashed_password: Some("$pbkdf2-sha256$i=10000,l=32$feX5cP9SyQrZdBZsOQfO3Q$vqdraNc8ecco+CdFr+2Vp+PcIK6R75rs72YovNCwd7s".to_owned()), + }], + }; + + assert_eq!(actual, expected); + } +} diff --git a/src/server/error.rs b/src/server/error.rs index 8eaded9..11245f4 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -92,6 +92,8 @@ impl From<app::Error> for APIError { app::Error::ThreadJoining(_) => APIError::Internal, app::Error::Io(p, e) => APIError::Io(p, e), + app::Error::InvalidDirectory(_) => APIError::Internal, + app::Error::SQL(_) => APIError::Internal, app::Error::FileWatch(_) => APIError::Internal, app::Error::Ape(_) => APIError::Internal, app::Error::Id3(p, e) => APIError::ThumbnailId3Decoding(p, e),