161 lines
4.8 KiB
Rust
161 lines
4.8 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
use std::{fs, io};
|
|
|
|
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<String, unexpected::Error> {
|
|
let mut h = Sha256::new();
|
|
serde_json::to_writer(&mut h, self).or_unexpected()?;
|
|
Ok(h.finalize().encode_hex::<String>())
|
|
}
|
|
}
|
|
|
|
#[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),
|
|
}
|
|
|
|
#[mockall::automock]
|
|
pub trait Store {
|
|
fn get(&self, domain: &domain::Name) -> Result<Config, GetError>;
|
|
fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError>;
|
|
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
|
}
|
|
|
|
pub struct FSStore {
|
|
dir_path: PathBuf,
|
|
}
|
|
|
|
impl FSStore {
|
|
pub fn new(dir_path: &Path) -> io::Result<Self> {
|
|
fs::create_dir_all(dir_path)?;
|
|
Ok(Self {
|
|
dir_path: dir_path.into(),
|
|
})
|
|
}
|
|
|
|
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 Store for FSStore {
|
|
fn get(&self, domain: &domain::Name) -> Result<Config, GetError> {
|
|
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) -> Result<Vec<domain::Name>, unexpected::Error> {
|
|
fs::read_dir(&self.dir_path)
|
|
.or_unexpected()?
|
|
.map(
|
|
|dir_entry_res: io::Result<fs::DirEntry>| -> Result<domain::Name, unexpected::Error> {
|
|
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"))
|
|
},
|
|
)
|
|
.try_collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{Store, *};
|
|
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 = FSStore::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::<Config, GetError>(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"));
|
|
}
|
|
}
|