1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::str::FromStr;
4use thiserror::Error;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RetryConfig {
9 pub max_retries: u32,
10 pub initial_delay: u64,
11 pub max_delay: u64,
12 pub backoff_factor: f64,
13}
14
15impl Default for RetryConfig {
16 fn default() -> Self {
17 Self {
18 max_retries: 3,
19 initial_delay: 1,
20 max_delay: 30,
21 backoff_factor: 2.0,
22 }
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Config {
28 pub download_dir: PathBuf,
29 pub workers: usize,
30 pub random_order: bool,
31 pub urls: Vec<String>,
32 pub rate_limit_kb: Option<u64>,
33 pub retry: RetryConfig,
34 pub concurrent_downloads: usize,
35 pub connection_timeout: u64,
36}
37
38impl Default for Config {
39 fn default() -> Self {
40 Self {
41 download_dir: PathBuf::from("downloads"),
42 workers: 4,
43 random_order: false,
44 urls: Vec::new(),
45 rate_limit_kb: None,
46 retry: RetryConfig::default(),
47 concurrent_downloads: 4,
48 connection_timeout: 30,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct IntegrityCheck {
55 pub enabled: bool,
56 pub algorithm: ChecksumAlgorithm,
57 pub checksums: HashMap<String, String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub enum ChecksumAlgorithm {
62 MD5,
63 SHA256,
64 SHA512,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct DownloadFilter {
69 pub include_patterns: Vec<String>,
70 pub exclude_patterns: Vec<String>,
71 pub min_size: Option<u64>,
72 pub max_size: Option<u64>,
73}
74
75#[derive(Debug, Error)]
76pub enum ConfigError {
77 #[error("Invalid download directory: {0}")]
78 InvalidDownloadDir(String),
79 #[error("Invalid number of workers: {0}")]
80 InvalidWorkers(usize),
81 #[error("No download URLs provided")]
82 NoUrls,
83 #[error("Invalid URL format: {0}")]
84 InvalidUrl(String),
85}
86
87impl Config {
88 pub fn validate(&self) -> Result<(), ConfigError> {
89 if !self.download_dir.to_str().map_or(false, |s| !s.is_empty()) {
91 return Err(ConfigError::InvalidDownloadDir(
92 self.download_dir.to_string_lossy().to_string(),
93 ));
94 }
95
96 if let Err(e) = std::fs::create_dir_all(&self.download_dir) {
98 return Err(ConfigError::InvalidDownloadDir(format!(
99 "Cannot create directory: {}",
100 e
101 )));
102 }
103
104 if self.workers == 0 || self.workers > 100 {
106 return Err(ConfigError::InvalidWorkers(self.workers));
107 }
108
109 if self.urls.is_empty() {
111 return Err(ConfigError::NoUrls);
112 }
113
114 if self.urls.len() > 163 {
116 return Err(ConfigError::InvalidUrl(format!(
117 "Too many URLs (max 163), got {}",
118 self.urls.len()
119 )));
120 }
121
122 for (index, url) in self.urls.iter().enumerate() {
123 if !url.starts_with("http://") && !url.starts_with("https://") {
124 return Err(ConfigError::InvalidUrl(format!(
125 "Invalid URL format at index {}: {}",
126 index, url
127 )));
128 }
129 }
130
131 Ok(())
132 }
133
134 pub fn from_file(path: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
135 let content = std::fs::read_to_string(path)?;
136 let config: Config = toml::from_str(&content)?;
137 Ok(config)
138 }
139}
140
141impl FromStr for Config {
142 type Err = toml::de::Error;
143
144 fn from_str(s: &str) -> Result<Self, Self::Err> {
145 toml::from_str(s)
146 }
147}