use crate::errors::prelude::{CliError, Result as CliResult};
use dashmap::DashMap;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::RwLock;
use tokio::time::Instant;
use toml;
use crate::constants::config::{CONFIG_FILE_NAME, DEFAULT_CONFIG_DIR, ENV_PREFIX};
pub static CONFIG: Lazy<Config> = Lazy::new(|| Config::load().expect("Failed to load config"));
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub api: ApiConfig,
#[serde(default)]
pub files: FileConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub ui: UiConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy: Option<ProxyConfig>,
#[serde(default)]
pub rate_limit: RateLimitConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default = "default_retries")]
pub max_retries: u32,
}
impl Default for ApiConfig {
fn default() -> Self {
Self {
token: None,
url: None,
timeout: default_timeout(),
max_retries: default_retries(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub download_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub upload_dir: Option<String>,
#[serde(default = "default_max_file_size")]
pub max_file_size: usize,
#[serde(default = "default_buffer_size")]
pub buffer_size: usize,
}
impl Default for FileConfig {
fn default() -> Self {
Self {
download_dir: None,
upload_dir: None,
max_file_size: default_max_file_size(),
buffer_size: default_buffer_size(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default = "default_log_format")]
pub format: String,
#[serde(default = "default_log_colors")]
pub colors: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: default_log_level(),
format: default_log_format(),
colors: default_log_colors(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiConfig {
#[serde(default = "default_show_progress")]
pub show_progress: bool,
#[serde(default = "default_progress_style")]
pub progress_style: String,
#[serde(default = "default_progress_refresh_rate")]
pub progress_refresh_rate: u64,
}
impl Default for UiConfig {
fn default() -> Self {
Self {
show_progress: default_show_progress(),
progress_style: default_progress_style(),
progress_refresh_rate: default_progress_refresh_rate(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
#[serde(default)]
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
url: "".to_string(),
user: None,
password: None,
}
}
}
pub fn default_timeout() -> u64 {
30
}
pub fn default_retries() -> u32 {
3
}
pub fn default_max_file_size() -> usize {
100 * 1024 * 1024 }
pub fn default_buffer_size() -> usize {
64 * 1024 }
pub fn default_log_level() -> String {
"info".to_string()
}
pub fn default_log_format() -> String {
"text".to_string()
}
pub fn default_log_colors() -> bool {
true
}
pub fn default_show_progress() -> bool {
true
}
pub fn default_progress_style() -> String {
"unicode".to_string()
}
pub fn default_progress_refresh_rate() -> u64 {
100
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
#[serde(default = "default_rate_limit_enabled")]
pub enabled: bool,
#[serde(default = "default_rate_limit_limit")]
pub limit: usize,
#[serde(default = "default_rate_limit_duration")]
pub duration: u64,
#[serde(default = "default_rate_limit_retry_delay")]
pub retry_delay: u64,
#[serde(default = "default_rate_limit_retry_attempts")]
pub retry_attempts: u16,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
enabled: default_rate_limit_enabled(),
limit: default_rate_limit_limit(),
duration: default_rate_limit_duration(),
retry_delay: default_rate_limit_retry_delay(),
retry_attempts: default_rate_limit_retry_attempts(),
}
}
}
pub fn default_rate_limit_enabled() -> bool {
false }
pub fn default_rate_limit_limit() -> usize {
1000 }
pub fn default_rate_limit_duration() -> u64 {
60
}
pub fn default_rate_limit_retry_delay() -> u64 {
500 }
pub fn default_rate_limit_retry_attempts() -> u16 {
3
}
impl Config {
pub fn load() -> CliResult<Self> {
let mut config = Config::default();
if let Ok(file_config) = Self::from_file() {
config = Self::merge_configs(config, file_config);
}
let env_config = Self::from_env()?;
config = Self::merge_configs(config, env_config);
Ok(config)
}
pub async fn load_async() -> CliResult<Self> {
let manager = AsyncConfigManager::default();
manager.load_config().await
}
pub fn save(&self, path: Option<&Path>) -> CliResult<()> {
let path = if let Some(p) = path {
p.to_owned()
} else {
let mut p = dirs::home_dir().ok_or_else(|| {
CliError::FileError("Could not determine home directory".to_string())
})?;
p.push(DEFAULT_CONFIG_DIR);
fs::create_dir_all(&p).map_err(|e| {
CliError::FileError(format!("Could not create config directory: {e}"))
})?;
p.push(CONFIG_FILE_NAME);
p
};
let content = toml::to_string_pretty(self)
.map_err(|e| CliError::UnexpectedError(format!("Could not serialize config: {e}")))?;
fs::write(&path, content)
.map_err(|e| CliError::FileError(format!("Could not write config file: {e}")))?;
Ok(())
}
pub async fn save_async(&self, path: Option<&Path>) -> CliResult<()> {
let path = if let Some(p) = path {
p.to_owned()
} else {
let mut p = dirs::home_dir().ok_or_else(|| {
CliError::FileError("Could not determine home directory".to_string())
})?;
p.push(DEFAULT_CONFIG_DIR);
tokio::fs::create_dir_all(&p).await.map_err(|e| {
CliError::FileError(format!("Could not create config directory: {e}"))
})?;
p.push(CONFIG_FILE_NAME);
p
};
let config_clone = self.clone();
let content = tokio::task::spawn_blocking(move || toml::to_string_pretty(&config_clone))
.await
.map_err(|e| CliError::UnexpectedError(format!("Task join error: {e}")))?
.map_err(|e| CliError::UnexpectedError(format!("Could not serialize config: {e}")))?;
tokio::fs::write(&path, content)
.await
.map_err(|e| CliError::FileError(format!("Could not write config file: {e}")))?;
Ok(())
}
pub fn from_file() -> CliResult<Self> {
let config_paths = crate::utils::config_helpers::get_config_paths();
for path in config_paths {
if path.exists() {
return Self::from_path(&path);
}
}
Ok(toml::from_str::<Config>("").unwrap())
}
pub fn from_path(path: &Path) -> CliResult<Self> {
let content = fs::read_to_string(path)
.map_err(|e| CliError::FileError(format!("Could not read config file: {e}")))?;
let config: Config = toml::from_str(&content)
.map_err(|e| CliError::UnexpectedError(format!("Could not parse config file: {e}")))?;
Ok(config)
}
pub async fn from_path_async(path: &Path) -> CliResult<Self> {
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| CliError::FileError(format!("Could not read config file: {e}")))?;
let config = tokio::task::spawn_blocking(move || toml::from_str::<Config>(&content))
.await
.map_err(|e| CliError::UnexpectedError(format!("Task join error: {e}")))?
.map_err(|e| CliError::UnexpectedError(format!("Could not parse config file: {e}")))?;
Ok(config)
}
pub fn from_env() -> CliResult<Self> {
let mut config = toml::from_str::<Config>("").unwrap();
if let Ok(token) = env::var(format!("{ENV_PREFIX}BOT_API_TOKEN")) {
config.api.token = Some(token);
}
if let Ok(url) = env::var(format!("{ENV_PREFIX}BOT_API_URL")) {
config.api.url = Some(url);
}
if let Ok(timeout_str) = env::var(format!("{ENV_PREFIX}TIMEOUT"))
&& let Ok(timeout_val) = timeout_str.parse::<u64>()
{
config.api.timeout = timeout_val;
}
if let Ok(download_dir) = env::var(format!("{ENV_PREFIX}DOWNLOAD_DIR")) {
config.files.download_dir = Some(download_dir);
}
if let Ok(upload_dir) = env::var(format!("{ENV_PREFIX}UPLOAD_DIR")) {
config.files.upload_dir = Some(upload_dir);
}
if let Ok(max_file_size_str) = env::var(format!("{ENV_PREFIX}MAX_FILE_SIZE"))
&& let Ok(max_file_size_val) = max_file_size_str.parse::<usize>()
{
config.files.max_file_size = max_file_size_val;
}
if let Ok(level) = env::var(format!("{ENV_PREFIX}LOG_LEVEL")) {
config.logging.level = level;
}
if let Ok(format) = env::var(format!("{ENV_PREFIX}LOG_FORMAT")) {
config.logging.format = format;
}
if let Ok(colors_str) = env::var(format!("{ENV_PREFIX}LOG_COLORS"))
&& let Ok(colors_val) = colors_str.parse::<bool>()
{
config.logging.colors = colors_val;
}
if let Ok(show_progress_str) = env::var(format!("{ENV_PREFIX}SHOW_PROGRESS"))
&& let Ok(show_progress_val) = show_progress_str.parse::<bool>()
{
config.ui.show_progress = show_progress_val;
}
if let Ok(progress_style) = env::var(format!("{ENV_PREFIX}PROGRESS_STYLE")) {
config.ui.progress_style = progress_style;
}
if let Ok(refresh_rate_str) = env::var(format!("{ENV_PREFIX}PROGRESS_REFRESH_RATE"))
&& let Ok(refresh_rate_val) = refresh_rate_str.parse::<u64>()
{
config.ui.progress_refresh_rate = refresh_rate_val;
}
if let Ok(proxy_url) = env::var(format!("{ENV_PREFIX}PROXY")) {
config.proxy = Some(ProxyConfig {
url: proxy_url,
user: env::var(format!("{ENV_PREFIX}PROXY_USER")).ok(),
password: env::var(format!("{ENV_PREFIX}PROXY_PASSWORD")).ok(),
});
}
if let Ok(enabled_str) = env::var(format!("{ENV_PREFIX}RATE_LIMIT_ENABLED"))
&& let Ok(enabled_val) = enabled_str.parse::<bool>()
{
config.rate_limit.enabled = enabled_val;
}
if let Ok(limit_str) = env::var(format!("{ENV_PREFIX}RATE_LIMIT_LIMIT"))
&& let Ok(limit_val) = limit_str.parse::<usize>()
{
config.rate_limit.limit = limit_val;
}
if let Ok(duration_str) = env::var(format!("{ENV_PREFIX}RATE_LIMIT_DURATION"))
&& let Ok(duration_val) = duration_str.parse::<u64>()
{
config.rate_limit.duration = duration_val;
}
Ok(config)
}
fn merge_configs(base: Self, overlay: Self) -> Self {
Self {
api: ApiConfig {
token: overlay.api.token.or(base.api.token),
url: overlay.api.url.or(base.api.url),
timeout: if overlay.api.timeout == default_timeout() {
base.api.timeout
} else {
overlay.api.timeout
},
max_retries: if overlay.api.max_retries == default_retries() {
base.api.max_retries
} else {
overlay.api.max_retries
},
},
files: FileConfig {
download_dir: overlay.files.download_dir.or(base.files.download_dir),
upload_dir: overlay.files.upload_dir.or(base.files.upload_dir),
max_file_size: if overlay.files.max_file_size == default_max_file_size() {
base.files.max_file_size
} else {
overlay.files.max_file_size
},
buffer_size: if overlay.files.buffer_size == default_buffer_size() {
base.files.buffer_size
} else {
overlay.files.buffer_size
},
},
logging: LoggingConfig {
level: if overlay.logging.level == default_log_level() {
base.logging.level
} else {
overlay.logging.level
},
format: if overlay.logging.format == default_log_format() {
base.logging.format
} else {
overlay.logging.format
},
colors: if overlay.logging.colors == default_log_colors() {
base.logging.colors
} else {
overlay.logging.colors
},
},
ui: UiConfig {
show_progress: if overlay.ui.show_progress == default_show_progress() {
base.ui.show_progress
} else {
overlay.ui.show_progress
},
progress_style: if overlay.ui.progress_style == default_progress_style() {
base.ui.progress_style
} else {
overlay.ui.progress_style
},
progress_refresh_rate: if overlay.ui.progress_refresh_rate
== default_progress_refresh_rate()
{
base.ui.progress_refresh_rate
} else {
overlay.ui.progress_refresh_rate
},
},
proxy: overlay.proxy.or(base.proxy),
rate_limit: crate::utils::config_helpers::merge_rate_limit_configs(
base.rate_limit,
overlay.rate_limit,
),
}
}
}
#[derive(Debug)]
pub struct AsyncConfigManager {
cache: Arc<RwLock<Option<(Config, SystemTime)>>>,
cache_ttl: Duration,
pub config_paths: Vec<PathBuf>,
}
#[derive(Clone, Debug)]
pub enum ConfigChange {
FileModified(PathBuf),
EnvironmentChanged(String),
ManualUpdate(Box<Config>),
}
#[derive(Debug, Clone)]
pub struct LockFreeConfigCache {
cache: Arc<DashMap<String, Arc<Config>>>,
timestamps: Arc<DashMap<String, Instant>>,
ttl: Duration,
}
impl Default for AsyncConfigManager {
fn default() -> Self {
Self::new(Duration::from_secs(300)) }
}
impl AsyncConfigManager {
pub fn new(cache_ttl: Duration) -> Self {
Self {
cache: Arc::new(RwLock::new(None)),
cache_ttl,
config_paths: crate::utils::config_helpers::get_config_paths(),
}
}
pub async fn load_config(&self) -> CliResult<Config> {
{
let cache = self.cache.read().await;
if let Some((config, timestamp)) = cache.as_ref()
&& timestamp.elapsed().unwrap_or(Duration::MAX) < self.cache_ttl
{
return Ok(config.clone());
}
}
let config = self.load_from_sources().await?;
{
let mut cache = self.cache.write().await;
*cache = Some((config.clone(), SystemTime::now()));
}
Ok(config)
}
pub async fn load_from_sources(&self) -> CliResult<Config> {
let file_futures: Vec<_> = self
.config_paths
.iter()
.filter(|path| path.exists())
.map(|path| self.load_config_file_async(path.clone()))
.collect();
let file_configs = futures::future::join_all(file_futures)
.await
.into_iter()
.filter_map(|result| result.ok())
.collect::<Vec<_>>();
let env_config = Self::from_env_async().await?;
let mut final_config = Config::default();
for config in file_configs {
final_config = Self::merge_configs_efficient(final_config, config);
}
final_config = Self::merge_configs_efficient(final_config, env_config);
Ok(final_config)
}
async fn load_config_file_async(&self, path: PathBuf) -> CliResult<Config> {
let content = tokio::fs::read_to_string(&path)
.await
.map_err(|e| CliError::FileError(format!("Could not read config file: {e}")))?;
let config = tokio::task::spawn_blocking(move || toml::from_str::<Config>(&content))
.await
.map_err(|e| CliError::UnexpectedError(format!("Task join error: {e}")))?
.map_err(|e| CliError::UnexpectedError(format!("Could not parse config file: {e}")))?;
Ok(config)
}
async fn from_env_async() -> CliResult<Config> {
tokio::task::spawn_blocking(Config::from_env)
.await
.map_err(|e| CliError::UnexpectedError(format!("Task join error: {e}")))?
}
pub fn merge_configs_efficient(base: Config, overlay: Config) -> Config {
Config {
api: ApiConfig {
token: overlay.api.token.or(base.api.token),
url: overlay.api.url.or(base.api.url),
timeout: if overlay.api.timeout != default_timeout() {
overlay.api.timeout
} else {
base.api.timeout
},
max_retries: if overlay.api.max_retries != default_retries() {
overlay.api.max_retries
} else {
base.api.max_retries
},
},
files: FileConfig {
download_dir: overlay.files.download_dir.or(base.files.download_dir),
upload_dir: overlay.files.upload_dir.or(base.files.upload_dir),
max_file_size: if overlay.files.max_file_size != default_max_file_size() {
overlay.files.max_file_size
} else {
base.files.max_file_size
},
buffer_size: if overlay.files.buffer_size != default_buffer_size() {
overlay.files.buffer_size
} else {
base.files.buffer_size
},
},
logging: LoggingConfig {
level: if overlay.logging.level != default_log_level() {
overlay.logging.level
} else {
base.logging.level
},
format: if overlay.logging.format != default_log_format() {
overlay.logging.format
} else {
base.logging.format
},
colors: if overlay.logging.colors != default_log_colors() {
overlay.logging.colors
} else {
base.logging.colors
},
},
ui: UiConfig {
show_progress: if overlay.ui.show_progress != default_show_progress() {
overlay.ui.show_progress
} else {
base.ui.show_progress
},
progress_style: if overlay.ui.progress_style != default_progress_style() {
overlay.ui.progress_style
} else {
base.ui.progress_style
},
progress_refresh_rate: if overlay.ui.progress_refresh_rate
!= default_progress_refresh_rate()
{
overlay.ui.progress_refresh_rate
} else {
base.ui.progress_refresh_rate
},
},
proxy: overlay.proxy.or(base.proxy),
rate_limit: overlay.rate_limit, }
}
}
impl LockFreeConfigCache {
pub fn new(ttl: Duration) -> Self {
Self {
cache: Arc::new(DashMap::new()),
timestamps: Arc::new(DashMap::new()),
ttl,
}
}
pub async fn get_or_load<F, Fut>(&self, key: &str, loader: F) -> CliResult<Arc<Config>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = CliResult<Config>>,
{
if let Some(config) = self.cache.get(key)
&& let Some(timestamp) = self.timestamps.get(key)
&& timestamp.elapsed() < self.ttl
{
return Ok(config.clone());
}
let new_config = Arc::new(loader().await?);
self.cache.insert(key.to_string(), new_config.clone());
self.timestamps.insert(key.to_string(), Instant::now());
Ok(new_config)
}
pub fn invalidate(&self, key: &str) {
self.cache.remove(key);
self.timestamps.remove(key);
}
pub fn clear(&self) {
self.cache.clear();
self.timestamps.clear();
}
pub fn stats(&self) -> (usize, usize) {
(self.cache.len(), self.timestamps.len())
}
}