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}