1use crate::error::{EngineError, Result};
6use crate::scheduler::ScheduleRule;
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct EngineConfig {
13 pub download_dir: PathBuf,
15
16 pub max_concurrent_downloads: usize,
18
19 pub max_connections_per_download: usize,
21
22 pub min_segment_size: u64,
24
25 pub global_download_limit: Option<u64>,
27
28 pub global_upload_limit: Option<u64>,
30
31 #[serde(default)]
34 pub schedule_rules: Vec<ScheduleRule>,
35
36 pub user_agent: String,
38
39 pub enable_dht: bool,
41
42 pub enable_pex: bool,
44
45 pub enable_lpd: bool,
47
48 pub max_peers: usize,
50
51 pub seed_ratio: f64,
53
54 pub database_path: Option<PathBuf>,
56
57 pub http: HttpConfig,
59
60 pub torrent: TorrentConfig,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct HttpConfig {
67 pub connect_timeout: u64,
69
70 pub read_timeout: u64,
72
73 pub max_redirects: usize,
75
76 pub max_retries: usize,
78
79 pub retry_delay_ms: u64,
81
82 pub max_retry_delay_ms: u64,
84
85 pub accept_invalid_certs: bool,
87
88 pub proxy_url: Option<String>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
95#[serde(rename_all = "lowercase")]
96pub enum AllocationMode {
97 #[default]
99 None,
100 Sparse,
102 Full,
104}
105
106impl std::fmt::Display for AllocationMode {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 Self::None => write!(f, "none"),
110 Self::Sparse => write!(f, "sparse"),
111 Self::Full => write!(f, "full"),
112 }
113 }
114}
115
116impl std::str::FromStr for AllocationMode {
117 type Err = String;
118
119 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
120 match s.to_lowercase().as_str() {
121 "none" => Ok(Self::None),
122 "sparse" => Ok(Self::Sparse),
123 "full" | "preallocate" => Ok(Self::Full),
124 _ => Err(format!("Invalid allocation mode: {}", s)),
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct TorrentConfig {
132 pub listen_port_range: (u16, u16),
134
135 pub dht_bootstrap_nodes: Vec<String>,
137
138 #[serde(default)]
140 pub allocation_mode: AllocationMode,
141
142 pub tracker_update_interval: u64,
144
145 pub peer_timeout: u64,
147
148 pub max_pending_requests: usize,
150
151 pub enable_endgame: bool,
153
154 #[serde(default = "default_tick_interval_ms")]
158 pub tick_interval_ms: u64,
159
160 #[serde(default = "default_connect_interval_secs")]
164 pub connect_interval_secs: u64,
165
166 #[serde(default = "default_choking_interval_secs")]
172 pub choking_interval_secs: u64,
173
174 #[serde(default)]
176 pub webseed: WebSeedConfig,
177
178 #[serde(default)]
180 pub encryption: EncryptionConfig,
181
182 #[serde(default)]
184 pub utp: UtpConfigSettings,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct WebSeedConfig {
190 #[serde(default = "default_true")]
192 pub enabled: bool,
193
194 #[serde(default = "default_webseed_connections")]
196 pub max_connections: usize,
197
198 #[serde(default = "default_webseed_timeout")]
200 pub timeout_seconds: u64,
201
202 #[serde(default = "default_webseed_max_failures")]
204 pub max_failures: u32,
205}
206
207fn default_true() -> bool {
208 true
209}
210
211fn default_webseed_connections() -> usize {
212 4
213}
214
215fn default_webseed_timeout() -> u64 {
216 30
217}
218
219fn default_webseed_max_failures() -> u32 {
220 5
221}
222
223fn default_tick_interval_ms() -> u64 {
224 100
225}
226
227fn default_connect_interval_secs() -> u64 {
228 5
229}
230
231fn default_choking_interval_secs() -> u64 {
232 10
233}
234
235impl Default for WebSeedConfig {
236 fn default() -> Self {
237 Self {
238 enabled: true,
239 max_connections: 4,
240 timeout_seconds: 30,
241 max_failures: 5,
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
248#[serde(rename_all = "lowercase")]
249pub enum EncryptionPolicy {
250 Disabled,
252 Allowed,
254 #[default]
256 Preferred,
257 Required,
259}
260
261impl std::fmt::Display for EncryptionPolicy {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 match self {
264 Self::Disabled => write!(f, "disabled"),
265 Self::Allowed => write!(f, "allowed"),
266 Self::Preferred => write!(f, "preferred"),
267 Self::Required => write!(f, "required"),
268 }
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct EncryptionConfig {
275 #[serde(default)]
277 pub policy: EncryptionPolicy,
278
279 #[serde(default = "default_true")]
281 pub allow_plaintext: bool,
282
283 #[serde(default = "default_true")]
285 pub allow_rc4: bool,
286
287 #[serde(default)]
289 pub min_padding: usize,
290
291 #[serde(default = "default_max_padding")]
293 pub max_padding: usize,
294}
295
296fn default_max_padding() -> usize {
297 512
298}
299
300impl Default for EncryptionConfig {
301 fn default() -> Self {
302 Self {
303 policy: EncryptionPolicy::Preferred,
304 allow_plaintext: true,
305 allow_rc4: true,
306 min_padding: 0,
307 max_padding: 512,
308 }
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
314#[serde(rename_all = "lowercase")]
315pub enum TransportPolicy {
316 TcpOnly,
318 UtpOnly,
320 #[default]
322 PreferUtp,
323 PreferTcp,
325}
326
327impl std::fmt::Display for TransportPolicy {
328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329 match self {
330 Self::TcpOnly => write!(f, "tcp-only"),
331 Self::UtpOnly => write!(f, "utp-only"),
332 Self::PreferUtp => write!(f, "prefer-utp"),
333 Self::PreferTcp => write!(f, "prefer-tcp"),
334 }
335 }
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct UtpConfigSettings {
341 #[serde(default = "default_true")]
343 pub enabled: bool,
344
345 #[serde(default)]
347 pub policy: TransportPolicy,
348
349 #[serde(default = "default_true")]
351 pub tcp_fallback: bool,
352
353 #[serde(default = "default_target_delay")]
355 pub target_delay_us: u32,
356
357 #[serde(default = "default_max_window")]
359 pub max_window_size: u32,
360
361 #[serde(default = "default_recv_window")]
363 pub recv_window: u32,
364
365 #[serde(default = "default_true")]
367 pub enable_sack: bool,
368}
369
370fn default_target_delay() -> u32 {
371 100_000 }
373
374fn default_max_window() -> u32 {
375 1024 * 1024 }
377
378fn default_recv_window() -> u32 {
379 1024 * 1024 }
381
382impl Default for UtpConfigSettings {
383 fn default() -> Self {
384 Self {
385 enabled: true,
386 policy: TransportPolicy::PreferUtp,
387 tcp_fallback: true,
388 target_delay_us: 100_000,
389 max_window_size: 1024 * 1024,
390 recv_window: 1024 * 1024,
391 enable_sack: true,
392 }
393 }
394}
395
396impl Default for EngineConfig {
397 fn default() -> Self {
398 Self {
399 download_dir: dirs::download_dir().unwrap_or_else(|| PathBuf::from(".")),
400 max_concurrent_downloads: 5,
401 max_connections_per_download: 16,
402 min_segment_size: 1024 * 1024, global_download_limit: None,
404 global_upload_limit: None,
405 schedule_rules: Vec::new(),
406 user_agent: format!("gosh-dl/{}", env!("CARGO_PKG_VERSION")),
407 enable_dht: true,
408 enable_pex: true,
409 enable_lpd: true,
410 max_peers: 55,
411 seed_ratio: 1.0,
412 database_path: None,
413 http: HttpConfig::default(),
414 torrent: TorrentConfig::default(),
415 }
416 }
417}
418
419impl Default for HttpConfig {
420 fn default() -> Self {
421 Self {
422 connect_timeout: 30,
423 read_timeout: 60,
424 max_redirects: 10,
425 max_retries: 3,
426 retry_delay_ms: 1000,
427 max_retry_delay_ms: 30000,
428 accept_invalid_certs: false,
429 proxy_url: None,
430 }
431 }
432}
433
434impl Default for TorrentConfig {
435 fn default() -> Self {
436 Self {
437 listen_port_range: (6881, 6889),
438 dht_bootstrap_nodes: vec![
439 "router.bittorrent.com:6881".to_string(),
440 "router.utorrent.com:6881".to_string(),
441 "dht.transmissionbt.com:6881".to_string(),
442 ],
443 allocation_mode: AllocationMode::None,
444 tracker_update_interval: 1800, peer_timeout: 120,
446 max_pending_requests: 16,
447 enable_endgame: true,
448 tick_interval_ms: 100,
449 connect_interval_secs: 5,
450 choking_interval_secs: 10,
451 webseed: WebSeedConfig::default(),
452 encryption: EncryptionConfig::default(),
453 utp: UtpConfigSettings::default(),
454 }
455 }
456}
457
458impl EngineConfig {
459 pub fn new() -> Self {
461 Self::default()
462 }
463
464 pub fn download_dir(mut self, path: impl Into<PathBuf>) -> Self {
466 self.download_dir = path.into();
467 self
468 }
469
470 pub fn max_concurrent_downloads(mut self, max: usize) -> Self {
472 self.max_concurrent_downloads = max;
473 self
474 }
475
476 pub fn max_connections_per_download(mut self, max: usize) -> Self {
478 self.max_connections_per_download = max;
479 self
480 }
481
482 pub fn download_limit(mut self, limit: Option<u64>) -> Self {
484 self.global_download_limit = limit;
485 self
486 }
487
488 pub fn upload_limit(mut self, limit: Option<u64>) -> Self {
490 self.global_upload_limit = limit;
491 self
492 }
493
494 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
496 self.user_agent = ua.into();
497 self
498 }
499
500 pub fn schedule_rules(mut self, rules: Vec<ScheduleRule>) -> Self {
502 self.schedule_rules = rules;
503 self
504 }
505
506 pub fn add_schedule_rule(mut self, rule: ScheduleRule) -> Self {
508 self.schedule_rules.push(rule);
509 self
510 }
511
512 pub fn database_path(mut self, path: impl Into<PathBuf>) -> Self {
514 self.database_path = Some(path.into());
515 self
516 }
517
518 pub fn validate(&self) -> Result<()> {
520 if !self.download_dir.exists() {
522 return Err(EngineError::invalid_input(
523 "download_dir",
524 format!("Directory does not exist: {:?}", self.download_dir),
525 ));
526 }
527
528 if !self.download_dir.is_dir() {
529 return Err(EngineError::invalid_input(
530 "download_dir",
531 format!("Path is not a directory: {:?}", self.download_dir),
532 ));
533 }
534
535 if self.max_concurrent_downloads == 0 {
537 return Err(EngineError::invalid_input(
538 "max_concurrent_downloads",
539 "Must be at least 1",
540 ));
541 }
542
543 if self.max_connections_per_download == 0 {
544 return Err(EngineError::invalid_input(
545 "max_connections_per_download",
546 "Must be at least 1",
547 ));
548 }
549
550 if self.seed_ratio < 0.0 {
551 return Err(EngineError::invalid_input(
552 "seed_ratio",
553 "Must be non-negative",
554 ));
555 }
556
557 if self.torrent.listen_port_range.0 > self.torrent.listen_port_range.1 {
559 return Err(EngineError::invalid_input(
560 "listen_port_range",
561 "Start port must be <= end port",
562 ));
563 }
564
565 Ok(())
566 }
567
568 pub fn get_database_path(&self) -> PathBuf {
570 self.database_path.clone().unwrap_or_else(|| {
571 dirs::data_dir()
572 .unwrap_or_else(|| PathBuf::from("."))
573 .join("gosh-dl")
574 .join("gosh-dl.db")
575 })
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use tempfile::tempdir;
583
584 #[test]
585 fn test_default_config() {
586 let config = EngineConfig::default();
587 assert_eq!(config.max_concurrent_downloads, 5);
588 assert_eq!(config.max_connections_per_download, 16);
589 assert!(config.enable_dht);
590 }
591
592 #[test]
593 fn test_config_builder() {
594 let config = EngineConfig::new()
595 .max_concurrent_downloads(10)
596 .max_connections_per_download(8)
597 .download_limit(Some(1024 * 1024));
598
599 assert_eq!(config.max_concurrent_downloads, 10);
600 assert_eq!(config.max_connections_per_download, 8);
601 assert_eq!(config.global_download_limit, Some(1024 * 1024));
602 }
603
604 #[test]
605 fn test_config_validation() {
606 let dir = tempdir().unwrap();
607 let config = EngineConfig::new().download_dir(dir.path());
608 assert!(config.validate().is_ok());
609 }
610
611 #[test]
612 fn test_invalid_download_dir() {
613 let config = EngineConfig::new().download_dir("/nonexistent/path/12345");
614 assert!(config.validate().is_err());
615 }
616}