use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::app::{
CacheConfig, ClientConfig, CoordinatorConfig, ManifestConfig, WorkQueueConfig, WorkerConfig,
};
use crate::constants::{limits, workers};
use crate::errors::{AppError, Result};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AppConfig {
pub cache: CacheConfigToml,
pub client: ClientConfigToml,
pub coordinator: CoordinatorConfigToml,
pub queue: WorkQueueConfigToml,
pub manifest: ManifestConfigToml,
pub logging: LoggingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfigToml {
pub cache_root: Option<PathBuf>,
pub max_cache_size: u64,
pub fast_verification: bool,
pub reservation_timeout_secs: u64,
pub auto_cleanup: bool,
pub min_free_space: u64,
}
impl Default for CacheConfigToml {
fn default() -> Self {
Self {
cache_root: None,
max_cache_size: 0,
fast_verification: true,
reservation_timeout_secs: 30,
auto_cleanup: false,
min_free_space: 1_073_741_824, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfigToml {
pub http2: bool,
pub tcp_keepalive_secs: Option<u64>,
pub tcp_nodelay: bool,
pub pool_idle_timeout_secs: Option<u64>,
pub pool_max_per_host: usize,
pub request_timeout_secs: u64,
pub connect_timeout_secs: u64,
pub rate_limit_rps: u32,
}
impl Default for ClientConfigToml {
fn default() -> Self {
Self {
http2: false, tcp_keepalive_secs: Some(30),
tcp_nodelay: true,
pool_idle_timeout_secs: Some(90),
pool_max_per_host: 8,
request_timeout_secs: 60,
connect_timeout_secs: 30,
rate_limit_rps: limits::DEFAULT_RATE_LIMIT_RPS,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinatorConfigToml {
pub worker_count: usize,
pub progress_update_interval_ms: u64,
pub shutdown_timeout_secs: u64,
pub enable_progress_bar: bool,
pub verbose_logging: bool,
pub progress_batch_size: usize,
pub worker: WorkerConfigToml,
}
impl Default for CoordinatorConfigToml {
fn default() -> Self {
Self {
worker_count: workers::DEFAULT_WORKER_COUNT,
progress_update_interval_ms: 500,
shutdown_timeout_secs: 30,
enable_progress_bar: true,
verbose_logging: false,
progress_batch_size: 10_000,
worker: WorkerConfigToml::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkQueueConfigToml {
pub max_retries: u32,
pub retry_delay_ms: u64,
pub max_workers: u32,
pub work_timeout_secs: u64,
}
impl Default for WorkQueueConfigToml {
fn default() -> Self {
Self {
max_retries: 3,
retry_delay_ms: 1000,
max_workers: workers::DEFAULT_WORKER_COUNT as u32,
work_timeout_secs: 300,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestConfigToml {
pub destination_root: PathBuf,
pub max_tracked_hashes: usize,
pub allow_duplicates: bool,
pub progress_batch_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerConfigToml {
pub worker_count: usize,
pub max_retries: u32,
pub retry_base_delay_ms: u64,
pub retry_max_delay_secs: u64,
pub idle_sleep_duration_ms: u64,
pub progress_buffer_size: usize,
pub download_timeout_secs: u64,
pub detailed_progress: bool,
}
impl Default for WorkerConfigToml {
fn default() -> Self {
Self {
worker_count: workers::DEFAULT_WORKER_COUNT,
max_retries: 3,
retry_base_delay_ms: 100,
retry_max_delay_secs: 30,
idle_sleep_duration_ms: 100,
progress_buffer_size: 100,
download_timeout_secs: 600,
detailed_progress: true,
}
}
}
impl Default for ManifestConfigToml {
fn default() -> Self {
Self {
destination_root: PathBuf::from(""), max_tracked_hashes: 1_000_000,
allow_duplicates: false,
progress_batch_size: 10_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub file_logging: bool,
pub log_file: Option<PathBuf>,
pub colored_output: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
file_logging: false,
log_file: None,
colored_output: true,
}
}
}
impl AppConfig {
pub fn to_runtime_config(
&self,
) -> (
CacheConfig,
ClientConfig,
CoordinatorConfig,
WorkQueueConfig,
ManifestConfig,
) {
(
self.cache.to_runtime_config(),
self.client.to_runtime_config(),
self.coordinator.to_runtime_config(),
self.queue.to_runtime_config(),
self.manifest.to_runtime_config(),
)
}
pub async fn load(config_file_override: Option<PathBuf>) -> Result<Self> {
let mut config = Self::default();
let config_path = if let Some(ref path) = config_file_override {
Some(path.clone())
} else {
Self::find_config_file().await?
};
if let Some(path) = config_path {
if path.exists() {
debug!("Loading config from: {}", path.display());
config = Self::load_from_file(&path).await?;
} else if config_file_override.is_some() {
return Err(AppError::generic(format!(
"Specified config file not found: {}",
path.display()
)));
}
}
Ok(config)
}
pub async fn initialize_first_run() -> Result<Option<PathBuf>> {
let config_path = Self::get_default_config_path()?;
if config_path.exists() {
return Ok(Some(config_path));
}
info!("Creating default configuration file...");
if let Some(parent) = config_path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
AppError::generic(format!(
"Failed to create config directory {}: {}",
parent.display(),
e
))
})?;
}
let config_content = Self::generate_default_config_content();
tokio::fs::write(&config_path, config_content)
.await
.map_err(|e| {
AppError::generic(format!(
"Failed to write config file {}: {}",
config_path.display(),
e
))
})?;
println!("📁 Created default configuration file:");
println!(" {}", config_path.display());
println!(" You can customize settings by editing this file.");
println!();
Ok(Some(config_path))
}
async fn find_config_file() -> Result<Option<PathBuf>> {
let search_paths = vec![
PathBuf::from("./midas-fetcher.toml"),
PathBuf::from("./config.toml"),
Self::get_default_config_path()?,
#[cfg(unix)]
PathBuf::from("/etc/midas-fetcher/config.toml"),
];
for path in search_paths {
if path.exists() {
debug!("Found config file: {}", path.display());
return Ok(Some(path));
}
}
debug!("No config file found in standard locations");
Ok(None)
}
fn get_default_config_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| AppError::generic("Could not determine user config directory"))?;
Ok(config_dir.join("midas-fetcher").join("config.toml"))
}
async fn load_from_file(path: &PathBuf) -> Result<Self> {
let content = tokio::fs::read_to_string(path).await.map_err(|e| {
AppError::generic(format!(
"Failed to read config file {}: {}",
path.display(),
e
))
})?;
let config: AppConfig = toml::from_str(&content).map_err(|e| {
AppError::generic(format!(
"Failed to parse config file {}: {}",
path.display(),
e
))
})?;
info!("Loaded configuration from: {}", path.display());
Ok(config)
}
fn generate_default_config_content() -> String {
let default_cache_path = dirs::config_dir()
.map(|dir| dir.join("midas-fetcher").join("cache"))
.unwrap_or_else(|| PathBuf::from("./cache"));
format!(
r#"# MIDAS Fetcher Configuration
# This file was automatically generated on first run.
# You can customize any of these settings to suit your needs.
[cache]
# Cache directory (leave empty to use system default)
# cache_root = "/path/to/custom/cache"
# Maximum cache size in bytes (0 = unlimited)
max_cache_size = 0
# Enable fast verification using manifest hashes
fast_verification = true
# Reservation timeout in seconds
reservation_timeout_secs = 30
# Enable automatic cleanup of old files
auto_cleanup = false
# Minimum free space to maintain (1GB)
min_free_space = 1073741824
[client]
# HTTP client settings
http2 = false # Disabled for CEDA compatibility
tcp_keepalive_secs = 30
tcp_nodelay = true
pool_idle_timeout_secs = 90
pool_max_per_host = 8
request_timeout_secs = 60
connect_timeout_secs = 30
rate_limit_rps = {}
[coordinator]
# Download coordination settings
worker_count = {}
progress_update_interval_ms = 500
shutdown_timeout_secs = 30
enable_progress_bar = true
verbose_logging = false
progress_batch_size = 10000
[coordinator.worker]
# Worker-specific settings
worker_count = {}
max_retries = 3
retry_base_delay_ms = 100
retry_max_delay_secs = 30
idle_sleep_duration_ms = 100
progress_buffer_size = 100
download_timeout_secs = 600
detailed_progress = true
[queue]
# Work queue settings
max_retries = 3
retry_delay_ms = 1000
max_workers = {}
work_timeout_secs = 300
[manifest]
# Manifest processing settings
# Default destination: {}
destination_root = "" # Empty = use default cache directory
max_tracked_hashes = 1000000
allow_duplicates = false
progress_batch_size = 10000
[logging]
# Logging configuration
level = "info" # error, warn, info, debug, trace
file_logging = false
colored_output = true
# log_file = "/path/to/log/file.log" # Uncomment to enable file logging
"#,
limits::DEFAULT_RATE_LIMIT_RPS,
workers::DEFAULT_WORKER_COUNT,
workers::DEFAULT_WORKER_COUNT,
workers::DEFAULT_WORKER_COUNT,
default_cache_path.display()
)
}
}
impl CacheConfigToml {
pub fn to_runtime_config(&self) -> CacheConfig {
CacheConfig {
cache_root: self.cache_root.clone(),
max_cache_size: self.max_cache_size,
fast_verification: self.fast_verification,
reservation_timeout: Duration::from_secs(self.reservation_timeout_secs),
auto_cleanup: self.auto_cleanup,
min_free_space: self.min_free_space,
}
}
}
impl ClientConfigToml {
pub fn to_runtime_config(&self) -> ClientConfig {
ClientConfig {
http2: self.http2,
tcp_keepalive: self.tcp_keepalive_secs.map(Duration::from_secs),
tcp_nodelay: self.tcp_nodelay,
pool_idle_timeout: self.pool_idle_timeout_secs.map(Duration::from_secs),
pool_max_per_host: self.pool_max_per_host,
request_timeout: Duration::from_secs(self.request_timeout_secs),
connect_timeout: Duration::from_secs(self.connect_timeout_secs),
rate_limit_rps: self.rate_limit_rps,
}
}
}
impl CoordinatorConfigToml {
pub fn to_runtime_config(&self) -> CoordinatorConfig {
CoordinatorConfig {
worker_count: self.worker_count,
progress_update_interval: Duration::from_millis(self.progress_update_interval_ms),
shutdown_timeout: Duration::from_secs(self.shutdown_timeout_secs),
enable_progress_bar: self.enable_progress_bar,
verbose_logging: self.verbose_logging,
progress_batch_size: self.progress_batch_size,
worker_config: self.worker.to_runtime_config(),
}
}
}
impl WorkQueueConfigToml {
pub fn to_runtime_config(&self) -> WorkQueueConfig {
WorkQueueConfig {
max_retries: self.max_retries,
retry_delay: Duration::from_millis(self.retry_delay_ms),
max_workers: self.max_workers,
work_timeout: Duration::from_secs(self.work_timeout_secs),
}
}
}
impl ManifestConfigToml {
pub fn to_runtime_config(&self) -> ManifestConfig {
let destination_root = if self.destination_root.as_os_str().is_empty() {
ManifestConfig::default().destination_root
} else {
self.destination_root.clone()
};
ManifestConfig {
destination_root,
max_tracked_hashes: self.max_tracked_hashes,
allow_duplicates: self.allow_duplicates,
progress_batch_size: self.progress_batch_size,
}
}
}
impl WorkerConfigToml {
pub fn to_runtime_config(&self) -> WorkerConfig {
WorkerConfig {
worker_count: self.worker_count,
max_retries: self.max_retries,
retry_base_delay: Duration::from_millis(self.retry_base_delay_ms),
retry_max_delay: Duration::from_secs(self.retry_max_delay_secs),
idle_sleep_duration: Duration::from_millis(self.idle_sleep_duration_ms),
progress_buffer_size: self.progress_buffer_size,
download_timeout: Duration::from_secs(self.download_timeout_secs),
detailed_progress: self.detailed_progress,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_default_config_creation() {
let config = AppConfig::default();
assert_eq!(
config.coordinator.worker_count,
workers::DEFAULT_WORKER_COUNT
);
assert_eq!(config.client.rate_limit_rps, limits::DEFAULT_RATE_LIMIT_RPS);
assert_eq!(config.logging.level, "info");
assert!(config.cache.fast_verification);
}
#[tokio::test]
async fn test_config_file_generation() {
let content = AppConfig::generate_default_config_content();
let parsed: AppConfig = toml::from_str(&content).unwrap();
assert_eq!(
parsed.coordinator.worker_count,
workers::DEFAULT_WORKER_COUNT
);
assert!(content.contains("# MIDAS Fetcher Configuration"));
assert!(content.contains("[cache]"));
assert!(content.contains("[client]"));
}
#[tokio::test]
async fn test_config_loading_nonexistent_file() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("nonexistent.toml");
let result = AppConfig::load(Some(config_path)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_config_loading_from_file() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("test_config.toml");
let test_config = r#"
[cache]
max_cache_size = 0
fast_verification = true
reservation_timeout_secs = 30
auto_cleanup = false
min_free_space = 1073741824
[client]
http2 = false
tcp_keepalive_secs = 30
tcp_nodelay = true
pool_idle_timeout_secs = 90
pool_max_per_host = 8
request_timeout_secs = 60
connect_timeout_secs = 30
rate_limit_rps = 15
[coordinator]
worker_count = 16
progress_update_interval_ms = 500
shutdown_timeout_secs = 30
enable_progress_bar = true
verbose_logging = false
progress_batch_size = 10000
[coordinator.worker]
worker_count = 8
max_retries = 3
retry_base_delay_ms = 100
retry_max_delay_secs = 30
idle_sleep_duration_ms = 100
progress_buffer_size = 100
download_timeout_secs = 600
detailed_progress = true
[queue]
max_retries = 3
retry_delay_ms = 1000
max_workers = 8
work_timeout_secs = 300
[manifest]
destination_root = "./cache"
max_tracked_hashes = 1000000
allow_duplicates = false
progress_batch_size = 10000
[logging]
level = "debug"
file_logging = false
colored_output = true
"#;
tokio::fs::write(&config_path, test_config).await.unwrap();
let config = AppConfig::load(Some(config_path)).await.unwrap();
assert_eq!(config.coordinator.worker_count, 16);
assert_eq!(config.logging.level, "debug");
assert_eq!(config.client.rate_limit_rps, limits::DEFAULT_RATE_LIMIT_RPS);
}
}