Settings and auth secret migration

This commit is contained in:
Antoine Gersant 2025-01-07 21:51:43 -08:00
parent 73dc59f833
commit 3ad5e97b75
5 changed files with 245 additions and 4 deletions

59
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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()
);

View file

@ -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);
}
}

View file

@ -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),