pub mod ini_parser;
use crate::cli::Cli;
use ini_parser::IniConfig;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Config {
pub listen: String,
pub port: u16,
pub threads: usize,
pub chunk_size: usize,
pub directory: PathBuf,
pub enable_upload: bool,
pub max_upload_size: u64,
pub username: Option<String>,
pub password: Option<String>,
pub allowed_extensions: Vec<String>,
pub verbose: bool,
pub detailed_logging: bool,
pub log_dir: Option<PathBuf>,
}
impl Config {
pub fn load(cli: &Cli) -> Result<Self, String> {
log::debug!("Starting configuration loading process");
log::trace!("CLI config_file parameter: {:?}", cli.config_file);
let config_file = Self::find_config_file(cli)?;
log::debug!("Config file discovery result: {:?}", config_file);
let ini = if let Some(path) = config_file {
log::info!("Loading configuration from: {}", path.display());
let ini_config = IniConfig::load_file(&path)?;
log::debug!(
"Successfully parsed INI configuration with {} sections",
ini_config.sections().len()
);
ini_config
} else {
log::info!("No configuration file found, using defaults and CLI overrides");
IniConfig::new()
};
log::debug!("Building final configuration with precedence rules");
let config = Self {
listen: Self::get_listen(&ini, cli),
port: Self::get_port(&ini, cli),
threads: Self::get_threads(&ini, cli),
chunk_size: Self::get_chunk_size(&ini, cli),
directory: Self::get_directory(&ini, cli)?,
enable_upload: Self::get_enable_upload(&ini, cli),
max_upload_size: Self::get_max_upload_size(&ini, cli),
username: Self::get_username(&ini, cli),
password: Self::get_password(&ini, cli),
allowed_extensions: Self::get_allowed_extensions(&ini, cli),
verbose: Self::get_verbose(&ini, cli),
detailed_logging: Self::get_detailed_logging(&ini, cli),
log_dir: Self::get_log_dir(&ini, cli),
};
log::debug!("Configuration loading completed successfully");
log::trace!(
"Final config - listen: {}, port: {}, threads: {}, upload_enabled: {}",
config.listen,
config.port,
config.threads,
config.enable_upload
);
Ok(config)
}
fn find_config_file(cli: &Cli) -> Result<Option<PathBuf>, String> {
if let Some(ref config_path) = cli.config_file {
let path = PathBuf::from(config_path);
if path.exists() {
return Ok(Some(path));
}
return Err(format!(
"Config file specified but not found: {config_path}"
));
}
let current_config = PathBuf::from("irondrop.ini");
if current_config.exists() {
return Ok(Some(current_config));
}
let current_config_alt = PathBuf::from("irondrop.conf");
if current_config_alt.exists() {
return Ok(Some(current_config_alt));
}
if let Some(home_dir) = std::env::var_os("HOME") {
let user_config = Path::new(&home_dir)
.join(".config")
.join("irondrop")
.join("config.ini");
if user_config.exists() {
return Ok(Some(user_config));
}
}
#[cfg(unix)]
{
let system_config = PathBuf::from("/etc/irondrop/config.ini");
if system_config.exists() {
return Ok(Some(system_config));
}
}
Ok(None)
}
fn get_listen(ini: &IniConfig, cli: &Cli) -> String {
if let Some(listen) = &cli.listen {
return listen.clone();
}
if let Some(listen) = ini.get_string("server", "listen") {
return listen;
}
"127.0.0.1".to_string()
}
fn get_port(ini: &IniConfig, cli: &Cli) -> u16 {
if let Some(port) = cli.port {
return port;
}
if let Some(port) = ini.get_u16("server", "port") {
return port;
}
8080
}
fn get_threads(ini: &IniConfig, cli: &Cli) -> usize {
if let Some(threads) = cli.threads {
return threads;
}
if let Some(threads) = ini.get_usize("server", "threads") {
return threads;
}
8
}
fn get_chunk_size(ini: &IniConfig, cli: &Cli) -> usize {
if let Some(chunk_size) = cli.chunk_size {
return chunk_size;
}
if let Some(chunk_size) = ini.get_usize("server", "chunk_size") {
return chunk_size;
}
1024
}
fn get_directory(_ini: &IniConfig, cli: &Cli) -> Result<PathBuf, String> {
Ok(cli.directory.clone())
}
fn get_enable_upload(ini: &IniConfig, cli: &Cli) -> bool {
if let Some(enable_upload) = cli.enable_upload {
return enable_upload;
}
if let Some(enabled) = ini.get_bool("upload", "enable_upload") {
return enabled;
}
false
}
fn get_max_upload_size(ini: &IniConfig, cli: &Cli) -> u64 {
if let Some(max_upload_size) = cli.max_upload_size {
return max_upload_size * 1024 * 1024; }
if let Some(size_bytes) = ini.get_file_size("upload", "max_upload_size") {
return size_bytes;
}
if let Some(size_bytes) = ini.get_file_size("upload", "max_size") {
return size_bytes;
}
u64::MAX
}
fn get_username(ini: &IniConfig, cli: &Cli) -> Option<String> {
if let Some(ref username) = cli.username {
return Some(username.clone());
}
ini.get_string("auth", "username")
}
fn get_password(ini: &IniConfig, cli: &Cli) -> Option<String> {
if let Some(ref password) = cli.password {
return Some(password.clone());
}
ini.get_string("auth", "password")
}
fn get_allowed_extensions(ini: &IniConfig, cli: &Cli) -> Vec<String> {
if let Some(allowed_extensions) = &cli.allowed_extensions {
return allowed_extensions
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
let ini_extensions = ini.get_list("security", "allowed_extensions");
if !ini_extensions.is_empty() {
return ini_extensions;
}
vec!["*.zip".to_string(), "*.txt".to_string()]
}
fn get_verbose(ini: &IniConfig, cli: &Cli) -> bool {
if let Some(verbose) = cli.verbose {
return verbose;
}
ini.get_bool_or("logging", "verbose", false)
}
fn get_detailed_logging(ini: &IniConfig, cli: &Cli) -> bool {
if let Some(detailed_logging) = cli.detailed_logging {
return detailed_logging;
}
ini.get_bool_or("logging", "detailed", false)
}
fn get_log_dir(ini: &IniConfig, cli: &Cli) -> Option<PathBuf> {
if let Some(ref log_dir) = cli.log_dir {
return Some(log_dir.clone());
}
ini.get_string("logging", "log_dir").map(PathBuf::from)
}
pub fn print_summary(&self) {
log::info!("Configuration Summary:");
log::info!(" Server: {}:{}", self.listen, self.port);
log::info!(" Directory: {}", self.directory.display());
log::info!(" Threads: {}", self.threads);
log::info!(" Chunk Size: {} bytes", self.chunk_size);
log::info!(" Upload Enabled: {}", self.enable_upload);
if self.enable_upload {
log::info!(
" Max Upload Size: {} MB",
self.max_upload_size / (1024 * 1024)
);
}
log::info!(
" Authentication: {}",
if self.username.is_some() {
"Enabled"
} else {
"Disabled"
}
);
log::info!(" Allowed Extensions: {:?}", self.allowed_extensions);
log::info!(" Verbose Logging: {}", self.verbose);
log::info!(" Detailed Logging: {}", self.detailed_logging);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_cli(directory: PathBuf) -> Cli {
Cli {
directory,
listen: None, port: None, allowed_extensions: None,
threads: None,
chunk_size: None,
verbose: None,
detailed_logging: None,
username: None,
password: None,
enable_upload: None,
max_upload_size: None,
config_file: None,
log_dir: None,
}
}
#[test]
fn test_config_load_no_config_file() {
let temp_dir = TempDir::new().unwrap();
let cli = create_test_cli(temp_dir.path().to_path_buf());
let config = Config::load(&cli).unwrap();
assert_eq!(config.listen, "127.0.0.1");
assert_eq!(config.port, 8080);
assert_eq!(config.threads, 8);
assert_eq!(config.chunk_size, 1024);
assert_eq!(config.directory, temp_dir.path());
assert!(!config.enable_upload);
assert_eq!(config.max_upload_size, u64::MAX); assert_eq!(config.username, None);
assert_eq!(config.password, None);
assert_eq!(config.allowed_extensions, vec!["*.zip", "*.txt"]);
assert!(!config.verbose);
assert!(!config.detailed_logging);
}
#[test]
fn test_config_load_with_ini_file() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("test.ini");
let ini_content = r"
[server]
listen = 0.0.0.0
port = 9000
threads = 16
chunk_size = 2048
[upload]
enable_upload = true
max_upload_size = 5GB
[auth]
username = testuser
password = testpass
[security]
allowed_extensions = *.pdf,*.doc
[logging]
verbose = true
detailed = false
";
fs::write(&config_file, ini_content).unwrap();
let mut cli = create_test_cli(temp_dir.path().to_path_buf());
cli.config_file = Some(config_file.to_string_lossy().to_string());
let config = Config::load(&cli).unwrap();
assert_eq!(config.listen, "0.0.0.0");
assert_eq!(config.port, 9000);
assert_eq!(config.threads, 16);
assert_eq!(config.chunk_size, 2048);
assert!(config.enable_upload);
assert_eq!(config.max_upload_size, 5 * 1024 * 1024 * 1024);
assert_eq!(config.username, Some("testuser".to_string()));
assert_eq!(config.password, Some("testpass".to_string()));
assert_eq!(config.allowed_extensions, vec!["*.pdf", "*.doc"]);
assert!(config.verbose);
assert!(!config.detailed_logging);
}
#[test]
fn test_config_load_cli_overrides_ini() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("test.ini");
let ini_content = r"
[server]
listen = 0.0.0.0
port = 9000
threads = 16
";
fs::write(&config_file, ini_content).unwrap();
let mut cli = create_test_cli(temp_dir.path().to_path_buf());
cli.config_file = Some(config_file.to_string_lossy().to_string());
cli.listen = Some("192.168.1.1".to_string());
cli.port = Some(7777);
cli.verbose = Some(true);
let config = Config::load(&cli).unwrap();
assert_eq!(config.listen, "192.168.1.1");
assert_eq!(config.port, 7777);
assert!(config.verbose);
assert_eq!(config.threads, 16);
}
#[test]
fn test_config_file_discovery_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let mut cli = create_test_cli(temp_dir.path().to_path_buf());
cli.config_file = Some("/nonexistent/path.ini".to_string());
let result = Config::load(&cli);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("Config file specified but not found")
);
}
#[test]
fn test_config_upload_settings() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("test.ini");
let ini_content = r"
[upload]
enable_upload = true
max_upload_size = 2GB
";
fs::write(&config_file, ini_content).unwrap();
let mut cli = create_test_cli(temp_dir.path().to_path_buf());
cli.config_file = Some(config_file.to_string_lossy().to_string());
let config = Config::load(&cli).unwrap();
assert!(config.enable_upload);
assert_eq!(config.max_upload_size, 2 * 1024 * 1024 * 1024);
}
#[test]
fn test_config_max_upload_size_formats() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("test.ini");
let current_config = PathBuf::from("irondrop.ini");
let backup_config = PathBuf::from("irondrop.ini.backup");
let config_existed = if current_config.exists() {
std::fs::rename(¤t_config, &backup_config).ok();
true
} else {
false
};
let ini_content = r"
[upload]
max_upload_size = 1.5GB
";
fs::write(&config_file, ini_content).unwrap();
let mut cli = create_test_cli(temp_dir.path().to_path_buf());
cli.config_file = Some(config_file.to_string_lossy().to_string());
cli.max_upload_size = None;
let config = Config::load(&cli).unwrap();
assert_eq!(
config.max_upload_size,
(1.5 * 1024.0 * 1024.0 * 1024.0) as u64
);
if config_existed {
std::fs::rename(&backup_config, ¤t_config).ok();
}
}
#[test]
fn test_config_print_summary() {
let temp_dir = TempDir::new().unwrap();
let cli = create_test_cli(temp_dir.path().to_path_buf());
let config = Config::load(&cli).unwrap();
config.print_summary();
}
#[test]
fn test_config_directory_always_from_cli() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("test.ini");
let ini_content = r"
[server]
directory = /some/other/path
";
fs::write(&config_file, ini_content).unwrap();
let mut cli = create_test_cli(temp_dir.path().to_path_buf());
cli.config_file = Some(config_file.to_string_lossy().to_string());
let config = Config::load(&cli).unwrap();
assert_eq!(config.directory, temp_dir.path());
}
}