diff --git a/docker-compose-arm.yml b/docker-compose-arm.yml index 5b511ce..7bc33e6 100644 --- a/docker-compose-arm.yml +++ b/docker-compose-arm.yml @@ -13,6 +13,8 @@ services: environment: - RUST_LOG=info - WORTERBUCH_BIND_ADDRESS=0.0.0.0 + - WORTERBUCH_DATA_DIR=/data + - WORTERBUCH_PERSISTENCE_INTERVAL=5 volumes: - ./data:/data logging: diff --git a/docker-compose.yml b/docker-compose.yml index 545367a..e812231 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: environment: - RUST_LOG=info - WORTERBUCH_BIND_ADDRESS=0.0.0.0 + - WORTERBUCH_DATA_DIR=/data + - WORTERBUCH_PERSISTENCE_INTERVAL=5 volumes: - ./data:/data logging: diff --git a/worterbuch/src/config.rs b/worterbuch/src/config.rs index a7f5569..1f08738 100644 --- a/worterbuch/src/config.rs +++ b/worterbuch/src/config.rs @@ -61,14 +61,14 @@ impl Config { #[cfg(feature = "graphql")] if let Ok(val) = env::var("WORTERBUCH_GRAPHQL_PORT") { - self.graphql_port = val.parse()?; + self.graphql_port = val.parse().as_port()?; } if let Ok(val) = env::var("WORTERBUCH_BIND_ADDRESS") { self.bind_addr = val.parse()?; } - if let Ok(val) = env::var("WORTERBUCH_PERSISTENT_DATA") { + if let Ok(val) = env::var("WORTERBUCH_USE_PERSISTENCE") { self.persistent_data = val.to_lowercase() == "true"; } @@ -111,8 +111,7 @@ impl Default for Config { #[cfg(feature = "web")] key_path: None, persistent_data: false, - // TODO increase default persistence period - persistence_interval: Duration::from_secs(5), + persistence_interval: Duration::from_secs(30), data_dir: "./data".into(), } } diff --git a/worterbuch/src/main.rs b/worterbuch/src/main.rs index 691ec8e..5f3e617 100644 --- a/worterbuch/src/main.rs +++ b/worterbuch/src/main.rs @@ -36,7 +36,6 @@ async fn main() -> Result<()> { async fn run(msg: &str) -> Result<()> { let config = Config::new()?; let config_pers = config.clone(); - let config_ppers = config.clone(); App::new("worterbuch") .version(env!("CARGO_PKG_VERSION")) @@ -49,11 +48,16 @@ async fn run(msg: &str) -> Result<()> { log::debug!("Wildcard: {}", config.wildcard); log::debug!("Multi-Wildcard: {}", config.multi_wildcard); - // let restore_from_persistence = config.persistent_data; + let restore_from_persistence = config.persistent_data; - let worterbuch = Arc::new(RwLock::new(Worterbuch::with_config(config.clone()))); + let worterbuch = if restore_from_persistence { + persistence::load(config.clone()).await? + } else { + Worterbuch::with_config(config.clone()) + }; + + let worterbuch = Arc::new(RwLock::new(worterbuch)); let worterbuch_pers = worterbuch.clone(); - let worterbuch_ppers = worterbuch.clone(); spawn(persistence::periodic(worterbuch_pers, config_pers)); @@ -74,10 +78,10 @@ async fn run(msg: &str) -> Result<()> { #[cfg(not(feature = "docker"))] { - repl(worterbuch).await; + repl(worterbuch.clone()).await; } - persistence::once(worterbuch_ppers, config_ppers).await?; + persistence::once(worterbuch.clone(), config.clone()).await?; Ok(()) } diff --git a/worterbuch/src/persistence.rs b/worterbuch/src/persistence.rs index 2df96b1..79d8525 100644 --- a/worterbuch/src/persistence.rs +++ b/worterbuch/src/persistence.rs @@ -1,5 +1,6 @@ use crate::{config::Config, worterbuch::Worterbuch}; use anyhow::Result; +use sha2::{Digest, Sha256}; use std::{path::PathBuf, sync::Arc}; use tokio::{ fs::{self, File}, @@ -20,6 +21,70 @@ pub(crate) async fn periodic(worterbuch: Arc>, config: Config pub(crate) async fn once(worterbuch: Arc>, config: Config) -> Result<()> { let wb = worterbuch.read().await; + let (json_temp_path, json_path, sha_temp_path, sha_path) = file_paths(&config); + + let json = wb.export()?.to_string(); + + let mut hasher = Sha256::new(); + hasher.update(&json); + let result = hasher.finalize(); + let sha = hex::encode(&result); + + let mut file = File::create(&json_temp_path).await?; + file.write_all(json.as_bytes()).await?; + + let mut file = File::create(&sha_temp_path).await?; + file.write_all(sha.as_bytes()).await?; + + fs::copy(&json_temp_path, &json_path).await?; + fs::copy(&sha_temp_path, &sha_path).await?; + + Ok(()) +} + +pub(crate) async fn load(config: Config) -> Result { + log::info!("Restoring Wörterbuch form persistence …"); + + let (json_temp_path, json_path, sha_temp_path, sha_path) = file_paths(&config); + + if !json_path.exists() && !json_temp_path.exists() { + log::info!("No persistence file found, starting empty instance."); + return Ok(Worterbuch::with_config(config)); + } + + match try_load(&json_path, &sha_path, &config).await { + Ok(worterbuch) => { + log::info!("Wörterbuch successfully restored form persistence."); + Ok(worterbuch) + } + Err(e) => { + log::warn!("Default persistence file could not be loaded: {e}"); + log::info!("Restoring Wörterbuch form backup file …"); + let worterbuch = try_load(&json_temp_path, &sha_temp_path, &config).await?; + log::info!("Wörterbuch successfully restored form backup file."); + Ok(worterbuch) + } + } +} + +async fn try_load(json_path: &PathBuf, sha_path: &PathBuf, config: &Config) -> Result { + let json = fs::read_to_string(json_path).await?; + let sha = fs::read_to_string(sha_path).await?; + + let mut hasher = Sha256::new(); + hasher.update(&json); + let result = hasher.finalize(); + let loaded_sha = hex::encode(&result); + + if sha != loaded_sha { + Err(anyhow::Error::msg("checksums did not match")) + } else { + let worterbuch = Worterbuch::from_json(&json, config.to_owned())?; + Ok(worterbuch) + } +} + +fn file_paths(config: &Config) -> (PathBuf, PathBuf, PathBuf, PathBuf) { let dir = PathBuf::from(&config.data_dir); let mut json_temp_path = dir.clone(); @@ -31,14 +96,5 @@ pub(crate) async fn once(worterbuch: Arc>, config: Config) -> let mut sha_path = dir.clone(); sha_path.push(".store.sha"); - let mut file = File::create(&json_temp_path).await?; - let sha = wb.export_to_file(&mut file).await?; - let sha = hex::encode(&sha); - let mut file = File::create(&sha_temp_path).await?; - file.write_all(sha.as_bytes()).await?; - - fs::copy(&json_temp_path, &json_path).await?; - fs::copy(&sha_temp_path, &sha_path).await?; - - Ok(()) + (json_temp_path, json_path, sha_temp_path, sha_path) } diff --git a/worterbuch/src/worterbuch.rs b/worterbuch/src/worterbuch.rs index f2ae36b..5462865 100644 --- a/worterbuch/src/worterbuch.rs +++ b/worterbuch/src/worterbuch.rs @@ -9,7 +9,6 @@ use libworterbuch::{ }; use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_value, Value}; -use sha2::{Digest, Sha256}; use std::fmt::Display; use tokio::{ fs::File, @@ -45,6 +44,15 @@ impl Worterbuch { } } + pub fn from_json(json: &str, config: Config) -> WorterbuchResult { + let store: Store = from_str(json).context(|| format!("Error parsing JSON"))?; + Ok(Worterbuch { + config, + store, + ..Default::default() + }) + } + pub fn get<'a>(&self, key: impl AsRef) -> WorterbuchResult<(String, String)> { let path: Vec<&str> = key.as_ref().split(self.config.separator).collect(); @@ -193,20 +201,16 @@ impl Worterbuch { Ok(imported_values) } - pub async fn export_to_file(&self, file: &mut File) -> WorterbuchResult> { + pub async fn export_to_file(&self, file: &mut File) -> WorterbuchResult<()> { log::debug!("Exporting to {file:?} …"); let json = self.export()?.to_string(); let json_bytes = json.as_bytes(); - let mut hasher = Sha256::new(); - hasher.update(b"hello world"); - let result = hasher.finalize(); - file.write_all(json_bytes) .await .context(|| format!("Error writing to file {file:?}"))?; log::debug!("Done."); - Ok(result.as_slice().to_owned()) + Ok(()) } pub async fn import_from_file(&mut self, path: &Path) -> WorterbuchResult<()> {