Skip to main content

ryra_core/config/
mod.rs

1pub mod schema;
2pub mod status;
3
4use std::path::{Path, PathBuf};
5
6use crate::error::{Error, Result};
7use schema::Config;
8
9/// Resolved paths for all ryra config/state files.
10pub struct ConfigPaths {
11    pub config_dir: PathBuf,
12    pub config_file: PathBuf,
13    pub cache_dir: PathBuf,
14}
15
16impl ConfigPaths {
17    pub fn resolve() -> Result<Self> {
18        let home = dirs::home_dir()
19            .or_else(|| std::env::var("HOME").ok().map(PathBuf::from))
20            .ok_or(Error::HomeDirNotFound)?;
21        // RYRA_CONFIG_DIR overrides where preferences.toml lives (used by the
22        // test harness to isolate host runs from the user's real credentials).
23        let config_dir = match std::env::var_os(crate::paths::CONFIG_DIR_ENV) {
24            Some(dir) if !dir.is_empty() => PathBuf::from(dir),
25            _ => dirs::config_dir()
26                .unwrap_or_else(|| home.join(".config"))
27                .join("services"),
28        };
29        let cache_dir = dirs::cache_dir()
30            .unwrap_or_else(|| home.join(".cache"))
31            .join("services");
32        Ok(Self {
33            config_file: config_dir.join("preferences.toml"),
34            cache_dir,
35            config_dir,
36        })
37    }
38
39    pub fn ensure_cache_dir(&self) -> Result<()> {
40        ensure_dir(&self.cache_dir)
41    }
42
43    pub fn ensure_dirs(&self) -> Result<()> {
44        ensure_dir(&self.config_dir)?;
45        ensure_dir(&self.cache_dir)?;
46        Ok(())
47    }
48}
49
50fn ensure_dir(path: &Path) -> Result<()> {
51    std::fs::create_dir_all(path).map_err(|source| Error::DirCreate {
52        path: path.to_path_buf(),
53        source,
54    })
55}
56
57/// The version of this ryra binary, set at compile time from Cargo.toml.
58pub const VERSION: &str = env!("CARGO_PKG_VERSION");
59
60pub fn load_config(path: &Path) -> Result<Config> {
61    if !path.exists() {
62        return Err(Error::ConfigNotFound(path.to_path_buf()));
63    }
64    let contents = std::fs::read_to_string(path).map_err(|source| Error::FileRead {
65        path: path.to_path_buf(),
66        source,
67    })?;
68    let config: Config = toml::from_str(&contents).map_err(|source| Error::TomlParse {
69        path: path.to_path_buf(),
70        source,
71    })?;
72    if let Err(msg) = config.validate() {
73        return Err(Error::ConfigValidation(msg));
74    }
75    check_version(&config);
76    Ok(config)
77}
78
79/// Warn if the config was written by a newer major.minor version of ryra.
80fn check_version(config: &Config) {
81    let config_version = match &config.version {
82        Some(v) => v,
83        None => return, // pre-version config, accept silently
84    };
85    let binary = parse_major_minor(VERSION);
86    let config_v = parse_major_minor(config_version);
87    if let (Some((b_major, b_minor)), Some((c_major, c_minor))) = (binary, config_v)
88        && (c_major, c_minor) > (b_major, b_minor)
89    {
90        eprintln!(
91            "Warning: config was written by ryra {config_version}, \
92             but this is ryra {VERSION} — consider upgrading"
93        );
94    }
95}
96
97fn parse_major_minor(version: &str) -> Option<(u32, u32)> {
98    let mut parts = version.split('.');
99    let major = parts.next()?.parse().ok()?;
100    let minor = parts.next()?.parse().ok()?;
101    Some((major, minor))
102}
103
104/// Load config from path, returning a default config if the file doesn't exist.
105pub fn load_or_default(path: &Path) -> Result<Config> {
106    if !path.exists() {
107        return Ok(Config::default());
108    }
109    load_config(path)
110}
111
112pub fn save_config(path: &Path, config: &Config) -> Result<()> {
113    let mut config = config.clone();
114    config.version = Some(VERSION.to_string());
115    let contents = toml::to_string_pretty(&config)?;
116    // Atomic write with 0o600 from byte zero — config contains SMTP + auth
117    // credentials, so it must never be briefly world-readable and must never
118    // appear half-written if the process dies mid-save.
119    crate::system::atomic_write::atomic_write(path, contents.as_bytes(), 0o600)?;
120    Ok(())
121}