use anyhow::{Context, Result};
use directories::ProjectDirs;
use std::path::PathBuf;
pub trait AppContext: Send + Sync + std::fmt::Debug {
fn get_data_dir(&self) -> Result<PathBuf>;
fn get_config_dir(&self) -> Result<PathBuf>;
fn get_cache_dir(&self) -> Result<PathBuf>;
fn get_config_file_path(&self) -> Result<PathBuf> {
Ok(self.get_config_dir()?.join("config.toml"))
}
fn get_journal_path(&self) -> Option<PathBuf> {
self.get_data_dir().ok().map(|p| p.join("journal.json"))
}
fn get_local_task_path(&self) -> Option<PathBuf> {
self.get_data_dir().ok().map(|p| p.join("local.json"))
}
fn get_alarm_index_path(&self) -> Option<PathBuf> {
self.get_data_dir().ok().map(|p| p.join("alarm_index.json"))
}
}
#[derive(Clone, Debug)]
pub struct StandardContext {
override_root: Option<PathBuf>,
data_dir_override: Option<PathBuf>,
}
impl StandardContext {
pub fn new(override_root: Option<PathBuf>) -> Self {
let data_dir_override = if override_root.is_some() {
None
} else {
Self::read_data_dir_from_config()
};
Self {
override_root,
data_dir_override,
}
}
fn read_data_dir_from_config() -> Option<PathBuf> {
let proj = Self::get_proj_dirs()?;
let config_path = proj.config_dir().join("config.toml");
if !config_path.exists() {
return None;
}
let contents = std::fs::read_to_string(&config_path).ok()?;
#[derive(serde::Deserialize)]
struct DataDirOnly {
#[serde(default)]
data_dir: Option<String>,
}
let parsed: DataDirOnly = toml::from_str(&contents).ok()?;
parsed.data_dir.map(|s| Self::expand_tilde(&s))
}
fn expand_tilde(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME")
{
return PathBuf::from(home).join(rest);
}
if path == "~"
&& let Some(home) = std::env::var_os("HOME")
{
return PathBuf::from(home);
}
PathBuf::from(path)
}
fn ensure_exists(path: PathBuf) -> Result<PathBuf> {
if !path.exists() {
std::fs::create_dir_all(&path)
.with_context(|| format!("Failed to create directory: {:?}", path))?;
}
Ok(path)
}
fn get_proj_dirs() -> Option<ProjectDirs> {
ProjectDirs::from("com", "cfait", "cfait")
.or_else(|| ProjectDirs::from("com", "trougnouf", "cfait"))
}
}
impl AppContext for StandardContext {
fn get_data_dir(&self) -> Result<PathBuf> {
if let Some(root) = &self.override_root {
return Self::ensure_exists(root.join("data"));
}
if let Some(dir) = &self.data_dir_override {
return Self::ensure_exists(dir.clone());
}
let proj = Self::get_proj_dirs().ok_or_else(|| anyhow::anyhow!("No home directory"))?;
Self::ensure_exists(proj.data_dir().to_path_buf())
}
fn get_config_dir(&self) -> Result<PathBuf> {
if let Some(root) = &self.override_root {
return Self::ensure_exists(root.join("config"));
}
let proj = Self::get_proj_dirs().ok_or_else(|| anyhow::anyhow!("No home directory"))?;
Self::ensure_exists(proj.config_dir().to_path_buf())
}
fn get_cache_dir(&self) -> Result<PathBuf> {
if let Some(root) = &self.override_root {
return Self::ensure_exists(root.join("cache"));
}
let proj = Self::get_proj_dirs().ok_or_else(|| anyhow::anyhow!("No home directory"))?;
Self::ensure_exists(proj.cache_dir().to_path_buf())
}
}
#[derive(Clone, Debug)]
pub struct TestContext {
pub root: PathBuf,
}
impl TestContext {
pub fn new() -> Self {
let uuid = uuid::Uuid::new_v4();
let root = std::env::temp_dir().join(format!("cfait_test_{}", uuid));
std::fs::create_dir_all(&root).expect("failed to create TestContext temp dir");
Self { root }
}
}
impl Default for TestContext {
fn default() -> Self {
Self::new()
}
}
impl AppContext for TestContext {
fn get_data_dir(&self) -> Result<PathBuf> {
let p = self.root.join("data");
std::fs::create_dir_all(&p)?;
Ok(p)
}
fn get_config_dir(&self) -> Result<PathBuf> {
let p = self.root.join("config");
std::fs::create_dir_all(&p)?;
Ok(p)
}
fn get_cache_dir(&self) -> Result<PathBuf> {
let p = self.root.join("cache");
std::fs::create_dir_all(&p)?;
Ok(p)
}
}
impl Drop for TestContext {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
pub type SharedContext = std::sync::Arc<dyn AppContext>;