/* * Copyright 2025 Bryson Steck * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::common; use crate::refractr::Refractr; use core::fmt; use serde_derive::Deserialize; use std::env; use std::fs::{self, File, Metadata}; use std::io::Read; use std::path::PathBuf; use toml; pub struct ConfigFile { pub path: String, pub file: Metadata, pub config: Config, } impl fmt::Display for ConfigFile { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut branches_list = String::from("["); for i in 0..self.config.branches.len() { branches_list = format!("{}{}", branches_list, &self.config.branches[i]); if i < self.config.branches.len() - 1 { branches_list.push_str(", "); } } branches_list.push(']'); let mut to_list = String::from("["); for i in 0..self.config.to.len() { to_list.push_str(&self.config.to[i]); if i < self.config.to.len() - 1 { to_list.push_str(", "); } } to_list.push(']'); let work_dir_path = match &self.config.work_dir { None => { if cfg!(windows) { format!( "Using default \"{}\\refractr\"", match env::var("TEMP") { Ok(val) => val, Err(_) => format!("This shouldn't happen!"), } ) } else { format!("Using default: /tmp/refractr") } }, Some(path) => format!("{}", path), }; let schedule_interval = match self.config.schedule.interval { None => { if !self.config.schedule.enabled { String::from("") } else { String::from("This shouldn't happen!\n") } }, Some(int) => format!( "\n Scheduled interval in seconds: {}", int.to_string() ), }; write!( f, "Config file: \"{}\"\n \ Is a file: {}\n \ Read only: {}\n \ Configuration:\n \ Git repo to clone: {}\n \ Git repos to push clone to: {}\n \ Branches of clone to push: {}\n \ Working directory: {}\n \ SSH key for pushing clone: {}\n \ Schedule enabled: {}\ {}", self.path, self.file.is_file(), self.file.permissions().readonly(), self.config.from, to_list, branches_list, work_dir_path, self.config.git.ssh_identity_file, self.config.schedule.enabled, schedule_interval ) } } #[derive(PartialEq, Deserialize)] pub struct Config { pub from: String, pub to: Vec, pub branches: Vec, pub work_dir: Option, pub git: Git, pub schedule: Schedule, } #[derive(PartialEq, Deserialize)] pub struct Git { pub ssh_identity_file: String, } #[derive(PartialEq, Deserialize)] pub struct Schedule { pub enabled: bool, pub interval: Option, } pub fn read_config(paths: Vec, refractr: &Refractr) -> Result, String> { let mut config_files: Vec = vec![]; for path in paths { common::verbose( refractr.verbose, 1, format!( "Reading config file: \"{}\"", String::from(path.to_string_lossy()) ), ); let mut data = String::new(); let mut file = match File::open(path.as_path()) { Err(e) => { return Err(format!( "unable to open {}: {}", path.as_path().display(), e )) }, Ok(file) => file, }; if let Err(e) = file.read_to_string(&mut data) { return Err(format!( "unable to read {}: {}", path.as_path().display(), e )); } let config: Config = match toml::from_str(&data) { Ok(c) => c, Err(e) => { return Err(format!( "issues parsing toml file {}: {}", path.as_path().display(), e )) }, }; let config_file = ConfigFile { path: match fs::canonicalize(&path) { Err(_) => { return Err(format!( "cannot get absolute path of config file: {}", path.as_path().display() )) }, Ok(abs) => abs.to_string_lossy().to_string(), }, file: match fs::metadata(&path) { Err(_) => { return Err(format!( "cannot obtain metadata for config file: {}", path.as_path().display() )) }, Ok(metadata) => metadata, }, config: match verify_config(&config) { Err(e) => { return Err(format!( "invalid config {}: {}", path.as_path().display(), e )) }, Ok(_) => config, }, }; let mut dup = false; for i in &config_files { if i.path == config_file.path { common::warning(format!( "skipping config file \"{}\" as it was already read", path.as_path().display() )); dup = true; break; } else if i.config == config_file.config { common::warning(format!( "config files \"{}\" and \"{}\" appear to have the same config", i.path, config_file.path )); } } if !dup { config_files.push(config_file); } } return Ok(config_files); } fn verify_config(config: &Config) -> Result<(), String> { if config.schedule.enabled { match config.schedule.interval { Some(i) => { if i < 60 { return Err(format!("schedule is enabled, but less than 60")); } }, None => return Err(format!("schedule is enabled, but no interval was defined")), } } match &config.work_dir { Some(path) => format!("{}", path), None => { if cfg!(windows) { match env::var("TEMP") { Ok(val) => val, Err(_) => return Err(format!("cannot determine the default temp dir")), } } else { format!("/tmp/refractr") } }, }; if !&config.from.starts_with("ssh://") && !&config.from.starts_with("https://") && !&config.from.starts_with("http://") { return Err(format!("'from' value does not use a supported protocol")); } Ok(()) }