forge_backup/
config.rs

1use std::path::{Path, PathBuf};
2
3use crate::{
4    args::Args,
5    error::{AppResult, ConfigError},
6};
7use directories::ProjectDirs;
8use serde::{Deserialize, Serialize};
9
10#[derive(Deserialize, Serialize, Debug, Default)]
11pub struct Config {
12    pub exclude_users: Vec<String>,
13    pub exclude_files: Vec<String>,
14    pub temp_folder: String,
15    pub home_dir: String,
16
17    pub hostname: String,
18
19    pub aws_profile: Option<String>,
20    pub s3_bucket: String,
21    pub s3_folder: String,
22
23    pub alert_email: String,
24    pub notify_on_success: bool,
25
26    pub mailgun_api_base: String,
27    pub mailgun_api_key: String,
28    pub mailgun_domain: String,
29    pub sender_email: String,
30}
31
32macro_rules! update_field {
33    ($arg:expr, $field:expr) => {
34        if let Some(value) = $arg {
35            $field = value;
36        }
37    };
38}
39impl std::fmt::Display for Config {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match toml::to_string_pretty(self) {
42            Ok(toml_string) => write!(f, "{toml_string}"),
43            Err(_) => write!(f, "Failed to conver config to TOML"),
44        }
45    }
46}
47
48impl Config {
49    pub fn load() -> AppResult<Self> {
50        let config = get_config_path()?;
51        let config = read_config_file(config)?;
52        let config = substitute_env_vars(&config)?;
53        parse_config(&config)
54    }
55
56    pub fn fallback() -> Self {
57        Self {
58            exclude_files: [
59                "**/ai1m-backups/*".into(),
60                "**/node_modules/*".into(),
61                "**/.cache/*".into(),
62                "**/.npm/*".into(),
63                "**/vendor/*".into(),
64            ]
65            .to_vec(),
66            home_dir: "/home".into(),
67            temp_folder: "/tmp/forge-backups".into(),
68            s3_folder: "daily".into(),
69            hostname: hostname::get()
70                .ok()
71                .and_then(|h| h.to_str().map(|s| s.to_string()))
72                .unwrap_or_default(),
73            ..Default::default()
74        }
75    }
76
77    pub fn update_from_args(&mut self, args: Args) {
78        update_field!(args.temp_folder, self.temp_folder);
79        if let Some(profile) = args.aws_profile {
80            self.aws_profile = Some(profile);
81        }
82        update_field!(args.s3_folder, self.s3_folder);
83        update_field!(args.s3_bucket, self.s3_bucket);
84        update_field!(args.home_dir, self.home_dir);
85        update_field!(args.alert_email, self.alert_email);
86        update_field!(args.notify_success, self.notify_on_success);
87        update_field!(args.exclude_users, self.exclude_users);
88        update_field!(args.hostname, self.hostname);
89    }
90}
91
92fn read_config_file<P: AsRef<Path>>(file: P) -> AppResult<String> {
93    let file_path = file.as_ref().to_path_buf();
94
95    let contents = std::fs::read_to_string(&file_path)
96        .map_err(|_| ConfigError::FileNotFoundError(file_path.clone()))?;
97
98    Ok(contents)
99}
100
101fn get_config_path() -> AppResult<PathBuf> {
102    if let Some(proj_dirs) = ProjectDirs::from("dev", "Popplestones", "Forge-Backup") {
103        let config_dir = proj_dirs.config_dir();
104        Ok(config_dir.join("forge_backup.toml"))
105    } else {
106        Err(ConfigError::ProjectPathNotFoundError.into())
107    }
108}
109
110fn parse_config(content: &str) -> AppResult<Config> {
111    Ok(toml::from_str::<Config>(content)
112        .map_err(|e| ConfigError::DeserializationError(content.into(), e))?)
113}
114
115fn substitute_env_vars(config_str: &str) -> AppResult<String> {
116    fn helper(config_str: &str) -> AppResult<String> {
117        if let Some(start) = config_str.find('$') {
118            if let Some(end) = config_str[start..].find('}') {
119                let end = start + end;
120                if config_str.as_bytes()[start + 1] == b'{' {
121                    let var_name = &config_str[start + 2..end];
122                    if let Ok(env_value) = std::env::var(var_name) {
123                        let before = &config_str[..start];
124                        let after = &config_str[end + 1..];
125                        let substituted = format!("{before}{env_value}{after}");
126                        return helper(&substituted);
127                    } else {
128                        Err(ConfigError::EnvVarSubstitutionError(var_name.into()))?;
129                    }
130                }
131            }
132        }
133        Ok(config_str.to_string())
134    }
135    helper(config_str)
136}