use std::path::{Path, PathBuf};
use crate::{
args::Args,
error::{AppResult, ConfigError},
};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, Default)]
pub struct Config {
pub exclude_users: Vec<String>,
pub exclude_files: Vec<String>,
pub temp_folder: String,
pub home_dir: String,
pub hostname: String,
pub aws_profile: Option<String>,
pub s3_bucket: String,
pub s3_folder: String,
pub alert_email: String,
pub notify_on_success: bool,
pub mailgun_api_base: String,
pub mailgun_api_key: String,
pub mailgun_domain: String,
pub sender_email: String,
}
macro_rules! update_field {
($arg:expr, $field:expr) => {
if let Some(value) = $arg {
$field = value;
}
};
}
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match toml::to_string_pretty(self) {
Ok(toml_string) => write!(f, "{toml_string}"),
Err(_) => write!(f, "Failed to conver config to TOML"),
}
}
}
impl Config {
pub fn load() -> AppResult<Self> {
let config = get_config_path()?;
let config = read_config_file(config)?;
let config = substitute_env_vars(&config)?;
parse_config(&config)
}
pub fn fallback() -> Self {
Self {
exclude_files: [
"**/ai1m-backups/*".into(),
"**/node_modules/*".into(),
"**/.cache/*".into(),
"**/.npm/*".into(),
"**/vendor/*".into(),
]
.to_vec(),
home_dir: "/home".into(),
temp_folder: "/tmp/forge-backups".into(),
s3_folder: "daily".into(),
hostname: hostname::get()
.ok()
.and_then(|h| h.to_str().map(|s| s.to_string()))
.unwrap_or_default(),
..Default::default()
}
}
pub fn update_from_args(&mut self, args: Args) {
update_field!(args.temp_folder, self.temp_folder);
if let Some(profile) = args.aws_profile {
self.aws_profile = Some(profile);
}
update_field!(args.s3_folder, self.s3_folder);
update_field!(args.s3_bucket, self.s3_bucket);
update_field!(args.home_dir, self.home_dir);
update_field!(args.alert_email, self.alert_email);
update_field!(args.notify_success, self.notify_on_success);
update_field!(args.exclude_users, self.exclude_users);
update_field!(args.hostname, self.hostname);
}
}
fn read_config_file<P: AsRef<Path>>(file: P) -> AppResult<String> {
let file_path = file.as_ref().to_path_buf();
let contents = std::fs::read_to_string(&file_path)
.map_err(|_| ConfigError::FileNotFoundError(file_path.clone()))?;
Ok(contents)
}
fn get_config_path() -> AppResult<PathBuf> {
if let Some(proj_dirs) = ProjectDirs::from("dev", "Popplestones", "Forge-Backup") {
let config_dir = proj_dirs.config_dir();
Ok(config_dir.join("forge_backup.toml"))
} else {
Err(ConfigError::ProjectPathNotFoundError.into())
}
}
fn parse_config(content: &str) -> AppResult<Config> {
Ok(toml::from_str::<Config>(content)
.map_err(|e| ConfigError::DeserializationError(content.into(), e))?)
}
fn substitute_env_vars(config_str: &str) -> AppResult<String> {
fn helper(config_str: &str) -> AppResult<String> {
if let Some(start) = config_str.find('$') {
if let Some(end) = config_str[start..].find('}') {
let end = start + end;
if config_str.as_bytes()[start + 1] == b'{' {
let var_name = &config_str[start + 2..end];
if let Ok(env_value) = std::env::var(var_name) {
let before = &config_str[..start];
let after = &config_str[end + 1..];
let substituted = format!("{before}{env_value}{after}");
return helper(&substituted);
} else {
Err(ConfigError::EnvVarSubstitutionError(var_name.into()))?;
}
}
}
}
Ok(config_str.to_string())
}
helper(config_str)
}