Settings and auth secret migration
This commit is contained in:
parent
73dc59f833
commit
3ad5e97b75
5 changed files with 245 additions and 4 deletions
59
Cargo.lock
generated
59
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
59
src/app.rs
59
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()
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Reference in a new issue