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        let config_dir = dirs::config_dir()
22            .unwrap_or_else(|| home.join(".config"))
23            .join("services");
24        let cache_dir = dirs::cache_dir()
25            .unwrap_or_else(|| home.join(".cache"))
26            .join("services");
27        Ok(Self {
28            config_file: config_dir.join("preferences.toml"),
29            cache_dir,
30            config_dir,
31        })
32    }
33
34    pub fn ensure_cache_dir(&self) -> Result<()> {
35        ensure_dir(&self.cache_dir)
36    }
37
38    pub fn ensure_dirs(&self) -> Result<()> {
39        ensure_dir(&self.config_dir)?;
40        ensure_dir(&self.cache_dir)?;
41        Ok(())
42    }
43}
44
45fn ensure_dir(path: &Path) -> Result<()> {
46    std::fs::create_dir_all(path).map_err(|source| Error::DirCreate {
47        path: path.to_path_buf(),
48        source,
49    })
50}
51
52/// The version of this ryra binary, set at compile time from Cargo.toml.
53pub const VERSION: &str = env!("CARGO_PKG_VERSION");
54
55pub fn load_config(path: &Path) -> Result<Config> {
56    if !path.exists() {
57        return Err(Error::ConfigNotFound(path.to_path_buf()));
58    }
59    let contents = std::fs::read_to_string(path).map_err(|source| Error::FileRead {
60        path: path.to_path_buf(),
61        source,
62    })?;
63    let config: Config = toml::from_str(&contents).map_err(|source| Error::TomlParse {
64        path: path.to_path_buf(),
65        source,
66    })?;
67    if let Err(msg) = config.validate() {
68        return Err(Error::ConfigValidation(msg));
69    }
70    check_version(&config);
71    Ok(config)
72}
73
74/// Warn if the config was written by a newer major.minor version of ryra.
75fn check_version(config: &Config) {
76    let config_version = match &config.version {
77        Some(v) => v,
78        None => return, // pre-version config, accept silently
79    };
80    let binary = parse_major_minor(VERSION);
81    let config_v = parse_major_minor(config_version);
82    if let (Some((b_major, b_minor)), Some((c_major, c_minor))) = (binary, config_v)
83        && (c_major, c_minor) > (b_major, b_minor)
84    {
85        eprintln!(
86            "Warning: config was written by ryra {config_version}, \
87             but this is ryra {VERSION} — consider upgrading"
88        );
89    }
90}
91
92fn parse_major_minor(version: &str) -> Option<(u32, u32)> {
93    let mut parts = version.split('.');
94    let major = parts.next()?.parse().ok()?;
95    let minor = parts.next()?.parse().ok()?;
96    Some((major, minor))
97}
98
99/// Load config from path, returning a default config if the file doesn't exist.
100pub fn load_or_default(path: &Path) -> Result<Config> {
101    if !path.exists() {
102        return Ok(Config::default());
103    }
104    load_config(path)
105}
106
107pub fn save_config(path: &Path, config: &Config) -> Result<()> {
108    let mut config = config.clone();
109    config.version = Some(VERSION.to_string());
110    let contents = toml::to_string_pretty(&config)?;
111    // Atomic write with 0o600 from byte zero — config contains SMTP + auth
112    // credentials, so it must never be briefly world-readable and must never
113    // appear half-written if the process dies mid-save.
114    crate::system::atomic_write::atomic_write(path, contents.as_bytes(), 0o600)?;
115    Ok(())
116}