use crate::env; use crate::tilde; use crate::Ini; use crate::Path; use crate::PathBuf; lazy_static::lazy_static! { pub static ref SETTINGS: Settings = Settings::load().expect("Failed to load settings"); } #[derive(Debug)] pub struct Settings { pub block_path: String, pub torrent_path: String, pub db_path: String, pub wallet_path: String, pub wallet_name: String, pub balance_sheet: String, pub log_path: String, pub log_level: String, pub public_ip: String, pub listen_ip: String, pub rpc_port: String, pub testnet_rpc_port: String, pub outgoing_connections: u8, pub incoming_connections: u8, pub threads: u16, pub piggybacks: Vec, pub pg_host: String, pub pg_port: u16, pub pg_user: String, pub pg_password: Option, pub pg_dbname: String, } impl Settings { fn expand(path: &str, base_dir: &Path) -> String { let expanded = PathBuf::from(tilde(path).as_ref()); if expanded.is_relative() { base_dir.join(expanded).to_string_lossy().into_owned() } else { expanded.to_string_lossy().into_owned() } } fn get_config_path() -> PathBuf { // Priority: CLI arg > ENV var > ./settings.ini > executable directory > platform fallback let mut args = env::args().skip(1); // Skip the executable name while let Some(arg) = args.next() { if arg == "--config" { if let Some(path) = args.next() { return PathBuf::from(path); } else { eprintln!("Error: --config flag requires a path"); std::process::exit(1); } } } if let Ok(path) = env::var("SETTINGS_PATH") { return PathBuf::from(path); } let local_settings = env::current_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join("settings.ini"); if local_settings.exists() { return local_settings; } if let Ok(exe_path) = env::current_exe() { if let Some(exe_dir) = exe_path.parent() { let exe_settings = exe_dir.join("settings.ini"); if exe_settings.exists() { return exe_settings; } } } #[cfg(unix)] { PathBuf::from("/etc/contractless/settings.ini") } #[cfg(not(unix))] { env::current_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join("settings.ini") } } pub fn load() -> Result> { let config_file_path = Self::get_config_path(); let config_dir = config_file_path.parent().unwrap_or_else(|| Path::new(".")); let conf = Ini::load_from_file(&config_file_path)?; let section = conf .section(Some("Piggyback")) .ok_or("Piggyback section not found")?; #[cfg(feature = "mainnet")] let pg_section = conf .section(Some("Postgres")) .ok_or("Postgres section not found")?; #[cfg(feature = "testnet")] let pg_section = conf .section(Some("Postgres-Testnet")) .ok_or("Postgres-Testnet section not found")?; let mut piggybacks = Vec::new(); for (key, value) in section.iter() { if key.to_uppercase().starts_with("PIGGYBACK_") { piggybacks.push(value.to_string()); } } let threads = conf .get_from(Some("Settings"), "THREADS") .unwrap_or("1") .parse::()?; if threads != 1 && threads != 2 && threads % 4 != 0 { return Err("THREADS must be 1, 2, or a multiple of 4".into()); } if threads == 0 || threads > 256 { return Err("THREADS must be between 1 and 256".into()); } Ok(Settings { block_path: Self::expand( conf.get_from(Some("Paths"), "BLOCK_PATH") .ok_or("BLOCK_PATH not found")?, config_dir, ), torrent_path: Self::expand( conf.get_from(Some("Paths"), "TORRENT_PATH") .ok_or("TORRENT_PATH not found")?, config_dir, ), db_path: Self::expand( conf.get_from(Some("Paths"), "DB_PATH") .ok_or("DB_PATH not found")?, config_dir, ), wallet_path: Self::expand( conf.get_from(Some("Paths"), "WALLET_PATH") .ok_or("WALLET_PATH not found")?, config_dir, ), wallet_name: conf .get_from(Some("Paths"), "WALLET_NAME") .ok_or("WALLET_NAME not found")? .to_string(), balance_sheet: Self::expand( conf.get_from(Some("Paths"), "BALANCE_SHEET") .ok_or("BALANCE_SHEET not found")?, config_dir, ), log_path: Self::expand( conf.get_from(Some("Paths"), "LOG_PATH") .unwrap_or("./logs"), config_dir, ), log_level: conf .get_from(Some("Settings"), "LOG_LEVEL") .unwrap_or("info") .to_string(), public_ip: conf .get_from(Some("Settings"), "PUBLIC_IP") .or_else(|| conf.get_from(Some("Settings"), "IP")) .ok_or("PUBLIC_IP not found")? .to_string(), listen_ip: conf .get_from(Some("Settings"), "LISTEN_IP") .unwrap_or("0.0.0.0") .to_string(), rpc_port: conf .get_from(Some("Settings"), "RPC_PORT") .ok_or("RPC_PORT not found")? .to_string(), testnet_rpc_port: conf .get_from(Some("Settings"), "TESTNET_RPC_PORT") .ok_or("TESTNET_RPC_PORT not found")? .to_string(), outgoing_connections: conf .get_from(Some("Settings"), "OUTGOING_CONNECTIONS") .ok_or("OUTGOING_CONNECTIONS not found")? .parse::()?, incoming_connections: conf .get_from(Some("Settings"), "INCOMING_CONNECTIONS") .ok_or("INCOMING_CONNECTIONS not found")? .parse::()?, threads, pg_host: pg_section.get("host").unwrap_or("127.0.0.1").to_string(), pg_port: pg_section.get("port").unwrap_or("5432").parse::()?, pg_user: pg_section .get("user") .ok_or("postgres user not set")? .to_string(), pg_password: pg_section.get("password").map(|s| s.to_string()), pg_dbname: pg_section .get("dbname") .ok_or("postgres dbname not set")? .to_string(), piggybacks, }) } }