Working on refactor to use hashmaps
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
notplants 2022-05-09 15:53:03 +02:00
parent 29804b0dce
commit 5b86f754f4
5 changed files with 171 additions and 140 deletions

View File

@ -3,14 +3,19 @@
//! Different PeachCloud microservices import peach-lib, so that they can share //! Different PeachCloud microservices import peach-lib, so that they can share
//! this interface. //! this interface.
//! //!
//! Config values are looked up from three locations in this order by key name:
//! 1. from environmental variables
//! 2. from a configuration file
//! 3. from default values
//!
//! The configuration file is located at: "/var/lib/peachcloud/config.yml" //! The configuration file is located at: "/var/lib/peachcloud/config.yml"
//! unless its path is configured by setting PEACH_CONFIG_PATH env variable.
use std::{env, fs}; use std::{env, fs};
use std::collections::HashMap;
use lazy_static::lazy_static;
use fslock::LockFile; use fslock::LockFile;
use log::debug; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::error::PeachError; use crate::error::PeachError;
@ -27,161 +32,168 @@ lazy_static! {
DEFAULT_YAML_PATH.to_string() DEFAULT_YAML_PATH.to_string()
} }
}; };
// lock file (used to avoid race conditions during config reading & writing)
// the lock file path is the config file path + ".lock" // the lock file path is the config file path + ".lock"
static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH); static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH);
} }
// lock file (used to avoid race conditions during config reading & writing) // primary interface for getting config values
pub fn get_config_value(key: &str) -> Result<String, PeachError> {
// first check if it is an environmental variable
// default values if let Ok(val) = env::var(key) {
pub const DEFAULT_DYN_SERVER_ADDRESS: &str = "http://dynserver.dyn.peachcloud.org"; Ok(val)
pub const DEFAULT_DYN_NAMESERVER: &str = "ns.peachcloud.org"; } else {
// then check disc
// we make use of Serde default values in order to make PeachCloud let peach_config_on_disc = load_peach_config_from_disc()?;
// robust and keep running even with a not fully complete config.yml let val = peach_config_on_disc.get(key);
// main type which represents all peachcloud configurations // then check defaults
#[derive(Debug, PartialEq, Serialize, Deserialize)] match val {
pub struct PeachConfig { Some(v) => Ok(v.to_string()),
#[serde(default)] None => {
pub external_domain: String, match get_peach_config_defaults().get(key) {
#[serde(default)] Some(v) => Ok(v.to_string()),
pub dyn_domain: String, None => {
#[serde(default)] Err(PeachError::InvalidKey { msg: format!("No default config value set for key: {}", key) })
pub dyn_dns_server_address: String, }
#[serde(default)] }
pub dyn_use_custom_server: bool, }
#[serde(default)] }
pub dyn_nameserver: String, }
#[serde(default)]
pub dyn_tsig_key_path: String,
#[serde(default)] // default is false
pub dyn_enabled: bool,
#[serde(default)] // default is empty vector
pub ssb_admin_ids: Vec<String>,
#[serde(default)]
pub admin_password_hash: String,
#[serde(default)]
pub temporary_password_hash: String,
} }
// helper functions for serializing and deserializing PeachConfig from disc
pub fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> { // Default values for PeachCloud configs which are used for any key which is not set
// via an environment variable or in a saved configuration file.
pub fn get_peach_config_defaults() -> HashMap<String, String> {
let peach_config_defaults: HashMap<String, String> = HashMap::from([
("STANDALONE_MODE".to_string(), "true".to_string()),
("DISABLE_AUTH".to_string(), "false".to_string()),
("ADDR".to_string(), "127.0.0.1".to_string()),
("PORT".to_string(), "8000".to_string()),
("EXTERNAL_DOMAIN".to_string(), "".to_string()),
("DYN_DOMAIN".to_string(), "".to_string()),
("DYN_DNS_SERVER_ADDRESS".to_string(), "http://dynserver.dyn.peachcloud.org".to_string()),
("DYN_USE_CUSTOM_SERVER".to_string(), "true".to_string()),
("DYN_TSIG_KEY_PATH".to_string(), "".to_string()),
("DYN_NAMESERVER".to_string(), "ns.peachcloud.org".to_string()),
("DYN_ENABLED".to_string(), "false".to_string()),
("SSB_ADMIN_IDS".to_string(), "[]".to_string()),
("ADMIN_PASSWORD_HASH".to_string(), "146".to_string()),
("TEMPORARY_PASSWORD_HASH".to_string(), "".to_string()),
("GO_SBOT_DATADIR".to_string(), "".to_string()),
("PEACH_CONFIGDIR".to_string(), "/var/lib/peachcloud".to_string()),
]);
peach_config_defaults
}
// helper function to load PeachCloud configuration files saved to disc
pub fn load_peach_config_from_disc() -> Result<HashMap<String, String>, PeachError> {
let peach_config : HashMap<String, String> = HashMap::new();
// TODO: implement
Ok(peach_config)
}
pub fn save_peach_config_to_disc(peach_config: HashMap<String, String>) -> Result<HashMap<String, String>, PeachError> {
// use a file lock to avoid race conditions while saving config // use a file lock to avoid race conditions while saving config
let mut lock = LockFile::open(&*LOCK_FILE_PATH)?; let mut lock = LockFile::open(&*LOCK_FILE_PATH)?;
lock.lock()?; lock.lock()?;
// convert HashMap to yaml
let yaml_str = serde_yaml::to_string(&peach_config)?; let yaml_str = serde_yaml::to_string(&peach_config)?;
fs::write((*CONFIG_PATH).to_string(), yaml_str).map_err(|source| PeachError::Write { // write yaml to file
fs::write(CONFIG_PATH.as_str(), yaml_str).map_err(|source| PeachError::Write {
source, source,
path: (*CONFIG_PATH).to_string(), path: CONFIG_PATH.to_string(),
})?; })?;
// unlock file lock // unlock file lock
lock.unlock()?; lock.unlock()?;
// return peach_config // return modified HashMap
Ok(peach_config) Ok(peach_config)
} }
pub fn load_peach_config() -> Result<PeachConfig, PeachError> { // helper functions for serializing and deserializing PeachConfig from disc
let peach_config_exists = std::path::Path::new(&*CONFIG_PATH).exists(); pub fn save_peach_config_value(key: &str, value: String) -> Result<HashMap<String, String>, PeachError> {
let peach_config: PeachConfig = if !peach_config_exists { // get current config from disc
debug!("Loading peach config: {} does not exist", *CONFIG_PATH); let mut peach_config = load_peach_config_from_disc()?;
PeachConfig {
external_domain: "".to_string(),
dyn_domain: "".to_string(),
dyn_dns_server_address: DEFAULT_DYN_SERVER_ADDRESS.to_string(),
dyn_use_custom_server: false,
dyn_nameserver: DEFAULT_DYN_NAMESERVER.to_string(),
dyn_tsig_key_path: "".to_string(),
dyn_enabled: false,
ssb_admin_ids: Vec::new(),
// default password is `peach`
admin_password_hash: "146".to_string(),
temporary_password_hash: "".to_string(),
}
}
// otherwise we load peach config from disk
else {
debug!("Loading peach config: {} exists", *CONFIG_PATH);
let contents = fs::read_to_string((*CONFIG_PATH).to_string()).map_err(|source| PeachError::Read {
source,
path: (*CONFIG_PATH).to_string(),
})?;
serde_yaml::from_str(&contents)?
};
Ok(peach_config) // insert new key/value
peach_config.insert(key.to_string(), value);
// save hte modified hashmap to disc
save_peach_config_to_disc(peach_config)
} }
// interfaces for setting specific config values
// set all dyn configuration values at once
pub fn set_peach_dyndns_config( pub fn set_peach_dyndns_config(
dyn_domain: &str, dyn_domain: &str,
dyn_dns_server_address: &str, dyn_dns_server_address: &str,
dyn_tsig_key_path: &str, dyn_tsig_key_path: &str,
dyn_enabled: bool, dyn_enabled: bool,
) -> Result<PeachConfig, PeachError> { ) -> Result<HashMap<String, String>, PeachError> {
let mut peach_config = load_peach_config()?; let mut peach_config = load_peach_config_from_disc()?;
peach_config.dyn_domain = dyn_domain.to_string(); let dyn_enabled_str = match dyn_enabled {
peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string(); true => "true",
peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string(); false => "false"
peach_config.dyn_enabled = dyn_enabled; };
save_peach_config(peach_config) peach_config.insert("DYN_DOMAIN".to_string(), dyn_domain.to_string());
peach_config.insert("DYN_DNS_SERVER_ADDRESS".to_string(), dyn_dns_server_address.to_string());
peach_config.insert("DYN_TSIG_KEY_PATH".to_string(), dyn_tsig_key_path.to_string());
peach_config.insert("DYN_ENABLED".to_string(), dyn_enabled_str.to_string());
save_peach_config_to_disc(peach_config)
} }
pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> { pub fn set_external_domain(new_external_domain: &str) -> Result<HashMap<String, String>, PeachError> {
let mut peach_config = load_peach_config()?; save_peach_config_value("EXTERNAL_DOMAIN", new_external_domain.to_string())
peach_config.external_domain = new_external_domain.to_string();
save_peach_config(peach_config)
} }
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> { pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
let peach_config = load_peach_config()?; let external_domain = get_config_value("EXTERNAL_DOMAIN")?;
if !peach_config.external_domain.is_empty() { let dyn_domain = get_config_value("DYN_DOMAIN")?;
Ok(Some(peach_config.external_domain)) if !external_domain.is_empty() {
} else if !peach_config.dyn_domain.is_empty() { Ok(Some(external_domain.to_string()))
Ok(Some(peach_config.dyn_domain)) } else if !dyn_domain.is_empty() {
Ok(Some(dyn_domain.to_string()))
} else { } else {
Ok(None) Ok(None)
} }
} }
pub fn get_dyndns_server_address() -> Result<String, PeachError> { pub fn get_dyndns_server_address() -> Result<String, PeachError> {
let peach_config = load_peach_config()?; get_config_value("DYN_DNS_SERVER_ADDRESS")
// if the user is using a custom dyn server then load the address from the config }
if peach_config.dyn_use_custom_server {
Ok(peach_config.dyn_dns_server_address) pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<HashMap<String, String>, PeachError> {
} match enabled_value {
// otherwise hardcode the address true => save_peach_config_value("DYN_ENABLED", "true".to_string()),
else { false => save_peach_config_value("DYN_ENABLED", "false".to_string())
Ok(DEFAULT_DYN_SERVER_ADDRESS.to_string())
} }
} }
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> { pub fn get_dyndns_enabled_value() -> Result<bool, PeachError> {
let mut peach_config = load_peach_config()?; let val = get_config_value("DYN_ENABLED")?;
peach_config.dyn_enabled = enabled_value; return Ok(val == "true")
save_peach_config(peach_config)
} }
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> { pub fn add_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut peach_config = load_peach_config()?; let mut ssb_admin_ids = get_ssb_admin_ids()?;
peach_config.ssb_admin_ids.push(ssb_id.to_string()); ssb_admin_ids.push(ssb_id.to_string());
save_peach_config(peach_config) save_ssb_admin_ids(ssb_admin_ids)
} }
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> { pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut peach_config = load_peach_config()?; let mut ssb_admin_ids = get_ssb_admin_ids()?;
let mut ssb_admin_ids = peach_config.ssb_admin_ids;
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id); let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
match index_result { match index_result {
Some(index) => { Some(index) => {
ssb_admin_ids.remove(index); ssb_admin_ids.remove(index);
peach_config.ssb_admin_ids = ssb_admin_ids; save_ssb_admin_ids(ssb_admin_ids)
save_peach_config(peach_config)
} }
None => Err(PeachError::SsbAdminIdNotFound { None => Err(PeachError::SsbAdminIdNotFound {
id: ssb_id.to_string(), id: ssb_id.to_string(),
@ -189,32 +201,39 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
} }
} }
pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> { pub fn save_ssb_admin_ids(ssb_admin_ids: Vec<String>) -> Result<Vec<String>, PeachError> {
let mut peach_config = load_peach_config()?; // save_peach_config_value("SSB_ADMIN_IDS", ssb_admin_ids.to_string())
peach_config.admin_password_hash = password_hash.to_string(); // TODO: implement
save_peach_config(peach_config) Ok(ssb_admin_ids)
}
pub fn set_admin_password_hash(password_hash: String) -> Result<HashMap<String, String>, PeachError> {
save_peach_config_value("ADMIN_PASSWORD_HASH", password_hash)
} }
pub fn get_admin_password_hash() -> Result<String, PeachError> { pub fn get_admin_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?; let admin_password_hash = get_config_value("ADMIN_PASSWORD_HASH")?;
if !peach_config.admin_password_hash.is_empty() { if !admin_password_hash.is_empty() {
Ok(peach_config.admin_password_hash) Ok(admin_password_hash.to_string())
} else { } else {
Err(PeachError::PasswordNotSet) Err(PeachError::PasswordNotSet)
} }
} }
pub fn set_temporary_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> { pub fn set_temporary_password_hash(password_hash: String) -> Result<HashMap<String, String>, PeachError> {
let mut peach_config = load_peach_config()?; save_peach_config_value("TEMPORARY_PASSWORD_HASH", password_hash)
peach_config.temporary_password_hash = password_hash.to_string();
save_peach_config(peach_config)
} }
pub fn get_temporary_password_hash() -> Result<String, PeachError> { pub fn get_temporary_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?; let admin_password_hash = get_config_value("TEMPORARY_PASSWORD_HASH")?;
if !peach_config.temporary_password_hash.is_empty() { if !admin_password_hash.is_empty() {
Ok(peach_config.temporary_password_hash) Ok(admin_password_hash.to_string())
} else { } else {
Err(PeachError::PasswordNotSet) Err(PeachError::PasswordNotSet)
} }
} }
pub fn get_ssb_admin_ids() -> Result<Vec<String>, PeachError> {
let mut ssb_admin_ids = vec!["x".to_string(), "y".to_string(), "z".to_string()];
Ok(ssb_admin_ids)
}

View File

@ -18,7 +18,7 @@ use jsonrpc_client_http::HttpTransport;
use log::{debug, info}; use log::{debug, info};
use regex::Regex; use regex::Regex;
use crate::config_manager::get_dyndns_server_address; use crate::config_manager::{get_dyndns_server_address, get_config_value, get_dyndns_enabled_value};
use crate::{config_manager, error::PeachError}; use crate::{config_manager, error::PeachError};
/// constants for dyndns configuration /// constants for dyndns configuration
@ -107,7 +107,11 @@ fn get_public_ip_address() -> Result<String, PeachError> {
/// Reads dyndns configurations from config.yml /// Reads dyndns configurations from config.yml
/// and then uses nsupdate to update the IP address for the configured domain /// and then uses nsupdate to update the IP address for the configured domain
pub fn dyndns_update_ip() -> Result<bool, PeachError> { pub fn dyndns_update_ip() -> Result<bool, PeachError> {
let peach_config = config_manager::load_peach_config()?; let dyn_tsig_key_path = get_config_value("DYN_TSIG_KEY_PATH")?;
let dyn_enabled = get_dyndns_enabled_value()?;
let dyn_domain = get_config_value("DYN_DOMAIN")?;
let dyn_dns_server_address = get_config_value("DYN_DNS_SERVER_ADDRESS")?;
let dyn_nameserver = get_config_value("DYN_NAMESERVER")?;
info!( info!(
"Using config: "Using config:
dyn_tsig_key_path: {:?} dyn_tsig_key_path: {:?}
@ -116,13 +120,13 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
dyn_enabled: {:?} dyn_enabled: {:?}
dyn_nameserver: {:?} dyn_nameserver: {:?}
", ",
peach_config.dyn_tsig_key_path, dyn_tsig_key_path,
peach_config.dyn_domain, dyn_domain,
peach_config.dyn_dns_server_address, dyn_dns_server_address,
peach_config.dyn_enabled, dyn_enabled,
peach_config.dyn_nameserver, dyn_nameserver,
); );
if !peach_config.dyn_enabled { if !dyn_enabled {
info!("dyndns is not enabled, not updating"); info!("dyndns is not enabled, not updating");
Ok(false) Ok(false)
} else { } else {
@ -130,7 +134,7 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
let mut nsupdate_command = Command::new("nsupdate"); let mut nsupdate_command = Command::new("nsupdate");
nsupdate_command nsupdate_command
.arg("-k") .arg("-k")
.arg(&peach_config.dyn_tsig_key_path) .arg(&dyn_tsig_key_path)
.arg("-v"); .arg("-v");
// pass nsupdate commands via stdin // pass nsupdate commands via stdin
let public_ip_address = get_public_ip_address()?; let public_ip_address = get_public_ip_address()?;
@ -142,9 +146,9 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
update delete {DOMAIN} A update delete {DOMAIN} A
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS} update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
send", send",
NAMESERVER = peach_config.dyn_nameserver, NAMESERVER = dyn_nameserver,
ZONE = peach_config.dyn_domain, ZONE = dyn_domain,
DOMAIN = peach_config.dyn_domain, DOMAIN = dyn_domain,
PUBLIC_IP_ADDRESS = public_ip_address, PUBLIC_IP_ADDRESS = public_ip_address,
); );
info!("ns_commands: {:?}", ns_commands); info!("ns_commands: {:?}", ns_commands);
@ -217,8 +221,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
/// and has successfully run recently (in the last six minutes) /// and has successfully run recently (in the last six minutes)
pub fn is_dns_updater_online() -> Result<bool, PeachError> { pub fn is_dns_updater_online() -> Result<bool, PeachError> {
// first check if it is enabled in peach-config // first check if it is enabled in peach-config
let peach_config = config_manager::load_peach_config()?; let is_enabled = get_dyndns_enabled_value()?;
let is_enabled = peach_config.dyn_enabled;
// then check if it has successfully run within the last 6 minutes (60*6 seconds) // then check if it has successfully run within the last 6 minutes (60*6 seconds)
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?; let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
let ran_recently: bool = match num_seconds_since_successful_update { let ran_recently: bool = match num_seconds_since_successful_update {
@ -248,8 +251,7 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
// helper function which checks if a dyndns domain is new // helper function which checks if a dyndns domain is new
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> { pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
let peach_config = config_manager::load_peach_config()?; let previous_dyndns_domain = get_config_value("DYN_DOMAIN")?;
let previous_dyndns_domain = peach_config.dyn_domain;
Ok(dyndns_full_domain != previous_dyndns_domain) Ok(dyndns_full_domain != previous_dyndns_domain)
} }

View File

@ -7,6 +7,11 @@ use std::{io, str, string};
/// This type represents all possible errors that can occur when interacting with the PeachCloud library. /// This type represents all possible errors that can occur when interacting with the PeachCloud library.
#[derive(Debug)] #[derive(Debug)]
pub enum PeachError { pub enum PeachError {
/// Represents looking up a Config value with a non-existent key
InvalidKey {
msg: String,
},
/// Represents a failure to determine the path of the user's home directory. /// Represents a failure to determine the path of the user's home directory.
HomeDir, HomeDir,
@ -102,6 +107,7 @@ impl std::error::Error for PeachError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self { match *self {
PeachError::HomeDir => None, PeachError::HomeDir => None,
PeachError::InvalidKey{ .. } => None,
PeachError::Io(_) => None, PeachError::Io(_) => None,
PeachError::JsonRpcClientCore(_) => None, PeachError::JsonRpcClientCore(_) => None,
PeachError::JsonRpcCore(_) => None, PeachError::JsonRpcCore(_) => None,
@ -130,6 +136,9 @@ impl std::error::Error for PeachError {
impl std::fmt::Display for PeachError { impl std::fmt::Display for PeachError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self { match *self {
PeachError::InvalidKey { ref msg} => {
write!(f, "Invalid key in config lookup: {}", msg)
}
PeachError::HomeDir => { PeachError::HomeDir => {
write!( write!(
f, f,

View File

@ -33,7 +33,7 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
/// Sets a new password for the admin user /// Sets a new password for the admin user
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> { pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(new_password); let new_password_hash = hash_password(new_password);
config_manager::set_admin_password_hash(&new_password_hash)?; config_manager::set_admin_password_hash(new_password_hash)?;
Ok(()) Ok(())
} }
@ -53,7 +53,7 @@ pub fn hash_password(password: &str) -> String {
/// which can be used to reset the permanent password /// which can be used to reset the permanent password
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> { pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(new_password); let new_password_hash = hash_password(new_password);
config_manager::set_temporary_password_hash(&new_password_hash)?; config_manager::set_temporary_password_hash(new_password_hash)?;
Ok(()) Ok(())
} }
@ -103,8 +103,8 @@ using this link: http://peach.local/auth/reset",
}; };
msg += &remote_link; msg += &remote_link;
// finally send the message to the admins // finally send the message to the admins
let peach_config = config_manager::load_peach_config()?; let ssb_admin_ids = config_manager::get_ssb_admin_ids()?;
for ssb_admin_id in peach_config.ssb_admin_ids { for ssb_admin_id in ssb_admin_ids {
// use golgi to send a private message on scuttlebutt // use golgi to send a private message on scuttlebutt
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) { match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
Ok(_) => (), Ok(_) => (),

View File

@ -5,6 +5,7 @@ use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::error::PeachError; use crate::error::PeachError;
use crate::config_manager::get_config_value;
/* HELPER FUNCTIONS */ /* HELPER FUNCTIONS */