use serde::{Deserialize, Serialize}; use std::{env, ffi::OsString, fs, io, path::PathBuf}; pub(crate) fn cache_dir() -> Option { let dir = env::var_os("STATE_DIRECTORY").or_else(|| { env::var_os("HOME").and_then(|dir| { let mut dir: PathBuf = dir.into(); dir.push(".cache"); dir.push("rebterlai"); Some(OsString::from(dir)) }) }); dir } pub(crate) fn state_file(name: &str) -> Option { let mut dir: PathBuf = cache_dir()?.into(); dir.push(&format!("{}.json", name)); Some(dir) } /// The logtail configuration file. This contains cached copies of public and private /// IDs, however in practice the most read value is the private ID. #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] pub struct Config { // TODO(Xe): make this use a custom serde format, see https://serde.rs/custom-date-format.html private_id: String, public_id: String, } impl Config { /// Constructs a brand new configuration, including a rng'd private ID and its public /// counterpart. pub fn new() -> Self { let private_id = logtail::PrivateID::new(); Self { private_id: private_id.as_hex(), public_id: private_id.as_public().as_hex(), } } /// Returns the logtail private ID for this Config. pub fn private_id(self) -> logtail::PrivateID { logtail::PrivateID::from_hex(self.private_id).unwrap() } /// Loads the relevant configuration file off disk, and creates a new one if it /// doesn't already exist. This function is idempotent. pub fn load(collection: S) -> io::Result where S: Into, { let dir = cache_dir() .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "can't find directory"))?; if let Err(_) = fs::metadata(&dir) { fs::create_dir_all(&dir)?; } let fname = state_file(&collection.into()).ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "can't derive logtail config filename", ) })?; match fs::metadata(&fname) { Ok(_) => { let fin = fs::File::open(&fname)?; let cfg = serde_json::from_reader(io::BufReader::new(fin)) .or_else(|why| Err(io::Error::new(io::ErrorKind::Other, why)))?; Ok(cfg) } Err(_) => { let cfg = Self::new(); let mut fout = fs::File::create(&fname)?; serde_json::to_writer(&mut fout, &cfg) .or_else(|why| Err(io::Error::new(io::ErrorKind::Other, why)))?; Ok(cfg) } } } } #[cfg(test)] mod tests { use super::*; use tempdir::TempDir; #[test] #[serial_test::serial] fn cache_dir() { let home = TempDir::new("cache").unwrap(); std::env::set_var("STATE_DIRECTORY", home.path().to_str().unwrap()); let dir = super::cache_dir(); assert!(dir.is_some()); assert_eq!(dir.unwrap(), home.path().to_str().unwrap()); home.close().unwrap(); } #[test] #[serial_test::serial] fn new_and_load() { let home = TempDir::new("cache").unwrap(); std::env::set_var("STATE_DIRECTORY", home.path().to_str().unwrap()); let cfg = Config::load("foo.bar").unwrap(); let cfg2 = Config::load("foo.bar").unwrap(); assert_eq!(cfg, cfg2); home.close().unwrap(); } }