use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{fs, io, sync}; use crate::error::unexpected::{self, Intoable, Mappable}; use crate::{domain, origin}; use hex::ToHex; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] /// Values which the owner of a domain can configure when they install a domain. pub struct Config { pub origin_descr: origin::Descr, } impl Config { pub fn hash(&self) -> Result { let mut h = Sha256::new(); serde_json::to_writer(&mut h, self).or_unexpected()?; Ok(h.finalize().encode_hex::()) } } #[derive(thiserror::Error, Debug)] pub enum GetError { #[error("not found")] NotFound, #[error(transparent)] Unexpected(#[from] unexpected::Error), } #[derive(thiserror::Error, Debug)] pub enum SetError { #[error(transparent)] Unexpected(#[from] unexpected::Error), } /// Used in the return from all_domains from Store. pub type AllDomainsResult = Result; #[mockall::automock] pub trait Store { fn get(&self, domain: &domain::Name) -> Result; fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError>; fn all_domains(&self) -> AllDomainsResult>>; } pub trait BoxedStore: Store + Send + Sync + Clone {} struct FSStore { dir_path: PathBuf, } pub fn new(dir_path: &Path) -> io::Result { fs::create_dir_all(dir_path)?; Ok(sync::Arc::new(FSStore { dir_path: dir_path.into(), })) } impl FSStore { fn config_dir_path(&self, domain: &domain::Name) -> PathBuf { self.dir_path.join(domain.as_str()) } fn config_file_path(&self, domain: &domain::Name) -> PathBuf { self.config_dir_path(domain).join("config.json") } } impl BoxedStore for sync::Arc {} impl Store for sync::Arc { fn get(&self, domain: &domain::Name) -> Result { let path = self.config_file_path(domain); let config_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() { io::ErrorKind::NotFound => GetError::NotFound, _ => e .into_unexpected_while(format!("opening {}", path.display())) .into(), })?; let config = serde_json::from_reader(config_file) .map_unexpected_while(|| format!("json parsing {}", path.display()))?; Ok(config) } fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError> { let dir_path = self.config_dir_path(domain); fs::create_dir_all(dir_path.as_path()) .map_unexpected_while(|| format!("creating dir {}", dir_path.display()))?; let file_path = self.config_file_path(domain); let config_file = fs::File::create(file_path.as_path()) .map_unexpected_while(|| format!("creating file {}", file_path.display()))?; serde_json::to_writer(config_file, config) .map_unexpected_while(|| format!("writing config to {}", file_path.display()))?; Ok(()) } fn all_domains(&self) -> AllDomainsResult>> { Ok(fs::read_dir(&self.dir_path) .or_unexpected()? .map( |dir_entry_res: io::Result| -> AllDomainsResult { let domain = dir_entry_res.or_unexpected()?.file_name(); let domain = domain.to_str().ok_or(unexpected::Error::from( "couldn't convert os string to &str", ))?; domain::Name::from_str(domain) .map_unexpected_while(|| format!("parsing {domain} as domain name")) }, ) .collect()) } } #[cfg(test)] mod tests { use super::*; use crate::domain; use crate::origin::Descr; use std::str::FromStr; use tempdir::TempDir; #[test] fn basic() { let tmp_dir = TempDir::new("domain_config_store").unwrap(); let store = new(tmp_dir.path()).expect("store created"); let domain = domain::Name::from_str("foo.com").expect("domain parsed"); let config = Config { origin_descr: Descr::Git { url: "bar".to_string(), branch_name: "baz".to_string(), }, }; assert!(matches!( store.get(&domain), Err::(GetError::NotFound) )); store.set(&domain, &config).expect("config set"); assert_eq!(config, store.get(&domain).expect("config retrieved")); let new_config = Config { origin_descr: Descr::Git { url: "BAR".to_string(), branch_name: "BAZ".to_string(), }, }; store.set(&domain, &new_config).expect("config set"); assert_eq!(new_config, store.get(&domain).expect("config retrieved")); } }