Config refactor continued

This commit is contained in:
Antoine Gersant 2024-10-08 20:48:26 -07:00
parent c7a760e2c2
commit ae5da0f4f3
9 changed files with 103 additions and 63 deletions

View file

@ -5,12 +5,13 @@ use std::{
time::Duration,
};
use regex::Regex;
use tokio::sync::RwLock;
use crate::app::Error;
mod mounts;
mod raw;
pub mod storage;
mod user;
pub use mounts::*;
@ -27,27 +28,27 @@ pub struct Config {
pub users: HashMap<String, User>,
}
impl TryFrom<raw::Config> for Config {
impl TryFrom<storage::Config> for Config {
type Error = Error;
fn try_from(raw: raw::Config) -> Result<Self, Self::Error> {
fn try_from(c: storage::Config) -> Result<Self, Self::Error> {
let mut users: HashMap<String, User> = HashMap::new();
for user in raw.users {
if let Ok(user) = <raw::User as TryInto<User>>::try_into(user) {
for user in c.users {
if let Ok(user) = <storage::User as TryInto<User>>::try_into(user) {
users.insert(user.name.clone(), user);
}
}
let mount_dirs = raw
let mount_dirs = c
.mount_dirs
.into_iter()
.filter_map(|m| m.try_into().ok())
.collect();
Ok(Config {
reindex_every_n_seconds: raw.reindex_every_n_seconds, // TODO validate and warn
album_art_pattern: raw.album_art_pattern, // TODO validate and warn
ddns_url: raw.ddns_url, // TODO validate and warn
reindex_every_n_seconds: c.reindex_every_n_seconds, // TODO validate and warn
album_art_pattern: c.album_art_pattern, // TODO validate and warn
ddns_url: c.ddns_url, // TODO validate and warn
mount_dirs,
users,
})
@ -63,8 +64,8 @@ pub struct Manager {
impl Manager {
pub async fn new(config_file_path: &Path, auth_secret: auth::Secret) -> Result<Self, Error> {
let raw_config = raw::Config::default(); // TODO read from disk!!
let config = raw_config.try_into()?;
let config = storage::Config::default(); // TODO read from disk!!
let config: Config = config.try_into()?;
let manager = Self {
config_file_path: config_file_path.to_owned(),
config: Arc::new(RwLock::new(config)),
@ -73,8 +74,8 @@ impl Manager {
Ok(manager)
}
pub async fn apply(&self, raw_config: raw::Config) -> Result<(), Error> {
*self.config.write().await = raw_config.try_into()?;
pub async fn apply(&self, config: storage::Config) -> Result<(), Error> {
*self.config.write().await = config.try_into()?;
// TODO persistence
Ok(())
}
@ -85,16 +86,34 @@ impl Manager {
Duration::from_secs(seconds)
}
pub async fn set_index_sleep_duration(&self, duration: Duration) {
let mut config = self.config.write().await;
config.reindex_every_n_seconds = Some(duration.as_secs());
// TODO persistence
}
pub async fn get_index_album_art_pattern(&self) -> String {
let config = self.config.read().await;
let pattern = config.album_art_pattern.clone();
pattern.unwrap_or("Folder.(jpeg|jpg|png)".to_owned())
}
pub async fn set_index_album_art_pattern(&self, regex: Regex) {
let mut config = self.config.write().await;
config.album_art_pattern = Some(regex.as_str().to_owned());
// TODO persistence
}
pub async fn get_ddns_update_url(&self) -> Option<String> {
self.config.read().await.ddns_url.clone()
}
pub async fn set_ddns_update_url(&self, url: http::Uri) {
let mut config = self.config.write().await;
config.ddns_url = Some(url.to_string());
// TODO persistence
}
pub async fn get_users(&self) -> Vec<User> {
self.config.read().await.users.values().cloned().collect()
}
@ -160,7 +179,7 @@ impl Manager {
config.resolve_virtual_path(virtual_path)
}
pub async fn set_mounts(&self, mount_dirs: Vec<raw::MountDir>) {
pub async fn set_mounts(&self, mount_dirs: Vec<storage::MountDir>) {
self.config.write().await.set_mounts(mount_dirs);
// TODO persistence
}
@ -169,18 +188,20 @@ impl Manager {
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
use crate::app::config::storage::*;
use crate::app::test;
use crate::test_name;
#[tokio::test]
async fn can_apply_config() {
let ctx = test::ContextBuilder::new(test_name!()).build().await;
let new_config = raw::Config {
let new_config = Config {
reindex_every_n_seconds: Some(100),
album_art_pattern: Some("cool_pattern".to_owned()),
mount_dirs: vec![raw::MountDir {
source: "/home/music".to_owned(),
mount_dirs: vec![MountDir {
source: PathBuf::from("/home/music"),
name: "Library".to_owned(),
}],
ddns_url: Some("https://cooldns.com".to_owned()),

View file

@ -7,7 +7,7 @@ use regex::Regex;
use crate::app::Error;
use super::raw;
use super::storage;
use super::Config;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
@ -16,10 +16,10 @@ pub struct MountDir {
pub name: String,
}
impl TryFrom<raw::MountDir> for MountDir {
impl TryFrom<storage::MountDir> for MountDir {
type Error = Error;
fn try_from(mount_dir: raw::MountDir) -> Result<Self, Self::Error> {
fn try_from(mount_dir: storage::MountDir) -> Result<Self, Self::Error> {
// TODO validation
Ok(Self {
source: sanitize_path(&mount_dir.source),
@ -29,7 +29,7 @@ impl TryFrom<raw::MountDir> for MountDir {
}
impl Config {
pub fn set_mounts(&mut self, mount_dirs: Vec<raw::MountDir>) {
pub fn set_mounts(&mut self, mount_dirs: Vec<storage::MountDir>) {
self.mount_dirs = mount_dirs
.into_iter()
.filter_map(|m| m.try_into().ok())
@ -51,11 +51,12 @@ impl Config {
}
}
fn sanitize_path(source: &str) -> PathBuf {
fn sanitize_path(source: &PathBuf) -> PathBuf {
let path_string = source.to_string_lossy();
let separator_regex = Regex::new(r"\\|/").unwrap();
let mut correct_separator = String::new();
correct_separator.push(std::path::MAIN_SEPARATOR);
let path_string = separator_regex.replace_all(source, correct_separator.as_str());
let path_string = separator_regex.replace_all(&path_string, correct_separator.as_str());
PathBuf::from(path_string.deref())
}
@ -65,10 +66,10 @@ mod test {
#[test]
fn can_resolve_virtual_paths() {
let raw_config = raw::Config {
mount_dirs: vec![raw::MountDir {
let raw_config = storage::Config {
mount_dirs: vec![storage::MountDir {
name: "root".to_owned(),
source: "test_dir".to_owned(),
source: PathBuf::from("test_dir"),
}],
..Default::default()
};
@ -121,10 +122,10 @@ mod test {
};
for test in tests {
let raw_config = raw::Config {
mount_dirs: vec![raw::MountDir {
let raw_config = storage::Config {
mount_dirs: vec![storage::MountDir {
name: "root".to_owned(),
source: test.to_owned(),
source: PathBuf::from(test),
}],
..Default::default()
};

View file

@ -1,4 +1,7 @@
use std::{io::Read, path::Path};
use std::{
io::Read,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
@ -17,7 +20,7 @@ pub struct User {
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MountDir {
pub source: String,
pub source: PathBuf,
pub name: String,
}

View file

@ -1,6 +1,6 @@
use crate::app::{auth, Error};
use super::raw;
use super::storage;
use super::Config;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
@ -11,10 +11,10 @@ pub struct User {
pub hashed_password: String,
}
impl TryFrom<raw::User> for User {
impl TryFrom<storage::User> for User {
type Error = Error;
fn try_from(user: raw::User) -> Result<Self, Self::Error> {
fn try_from(user: storage::User) -> Result<Self, Self::Error> {
let hashed_password = match (&user.initial_password, &user.hashed_password) {
(_, Some(p)) => p.clone(),
(Some(p), None) => auth::hash_password(p)?,
@ -36,7 +36,7 @@ impl User {
}
}
impl From<User> for raw::User {
impl From<User> for storage::User {
fn from(user: User) -> Self {
Self {
name: user.name,
@ -137,7 +137,7 @@ mod test {
const TEST_PASSWORD: &str = "super_secret!";
fn adds_password_hashes() {
let user_in = raw::User {
let user_in = storage::User {
name: TEST_USERNAME.to_owned(),
initial_password: Some(TEST_PASSWORD.to_owned()),
..Default::default()
@ -145,7 +145,7 @@ mod test {
let user: User = user_in.try_into().unwrap();
let user_out: raw::User = user.into();
let user_out: storage::User = user.into();
assert_eq!(user_out.name, TEST_USERNAME);
assert_eq!(user_out.initial_password, Some(TEST_PASSWORD.to_owned()));
@ -153,13 +153,13 @@ mod test {
}
fn preserves_password_hashes() {
let user_in = raw::User {
let user_in = storage::User {
name: TEST_USERNAME.to_owned(),
hashed_password: Some("hash".to_owned()),
..Default::default()
};
let user: User = user_in.clone().try_into().unwrap();
let user_out: raw::User = user.into();
let user_out: storage::User = user.into();
assert_eq!(user_out, user_in);
}

View file

@ -1,5 +1,6 @@
use std::path::PathBuf;
use crate::app::config::storage::*;
use crate::app::{auth, config, index, ndb, playlist, scanner};
use crate::test::*;
@ -11,7 +12,7 @@ pub struct Context {
}
pub struct ContextBuilder {
config: config::Config,
config: Config,
pub test_directory: PathBuf,
}
@ -19,12 +20,12 @@ impl ContextBuilder {
pub fn new(test_name: String) -> Self {
Self {
test_directory: prepare_test_directory(test_name),
config: config::Config::default(),
config: Config::default(),
}
}
pub fn user(mut self, name: &str, password: &str, is_admin: bool) -> Self {
self.config.users.push(config::User {
self.config.users.push(User {
name: name.to_owned(),
initial_password: Some(password.to_owned()),
admin: Some(is_admin),
@ -34,9 +35,9 @@ impl ContextBuilder {
}
pub fn mount(mut self, name: &str, source: &str) -> Self {
self.config.mount_dirs.push(config::MountDir {
self.config.mount_dirs.push(MountDir {
name: name.to_owned(),
source: source.to_owned(),
source: PathBuf::from(source),
});
self
}

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{path::PathBuf, time::Duration};
use axum::{
extract::{DefaultBodyLimit, Path, Query, State},
@ -9,6 +9,7 @@ use axum::{
use axum_extra::headers::Range;
use axum_extra::TypedHeader;
use axum_range::{KnownSize, Ranged};
use regex::Regex;
use tower_http::{compression::CompressionLayer, CompressionLevel};
use crate::{
@ -116,9 +117,26 @@ async fn put_settings(
State(config_manager): State<config::Manager>,
Json(new_settings): Json<dto::NewSettings>,
) -> Result<(), APIError> {
settings_manager
.amend(&new_settings.to_owned().into())
.await?;
if let Some(pattern) = new_settings.album_art_pattern {
let Ok(regex) = Regex::new(&pattern) else {
return Err(APIError::InvalidAlbumArtPattern);
};
config_manager.set_index_album_art_pattern(regex).await;
}
if let Some(seconds) = new_settings.reindex_every_n_seconds {
config_manager
.set_index_sleep_duration(Duration::from_secs(seconds as u64))
.await;
}
if let Some(url_string) = new_settings.ddns_update_url {
let Ok(uri) = http::Uri::try_from(url_string) else {
return Err(APIError::InvalidDDNSURL);
};
config_manager.set_ddns_update_url(uri).await;
}
Ok(())
}
@ -136,7 +154,7 @@ async fn put_mount_dirs(
State(config_manager): State<config::Manager>,
new_mount_dirs: Json<Vec<dto::MountDir>>,
) -> Result<(), APIError> {
let new_mount_dirs: Vec<config::MountDir> =
let new_mount_dirs: Vec<config::storage::MountDir> =
new_mount_dirs.iter().cloned().map(|m| m.into()).collect();
config_manager.set_mounts(new_mount_dirs).await;
Ok(())

View file

@ -31,6 +31,8 @@ impl IntoResponse for APIError {
APIError::EmptyUsername => StatusCode::BAD_REQUEST,
APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED,
APIError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
APIError::InvalidAlbumArtPattern => StatusCode::BAD_REQUEST,
APIError::InvalidDDNSURL => StatusCode::BAD_REQUEST,
APIError::Io(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::OwnAdminPrivilegeRemoval => StatusCode::CONFLICT,
APIError::PasswordHashing => StatusCode::INTERNAL_SERVER_ERROR,

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::app::{config, index, peaks, playlist, thumbnail, user};
use crate::app::{config, index, peaks, playlist, thumbnail};
use std::{collections::HashMap, convert::From, path::PathBuf};
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
@ -127,16 +127,6 @@ pub struct NewUser {
pub admin: bool,
}
impl From<NewUser> for user::NewUser {
fn from(u: NewUser) -> Self {
Self {
name: u.name,
password: u.password,
admin: u.admin,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserUpdate {
pub new_password: Option<String>,
@ -145,11 +135,11 @@ pub struct UserUpdate {
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct MountDir {
pub source: String,
pub source: PathBuf,
pub name: String,
}
impl From<MountDir> for config::MountDir {
impl From<MountDir> for config::storage::MountDir {
fn from(m: MountDir) -> Self {
Self {
name: m.name,

View file

@ -49,6 +49,10 @@ pub enum APIError {
IncorrectCredentials,
#[error("Internal server error")]
Internal,
#[error("Could not parse album art pattern")]
InvalidAlbumArtPattern,
#[error("Could not parse DDNS update URL")]
InvalidDDNSURL,
#[error("File I/O error for `{0}`:\n\n{1}")]
Io(PathBuf, std::io::Error),
#[error("Cannot remove your own admin privilege")]