use std::path::{Path, PathBuf};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
const APP: &str = "ks";
const CONFIG_FILE: &str = "config.toml";
const IDENTITY_FILE: &str = "identity.age";
const STORE_DIR: &str = "store";
const RECIPIENTS_FILE: &str = ".recipients";
pub const ENV_CONFIG: &str = "KS_CONFIG";
pub const ENV_DATA_DIR: &str = "KS_DATA_DIR";
pub const ENV_STORE_DIR: &str = "KS_STORE_DIR";
pub const ENV_IDENTITY: &str = "KS_IDENTITY";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Tunables {
pub session_ttl_secs: u64,
pub clipboard_clear_secs: u64,
}
impl Default for Tunables {
fn default() -> Self {
Self {
session_ttl_secs: 900,
clipboard_clear_secs: 45,
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub identity_path: PathBuf,
pub store_dir: PathBuf,
pub config_path: PathBuf,
pub tunables: Tunables,
}
impl Config {
pub fn load() -> Result<Self> {
let dirs = ProjectDirs::from("", "", APP).ok_or(Error::NoUserDir)?;
let default_config = dirs.config_dir().join(CONFIG_FILE);
let default_data = dirs.data_dir().to_path_buf();
let config_path = env_path(ENV_CONFIG).unwrap_or(default_config);
let data_dir = env_path(ENV_DATA_DIR).unwrap_or(default_data);
let identity_path = env_path(ENV_IDENTITY).unwrap_or_else(|| data_dir.join(IDENTITY_FILE));
let store_dir = env_path(ENV_STORE_DIR).unwrap_or_else(|| data_dir.join(STORE_DIR));
let tunables = if config_path.exists() {
let text = std::fs::read_to_string(&config_path)?;
toml::from_str(&text)?
} else {
Tunables::default()
};
Ok(Self {
identity_path,
store_dir,
config_path,
tunables,
})
}
#[must_use]
pub fn recipients_path(&self) -> PathBuf {
self.store_dir.join(RECIPIENTS_FILE)
}
pub fn save_tunables(&self) -> Result<()> {
if let Some(parent) = self.config_path.parent() {
std::fs::create_dir_all(parent)?;
}
let text = toml::to_string_pretty(&self.tunables)?;
std::fs::write(&self.config_path, text)?;
Ok(())
}
}
fn env_path(name: &str) -> Option<PathBuf> {
let raw = std::env::var(name).ok()?;
if raw.is_empty() {
None
} else {
Some(PathBuf::from(raw))
}
}
#[must_use]
pub fn store_id(store_dir: &Path) -> String {
use std::hash::{Hash as _, Hasher as _};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
store_dir.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tunables_default_values() {
let t = Tunables::default();
assert_eq!(t.session_ttl_secs, 900);
assert_eq!(t.clipboard_clear_secs, 45);
}
#[test]
fn store_id_is_stable() {
let a = store_id(Path::new("/tmp/ks/store"));
let b = store_id(Path::new("/tmp/ks/store"));
assert_eq!(a, b);
assert_eq!(a.len(), 16);
}
}