gooty_proxy/io/filesystem.rs
1//! # Filesystem Module
2//!
3//! This module provides functionality for managing file-based storage of proxies, sources, and configuration.
4//! It includes methods for loading, saving, and managing data in TOML format.
5//!
6//! ## Components
7//!
8//! * **Filestore** - A struct for managing file-based storage
9//! * **`AppConfig`** - A struct for application-wide configuration settings
10//!
11//! ## Examples
12//!
13//! ```
14//! use gooty_proxy::io::filesystem::{Filestore, FilestoreConfig};
15//!
16//! let filestore = Filestore::new().unwrap();
17//! let proxies = filestore.load_proxies("my_proxies").unwrap();
18//! ```
19
20use crate::definitions::{
21 defaults,
22 errors::{FilestoreError, FilestoreResult},
23 proxy::Proxy,
24 source::Source,
25};
26use crate::utils::SerializableRegex;
27use chrono::Utc;
28use serde::{Deserialize, Serialize};
29use std::fs;
30use std::path::PathBuf;
31
32/// Configuration settings for the filestore
33///
34/// Controls how the filestore loads, saves, and manages data files.
35///
36/// # Examples
37///
38/// ```
39/// use gooty_proxy::io::filestore::FilestoreConfig;
40///
41/// // Create a custom configuration
42/// let config = FilestoreConfig {
43/// data_dir: "my_proxy_data".to_string(),
44/// create_defaults_if_missing: true,
45/// auto_save_interval_secs: 600, // 10 minutes
46/// pretty_print: true,
47/// };
48/// ```
49#[derive(Debug, Clone, Serialize, Deserialize, Default)]
50pub struct FilestoreConfig {
51 /// Directory where proxy data is stored
52 #[serde(default = "default_data_dir")]
53 pub data_dir: String,
54
55 /// Whether to create default files when they don't exist
56 #[serde(default = "default_true")]
57 pub create_defaults_if_missing: bool,
58
59 /// How often to auto-save data (in seconds)
60 #[serde(default = "default_auto_save_interval")]
61 pub auto_save_interval_secs: u64,
62
63 /// Whether to pretty-print TOML output
64 #[serde(default = "default_true")]
65 pub pretty_print: bool,
66}
67
68// Helper functions for default values
69fn default_data_dir() -> String {
70 "data".to_string()
71}
72
73fn default_true() -> bool {
74 true
75}
76
77fn default_auto_save_interval() -> u64 {
78 defaults::persistence::AUTO_SAVE_INTERVAL_SECS
79}
80
81/// Configuration for the entire application
82///
83/// Contains all configuration settings for the different components
84/// of the application, combining them into a single structure.
85///
86/// # Examples
87///
88/// ```
89/// use gooty_proxy::io::filestore::AppConfig;
90///
91/// // Create a default configuration
92/// let config = AppConfig::default();
93///
94/// // Access a configuration value
95/// assert!(config.request_timeout_secs > 0);
96/// ```
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct AppConfig {
99 /// Filestore configuration
100 pub filestore: FilestoreConfig,
101
102 /// Request timeout in seconds
103 pub request_timeout_secs: u64,
104
105 /// Number of retry attempts for failed requests
106 pub request_retries: u32,
107
108 /// Delay between sequential requests (ms)
109 pub request_delay_ms: u64,
110
111 /// Number of proxies to validate in parallel
112 pub parallel_validations: usize,
113
114 /// Maximum acceptable latency for proxies (ms)
115 pub max_acceptable_latency_ms: u32,
116
117 /// Minimum success rate for proxy rotation
118 pub min_success_rate: f64,
119
120 /// Logging level (error, warn, info, debug, trace)
121 pub log_level: String,
122}
123
124impl Default for AppConfig {
125 fn default() -> Self {
126 AppConfig {
127 filestore: FilestoreConfig::default(),
128 request_timeout_secs: defaults::DEFAULT_REQUEST_TIMEOUT_SECS,
129 request_retries: defaults::DEFAULT_REQUEST_RETRIES,
130 request_delay_ms: defaults::DEFAULT_REQUEST_DELAY_MS,
131 parallel_validations: defaults::DEFAULT_PARALLEL_VALIDATIONS,
132 max_acceptable_latency_ms: defaults::DEFAULT_MAX_ACCEPTABLE_LATENCY_MS,
133 min_success_rate: defaults::rotation::MIN_SUCCESS_RATE,
134 log_level: "info".to_string(),
135 }
136 }
137}
138
139/// Container for storing proxies in TOML format
140#[derive(Debug, Serialize, Deserialize)]
141struct ProxiesContainer {
142 last_updated: String,
143 proxies: Vec<Proxy>,
144}
145
146/// Container for storing sources in TOML format
147#[derive(Debug, Serialize, Deserialize)]
148struct SourcesContainer {
149 last_updated: String,
150 sources: Vec<Source>,
151}
152
153/// File-based storage manager for proxies, sources, and configuration
154///
155/// The Filestore provides methods for loading and saving data to the
156/// filesystem, managing proxy lists, source lists, and configuration files.
157///
158/// It handles file operations, serialization/deserialization, and ensures
159/// data consistency when reading and writing files.
160///
161/// # Examples
162///
163/// ```
164/// use gooty_proxy::io::filestore::{Filestore, FilestoreConfig};
165///
166/// // Create a filestore with default configuration
167/// let filestore = Filestore::new().unwrap();
168///
169/// // Create a filestore with custom configuration
170/// let config = FilestoreConfig {
171/// data_dir: "custom_data".to_string(),
172/// ..Default::default()
173/// };
174/// let filestore = Filestore::with_config(config).unwrap();
175///
176/// // Load proxies from a file
177/// let proxies = filestore.load_proxies("my_proxies").unwrap();
178/// ```
179pub struct Filestore {
180 /// Configuration for this filestore instance
181 config: FilestoreConfig,
182
183 /// Base directory for all data files
184 base_dir: PathBuf,
185}
186
187impl Filestore {
188 /// Create a new filestore with default configuration
189 ///
190 /// Creates a new filestore that stores data in the "data" directory.
191 ///
192 /// # Returns
193 ///
194 /// A new Filestore instance
195 ///
196 /// # Errors
197 ///
198 /// Returns an error if the data directory cannot be created or accessed
199 pub fn new() -> FilestoreResult<Self> {
200 Self::with_config(FilestoreConfig::default())
201 }
202
203 /// Create a new filestore with custom configuration
204 ///
205 /// # Arguments
206 ///
207 /// * `config` - Configuration settings for the filestore
208 ///
209 /// # Returns
210 ///
211 /// A new Filestore instance with the specified configuration
212 ///
213 /// # Errors
214 ///
215 /// Returns an error if the data directory cannot be created or accessed
216 pub fn with_config(config: FilestoreConfig) -> FilestoreResult<Self> {
217 let base_dir = PathBuf::from(&config.data_dir);
218
219 // Create the directory if it doesn't exist
220 if !base_dir.exists() {
221 fs::create_dir_all(&base_dir).map_err(|e| {
222 FilestoreError::IoError(format!("Failed to create directory: {e:?}"))
223 })?;
224 }
225
226 Ok(Filestore { config, base_dir })
227 }
228
229 /// Load proxies from a file
230 ///
231 /// # Arguments
232 ///
233 /// * `name` - Base name of the file (without extension)
234 ///
235 /// # Returns
236 ///
237 /// A vector of Proxy objects loaded from the file
238 ///
239 /// # Errors
240 ///
241 /// Returns an error if:
242 /// * The file doesn't exist and `create_defaults_if_missing` is false
243 /// * The file exists but cannot be read
244 /// * The file content is not valid TOML
245 /// * The TOML cannot be deserialized into proxies
246 pub fn load_proxies(&self, name: &str) -> FilestoreResult<Vec<Proxy>> {
247 let file_path = self.get_file_path(name, "toml");
248
249 if !file_path.exists() {
250 if self.config.create_defaults_if_missing {
251 // Create an empty proxies file
252 self.save_proxies(&Vec::new(), name)?;
253 return Ok(Vec::new());
254 }
255 return Err(FilestoreError::FileNotFound(
256 file_path.to_string_lossy().to_string(),
257 ));
258 }
259
260 // Read the file content
261 let content = fs::read_to_string(&file_path)
262 .map_err(|e| FilestoreError::IoError(format!("Failed to read file: {e:?}")))?;
263
264 // Parse TOML
265 let container: ProxiesContainer = toml::from_str(&content)
266 .map_err(|e| FilestoreError::ParseError(format!("Failed to parse TOML: {e:?}")))?;
267
268 Ok(container.proxies)
269 }
270
271 /// Save proxies to a file
272 ///
273 /// # Arguments
274 ///
275 /// * `proxies` - Vector of proxies to save
276 /// * `name` - Base name of the file (without extension)
277 ///
278 /// # Returns
279 ///
280 /// Ok(()) if the proxies were successfully saved
281 ///
282 /// # Errors
283 ///
284 /// Returns an error if:
285 /// * The file cannot be created or written to
286 /// * The proxies cannot be serialized to TOML
287 pub fn save_proxies(&self, proxies: &[Proxy], name: &str) -> FilestoreResult<()> {
288 let file_path = self.get_file_path(name, "toml");
289
290 // Ensure the directory exists
291 if let Some(parent) = file_path.parent() {
292 if !parent.exists() {
293 fs::create_dir_all(parent).map_err(|e| {
294 FilestoreError::IoError(format!("Failed to create directory: {e:?}"))
295 })?;
296 }
297 }
298
299 // Create a container with metadata
300 let container = ProxiesContainer {
301 last_updated: Utc::now().to_rfc3339(),
302 proxies: proxies.to_vec(),
303 };
304
305 // Serialize to TOML
306 let toml_content = if self.config.pretty_print {
307 toml::to_string_pretty(&container).map_err(|e| {
308 FilestoreError::SerializationError(format!("Failed to serialize to TOML: {e:?}"))
309 })?
310 } else {
311 toml::to_string(&container).map_err(|e| {
312 FilestoreError::SerializationError(format!("Failed to serialize to TOML: {e:?}"))
313 })?
314 };
315
316 // Write to file
317 fs::write(&file_path, toml_content)
318 .map_err(|e| FilestoreError::IoError(format!("Failed to write file: {e:?}")))?;
319
320 Ok(())
321 }
322
323 /// Load sources from a file
324 ///
325 /// # Arguments
326 ///
327 /// * `name` - Base name of the file (without extension)
328 ///
329 /// # Returns
330 ///
331 /// A vector of Source objects loaded from the file
332 ///
333 /// # Errors
334 ///
335 /// Returns an error if:
336 /// * The file doesn't exist and `create_defaults_if_missing` is false
337 /// * The file exists but cannot be read
338 /// * The file content is not valid TOML
339 /// * The TOML cannot be deserialized into sources
340 pub fn load_sources(&self, name: &str) -> FilestoreResult<Vec<Source>> {
341 let file_path = self.get_file_path(name, "toml");
342
343 if !file_path.exists() {
344 if self.config.create_defaults_if_missing {
345 // Create an empty sources file
346 self.save_sources(&Vec::new(), name)?;
347 return Ok(Vec::new());
348 }
349 return Err(FilestoreError::FileNotFound(
350 file_path.to_string_lossy().to_string(),
351 ));
352 }
353
354 // Read the file content
355 let content = fs::read_to_string(&file_path)
356 .map_err(|e| FilestoreError::IoError(format!("Failed to read file: {e:?}")))?;
357
358 // Parse TOML
359 let container: SourcesContainer = toml::from_str(&content)
360 .map_err(|e| FilestoreError::ParseError(format!("Failed to parse TOML: {e:?}")))?;
361
362 // Recompile regex patterns in sources
363 let mut sources = container.sources;
364 for source in &mut sources {
365 if let Ok(regex) = SerializableRegex::new(&source.regex_pattern) {
366 source.compiled_regex = Some(regex);
367 }
368 }
369
370 Ok(sources)
371 }
372
373 /// Save sources to a file
374 ///
375 /// # Arguments
376 ///
377 /// * `sources` - Vector of sources to save
378 /// * `name` - Base name of the file (without extension)
379 ///
380 /// # Returns
381 ///
382 /// Ok(()) if the sources were successfully saved
383 ///
384 /// # Errors
385 ///
386 /// Returns an error if:
387 /// * The file cannot be created or written to
388 /// * The sources cannot be serialized to TOML
389 pub fn save_sources(&self, sources: &[Source], name: &str) -> FilestoreResult<()> {
390 let file_path = self.get_file_path(name, "toml");
391
392 // Ensure the directory exists
393 if let Some(parent) = file_path.parent() {
394 if !parent.exists() {
395 fs::create_dir_all(parent).map_err(|e| {
396 FilestoreError::IoError(format!("Failed to create directory: {e:?}"))
397 })?;
398 }
399 }
400
401 // Create a container with metadata
402 let container = SourcesContainer {
403 last_updated: Utc::now().to_rfc3339(),
404 sources: sources.to_vec(),
405 };
406
407 // Serialize to TOML
408 let toml_content = if self.config.pretty_print {
409 toml::to_string_pretty(&container).map_err(|e| {
410 FilestoreError::SerializationError(format!("Failed to serialize to TOML: {e:?}"))
411 })?
412 } else {
413 toml::to_string(&container).map_err(|e| {
414 FilestoreError::SerializationError(format!("Failed to serialize to TOML: {e:?}"))
415 })?
416 };
417
418 // Write to file
419 fs::write(&file_path, toml_content)
420 .map_err(|e| FilestoreError::IoError(format!("Failed to write file: {e:?}")))?;
421
422 Ok(())
423 }
424
425 /// Load application configuration from a file
426 ///
427 /// # Arguments
428 ///
429 /// * `name` - Base name of the file (without extension)
430 ///
431 /// # Returns
432 ///
433 /// An `AppConfig` object loaded from the file
434 ///
435 /// # Errors
436 ///
437 /// Returns an error if:
438 /// * The file doesn't exist and `create_defaults_if_missing` is false
439 /// * The file exists but cannot be read
440 /// * The file content is not valid TOML
441 /// * The TOML cannot be deserialized into `AppConfig`
442 pub fn load_config(&self, name: &str) -> FilestoreResult<AppConfig> {
443 let file_path = self.get_file_path(name, "toml");
444
445 if !file_path.exists() {
446 if self.config.create_defaults_if_missing {
447 // Create a default config file
448 let default_config = AppConfig::default();
449 self.save_config(&default_config, name)?;
450 return Ok(default_config);
451 }
452 return Err(FilestoreError::FileNotFound(
453 file_path.to_string_lossy().to_string(),
454 ));
455 }
456
457 // Read the file content
458 let content = fs::read_to_string(&file_path)
459 .map_err(|e| FilestoreError::IoError(format!("Failed to read file: {e:?}")))?;
460
461 // Parse TOML
462 let config: AppConfig = toml::from_str(&content)
463 .map_err(|e| FilestoreError::ParseError(format!("Failed to parse TOML: {e:?}")))?;
464
465 Ok(config)
466 }
467
468 /// Save application configuration to a file
469 ///
470 /// # Arguments
471 ///
472 /// * `config` - `AppConfig` object to save
473 /// * `name` - Base name of the file (without extension)
474 ///
475 /// # Returns
476 ///
477 /// Ok(()) if the configuration was successfully saved
478 ///
479 /// # Errors
480 ///
481 /// Returns an error if:
482 /// * The file cannot be created or written to
483 /// * The configuration cannot be serialized to TOML
484 pub fn save_config(&self, config: &AppConfig, name: &str) -> FilestoreResult<()> {
485 let file_path = self.get_file_path(name, "toml");
486
487 // Ensure the directory exists
488 if let Some(parent) = file_path.parent() {
489 if !parent.exists() {
490 fs::create_dir_all(parent).map_err(|e| {
491 FilestoreError::IoError(format!("Failed to create directory: {e:?}"))
492 })?;
493 }
494 }
495
496 // Serialize to TOML
497 let toml_content = if self.config.pretty_print {
498 toml::to_string_pretty(config).map_err(|e| {
499 FilestoreError::SerializationError(format!("Failed to serialize to TOML: {e:?}"))
500 })?
501 } else {
502 toml::to_string(config).map_err(|e| {
503 FilestoreError::SerializationError(format!("Failed to serialize to TOML: {e:?}"))
504 })?
505 };
506
507 // Write to file
508 fs::write(&file_path, toml_content)
509 .map_err(|e| FilestoreError::IoError(format!("Failed to write file: {e:?}")))?;
510
511 Ok(())
512 }
513
514 /// Get the base directory where files are stored
515 ///
516 /// # Returns
517 ///
518 /// Reference to the base directory path
519 #[must_use] pub fn get_base_dir(&self) -> &PathBuf {
520 &self.base_dir
521 }
522
523 /// Get the current filestore configuration
524 ///
525 /// # Returns
526 ///
527 /// Reference to the current configuration
528 #[must_use] pub fn get_config(&self) -> &FilestoreConfig {
529 &self.config
530 }
531
532 /// Create a file path by joining the base directory with the name and extension
533 ///
534 /// # Arguments
535 ///
536 /// * `name` - Base name of the file
537 /// * `extension` - File extension (without dot)
538 ///
539 /// # Returns
540 ///
541 /// The complete file path
542 fn get_file_path(&self, name: &str, extension: &str) -> PathBuf {
543 self.base_dir.join(format!("{name}.{extension}"))
544 }
545}