Skip to main content

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}