Config refactor continued
This commit is contained in:
parent
c7a760e2c2
commit
ae5da0f4f3
9 changed files with 103 additions and 63 deletions
|
@ -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()),
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")]
|
||||
|
|
Loading…
Add table
Reference in a new issue