From 5ec0b5f7a50496ad6368a7f0349cf5fb8593e89e Mon Sep 17 00:00:00 2001 From: Antoine Gersant <antoine.gersant@lesforges.org> Date: Tue, 8 Oct 2024 23:38:11 -0700 Subject: [PATCH] Write config changes to disk --- src/app.rs | 2 + src/app/config.rs | 104 +++++++++++++++++++++++++++----------- src/app/config/storage.rs | 4 +- src/app/config/user.rs | 2 +- src/server/axum/api.rs | 8 +-- src/server/error.rs | 1 + test-data/blank.toml | 1 + 7 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 test-data/blank.toml diff --git a/src/app.rs b/src/app.rs index 49c3d70..0443389 100644 --- a/src/app.rs +++ b/src/app.rs @@ -87,6 +87,8 @@ pub enum Error { #[error("Could not deserialize configuration: `{0}`")] ConfigDeserialization(toml::de::Error), + #[error("Could not serialize configuration: `{0}`")] + ConfigSerialization(toml::ser::Error), #[error("Could not deserialize collection")] IndexDeserializationError, #[error("Could not serialize collection")] diff --git a/src/app/config.rs b/src/app/config.rs index c7adac9..37539fd 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -114,8 +114,32 @@ impl Manager { #[cfg(test)] pub async fn apply(&self, config: storage::Config) -> Result<(), Error> { - *self.config.write().await = config.try_into()?; - // TODO persistence + self.mutate_fallible(|c| { + *c = config.try_into()?; + Ok(()) + }) + .await + } + + async fn mutate<F: FnOnce(&mut Config)>(&self, op: F) -> Result<(), Error> { + self.mutate_fallible(|c| { + op(c); + Ok(()) + }) + .await + } + + async fn mutate_fallible<F: FnOnce(&mut Config) -> Result<(), Error>>( + &self, + op: F, + ) -> Result<(), Error> { + let mut config = self.config.write().await; + op(&mut config)?; + let serialized = toml::ser::to_string_pretty::<storage::Config>(&config.clone().into()) + .map_err(Error::ConfigSerialization)?; + tokio::fs::write(&self.config_file_path, serialized.as_bytes()) + .await + .map_err(|e| Error::Io(self.config_file_path.clone(), e))?; Ok(()) } @@ -125,10 +149,11 @@ 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 set_index_sleep_duration(&self, duration: Duration) -> Result<(), Error> { + self.mutate(|c| { + c.reindex_every_n_seconds = Some(duration.as_secs()); + }) + .await } pub async fn get_index_album_art_pattern(&self) -> Regex { @@ -137,20 +162,22 @@ impl Manager { pattern.unwrap_or_else(|| Regex::new("Folder.(jpeg|jpg|png)").unwrap()) } - pub async fn set_index_album_art_pattern(&self, regex: Regex) { - let mut config = self.config.write().await; - config.album_art_pattern = Some(regex); - // TODO persistence + pub async fn set_index_album_art_pattern(&self, regex: Regex) -> Result<(), Error> { + self.mutate(|c| { + c.album_art_pattern = Some(regex); + }) + .await } pub async fn get_ddns_update_url(&self) -> Option<http::Uri> { 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); - // TODO persistence + pub async fn set_ddns_update_url(&self, url: http::Uri) -> Result<(), Error> { + self.mutate(|c| { + c.ddns_url = Some(url); + }) + .await } pub async fn get_users(&self) -> Vec<User> { @@ -169,9 +196,8 @@ impl Manager { password: &str, admin: bool, ) -> Result<(), Error> { - let mut config = self.config.write().await; - config.create_user(username, password, admin) - // TODO persistence + self.mutate_fallible(|c| c.create_user(username, password, admin)) + .await } pub async fn login(&self, username: &str, password: &str) -> Result<auth::Token, Error> { @@ -180,15 +206,13 @@ impl Manager { } pub async fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> { - let mut config = self.config.write().await; - config.set_is_admin(username, is_admin) - // TODO persistence + self.mutate_fallible(|c| c.set_is_admin(username, is_admin)) + .await } pub async fn set_password(&self, username: &str, password: &str) -> Result<(), Error> { - let mut config = self.config.write().await; - config.set_password(username, password) - // TODO persistence + self.mutate_fallible(|c| c.set_password(username, password)) + .await } pub async fn authenticate( @@ -200,10 +224,8 @@ impl Manager { config.authenticate(auth_token, scope, &self.auth_secret) } - pub async fn delete_user(&self, username: &str) { - let mut config = self.config.write().await; - config.delete_user(username); - // TODO persistence + pub async fn delete_user(&self, username: &str) -> Result<(), Error> { + self.mutate(|c| c.delete_user(username)).await } pub async fn get_mounts(&self) -> Vec<MountDir> { @@ -220,15 +242,25 @@ impl Manager { } pub async fn set_mounts(&self, mount_dirs: Vec<storage::MountDir>) -> Result<(), Error> { - self.config.write().await.set_mounts(mount_dirs) - // TODO persistence + self.mutate_fallible(|c| c.set_mounts(mount_dirs)).await } } #[cfg(test)] mod test { + use crate::app::test; + use crate::test_name; + use super::*; + #[tokio::test] + async fn blank_config_is_valid() { + let config_path = PathBuf::from_iter(["test-data", "blank.toml"]); + Manager::new(&config_path, auth::Secret([0; 32])) + .await + .unwrap(); + } + #[tokio::test] async fn can_read_config() { let config_path = PathBuf::from_iter(["test-data", "config.toml"]); @@ -257,4 +289,18 @@ mod test { ); assert!(config.users[0].hashed_password.is_some()); } + + #[tokio::test] + async fn can_write_config() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; + ctx.config_manager + .create_user("Walter", "example_password", false) + .await + .unwrap(); + + let manager = Manager::new(&ctx.config_manager.config_file_path, auth::Secret([0; 32])) + .await + .unwrap(); + assert!(manager.get_user("Walter").await.is_ok()); + } } diff --git a/src/app/config/storage.rs b/src/app/config/storage.rs index 73a503d..e6fd764 100644 --- a/src/app/config/storage.rs +++ b/src/app/config/storage.rs @@ -25,10 +25,10 @@ pub struct Config { pub reindex_every_n_seconds: Option<u64>, #[serde(skip_serializing_if = "Option::is_none")] pub album_art_pattern: Option<String>, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub mount_dirs: Vec<MountDir>, #[serde(skip_serializing_if = "Option::is_none")] pub ddns_url: Option<String>, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub users: Vec<User>, } diff --git a/src/app/config/user.rs b/src/app/config/user.rs index 7091bc3..f4a7504 100644 --- a/src/app/config/user.rs +++ b/src/app/config/user.rs @@ -175,7 +175,7 @@ mod test { .unwrap(); assert!(ctx.config_manager.get_user(TEST_USERNAME).await.is_ok()); - ctx.config_manager.delete_user(TEST_USERNAME).await; + ctx.config_manager.delete_user(TEST_USERNAME).await.unwrap(); assert!(ctx.config_manager.get_user(TEST_USERNAME).await.is_err()); } diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs index 4725778..6611b92 100644 --- a/src/server/axum/api.rs +++ b/src/server/axum/api.rs @@ -126,20 +126,20 @@ async fn put_settings( let Ok(regex) = Regex::new(&pattern) else { return Err(APIError::InvalidAlbumArtPattern); }; - config_manager.set_index_album_art_pattern(regex).await; + 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; + .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; + config_manager.set_ddns_update_url(uri).await?; } Ok(()) @@ -239,7 +239,7 @@ async fn delete_user( return Err(APIError::DeletingOwnAccount); } } - config_manager.delete_user(&name).await; + config_manager.delete_user(&name).await?; Ok(()) } diff --git a/src/server/error.rs b/src/server/error.rs index c959ebc..46379bb 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -124,6 +124,7 @@ impl From<app::Error> for APIError { app::Error::IndexAlbumArtPatternInvalid => APIError::InvalidAlbumArtPattern, app::Error::ConfigDeserialization(_) => APIError::Internal, + app::Error::ConfigSerialization(_) => APIError::Internal, app::Error::IndexDeserializationError => APIError::Internal, app::Error::IndexSerializationError => APIError::Internal, diff --git a/test-data/blank.toml b/test-data/blank.toml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test-data/blank.toml @@ -0,0 +1 @@ +