gooty_proxy/config/
loader.rs1use std::fs;
26use std::path::{Path, PathBuf};
27
28use chrono::{DateTime, Utc};
29use log::{debug, info, warn};
30
31use crate::config::schema::AppConfig;
32use crate::definitions::errors::{ConfigError, ConfigResult};
33
34pub struct ConfigLoader {
36 config_dir: PathBuf,
38
39 config_filename: String,
41
42 config: AppConfig,
44}
45
46impl ConfigLoader {
47 pub fn new<P: AsRef<Path>>(config_dir: P) -> ConfigResult<Self> {
56 Self::with_filename(config_dir, "config.toml")
57 }
58
59 pub fn with_filename<P: AsRef<Path>>(config_dir: P, filename: &str) -> ConfigResult<Self> {
68 let config_dir = config_dir.as_ref().to_path_buf();
69
70 if !config_dir.exists() {
72 info!("Creating configuration directory: {}", config_dir.display());
73 fs::create_dir_all(&config_dir).map_err(ConfigError::IoError)?;
74 }
75
76 let config_path = config_dir.join(filename);
78
79 let config = if config_path.exists() {
81 Self::load_from_file(&config_path)?
82 } else {
83 info!("Configuration file not found, creating default");
84 let default_config = AppConfig::default();
85 Self::save_to_file(&default_config, &config_path)?;
86 default_config
87 };
88
89 Ok(ConfigLoader {
90 config_dir,
91 config_filename: filename.to_string(),
92 config,
93 })
94 }
95
96 #[must_use]
98 pub fn get_config(&self) -> &AppConfig {
99 &self.config
100 }
101
102 pub fn get_config_mut(&mut self) -> &mut AppConfig {
104 &mut self.config
105 }
106
107 pub fn update_config(&mut self, config: AppConfig) -> ConfigResult<()> {
115 let config_path = self.config_dir.join(&self.config_filename);
116 Self::save_to_file(&config, &config_path)?;
117 self.config = config;
118 debug!(
119 "Configuration updated and saved to {}",
120 config_path.display()
121 );
122 Ok(())
123 }
124
125 pub fn reload(&mut self) -> ConfigResult<()> {
134 let config_path = self.config_dir.join(&self.config_filename);
135 if config_path.exists() {
136 self.config = Self::load_from_file(&config_path)?;
137 debug!("Configuration reloaded from {}", config_path.display());
138 Ok(())
139 } else {
140 warn!("Configuration file not found at {}", config_path.display());
141 Err(ConfigError::MissingConfig(config_path))
142 }
143 }
144
145 pub fn save(&self) -> ConfigResult<()> {
153 let config_path = self.config_dir.join(&self.config_filename);
154 Self::save_to_file(&self.config, &config_path)?;
155 debug!("Configuration saved to {}", config_path.display());
156 Ok(())
157 }
158
159 pub fn reset_to_defaults(&mut self) -> ConfigResult<()> {
167 self.config = AppConfig::default();
168 self.save()?;
169 info!("Configuration reset to defaults");
170 Ok(())
171 }
172
173 #[must_use]
175 pub fn get_config_path(&self) -> PathBuf {
176 self.config_dir.join(&self.config_filename)
177 }
178
179 #[must_use]
181 pub fn config_exists(&self) -> bool {
182 self.get_config_path().exists()
183 }
184
185 fn load_from_file(path: &Path) -> ConfigResult<AppConfig> {
193 debug!("Loading configuration from {}", path.display());
194 let content = fs::read_to_string(path).map_err(ConfigError::IoError)?;
195
196 let config: AppConfig = toml::from_str(&content).map_err(ConfigError::TomlDeError)?;
197
198 Ok(config)
199 }
200
201 fn save_to_file(config: &AppConfig, path: &Path) -> ConfigResult<()> {
210 debug!("Saving configuration to {}", path.display());
211 if let Some(parent) = path.parent() {
213 if !parent.exists() {
214 fs::create_dir_all(parent).map_err(ConfigError::IoError)?;
215 }
216 }
217
218 let toml_string = if config.storage.pretty_print {
220 toml::to_string_pretty(config).map_err(ConfigError::TomlSerError)?
221 } else {
222 toml::to_string(config).map_err(ConfigError::TomlSerError)?
223 };
224
225 fs::write(path, toml_string).map_err(ConfigError::IoError)?;
226
227 Ok(())
228 }
229
230 pub fn validate(&self) -> ConfigResult<()> {
241 let valid_log_levels = ["error", "warn", "info", "debug", "trace"];
243 let log_level = self.config.application.log_level.to_lowercase();
244
245 if !valid_log_levels.contains(&log_level.as_str()) {
246 return Err(ConfigError::InvalidValue(format!(
247 "Invalid log_level: {log_level}. Must be one of: error, warn, info, debug, trace"
248 )));
249 }
250
251 if self.config.http.request_timeout_secs == 0 {
253 return Err(ConfigError::InvalidValue(
254 "request_timeout_secs must be greater than 0".to_string(),
255 ));
256 }
257
258 if self.config.judge.parallel_validations == 0 {
260 return Err(ConfigError::InvalidValue(
261 "parallel_validations must be greater than 0".to_string(),
262 ));
263 }
264
265 if self.config.proxies.min_success_rate < 0.0 || self.config.proxies.min_success_rate > 1.0
267 {
268 return Err(ConfigError::InvalidValue(
269 "min_success_rate must be between 0.0 and 1.0".to_string(),
270 ));
271 }
272
273 if self.config.storage.auto_save_interval_secs == 0 {
275 return Err(ConfigError::InvalidValue(
276 "auto_save_interval_secs must be greater than 0".to_string(),
277 ));
278 }
279
280 Ok(())
281 }
282
283 pub fn create_snapshot(&self) -> ConfigResult<PathBuf> {
292 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
293 let snapshot_filename = format!("config_backup_{timestamp}.toml");
294 let snapshot_path = self.config_dir.join("backups").join(&snapshot_filename);
295
296 if let Some(parent) = snapshot_path.parent() {
298 if !parent.exists() {
299 fs::create_dir_all(parent).map_err(ConfigError::IoError)?;
300 }
301 }
302
303 Self::save_to_file(&self.config, &snapshot_path)?;
304 info!(
305 "Configuration snapshot created at {}",
306 snapshot_path.display()
307 );
308
309 Ok(snapshot_path)
310 }
311
312 pub fn list_snapshots(&self) -> ConfigResult<Vec<PathBuf>> {
321 let backups_dir = self.config_dir.join("backups");
322
323 if !backups_dir.exists() {
324 return Ok(Vec::new());
325 }
326
327 let mut snapshots = Vec::new();
328 for entry in fs::read_dir(backups_dir).map_err(ConfigError::IoError)? {
329 let entry = entry.map_err(ConfigError::IoError)?;
330 let path = entry.path();
331
332 if path.is_file() && path.extension().is_some_and(|ext| ext == "toml") {
333 snapshots.push(path);
334 }
335 }
336
337 snapshots.sort_by(|a, b| {
339 let a_meta = fs::metadata(a).ok();
340 let b_meta = fs::metadata(b).ok();
341
342 match (a_meta, b_meta) {
343 (Some(a_meta), Some(b_meta)) => {
344 let a_modified = a_meta.modified().ok().map(DateTime::<Utc>::from);
345 let b_modified = b_meta.modified().ok().map(DateTime::<Utc>::from);
346
347 match (b_modified, a_modified) {
348 (Some(b_time), Some(a_time)) => b_time.cmp(&a_time),
349 _ => std::cmp::Ordering::Equal,
350 }
351 }
352 _ => std::cmp::Ordering::Equal,
353 }
354 });
355
356 Ok(snapshots)
357 }
358
359 pub fn restore_from_snapshot(&mut self, snapshot_path: &Path) -> ConfigResult<()> {
369 if !snapshot_path.exists() {
370 return Err(ConfigError::MissingConfig(snapshot_path.to_path_buf()));
371 }
372
373 let snapshot_config = Self::load_from_file(snapshot_path)?;
374 self.update_config(snapshot_config)?;
375
376 info!(
377 "Configuration restored from snapshot {}",
378 snapshot_path.display()
379 );
380 Ok(())
381 }
382}