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,
74
75 pub max_redirects: usize,
77
78 pub max_retries: usize,
80
81 pub retry_delay_ms: u64,
83
84 pub max_retry_delay_ms: u64,
86
87 pub accept_invalid_certs: bool,
89
90 pub proxy_url: Option<String>,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
97#[serde(rename_all = "lowercase")]
98pub enum AllocationMode {
99 #[default]
101 None,
102 Sparse,
104 Full,
106}
107
108impl std::fmt::Display for AllocationMode {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 Self::None => write!(f, "none"),
112 Self::Sparse => write!(f, "sparse"),
113 Self::Full => write!(f, "full"),
114 }
115 }
116}
117
118impl std::str::FromStr for AllocationMode {
119 type Err = String;
120
121 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
122 match s.to_lowercase().as_str() {
123 "none" => Ok(Self::None),
124 "sparse" => Ok(Self::Sparse),
125 "full" | "preallocate" => Ok(Self::Full),
126 _ => Err(format!("Invalid allocation mode: {}", s)),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct TorrentConfig {
134 pub listen_port_range: (u16, u16),
136
137 pub dht_bootstrap_nodes: Vec<String>,
139
140 #[serde(default)]
142 pub allocation_mode: AllocationMode,
143
144 pub tracker_update_interval: u64,
146
147 pub peer_timeout: u64,
149
150 pub max_pending_requests: usize,
152
153 pub enable_endgame: bool,
155
156 #[serde(default = "default_tick_interval_ms")]
160 pub tick_interval_ms: u64,
161
162 #[serde(default = "default_connect_interval_secs")]
166 pub connect_interval_secs: u64,
167
168 #[serde(default = "default_choking_interval_secs")]
174 pub choking_interval_secs: u64,
175
176 #[serde(default)]
178 pub webseed: WebSeedConfig,
179
180 #[serde(default)]
182 pub encryption: EncryptionConfig,
183
184 #[serde(default)]
186 pub utp: UtpConfigSettings,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct WebSeedConfig {
192 #[serde(default = "default_true")]
194 pub enabled: bool,
195
196 #[serde(default = "default_webseed_connections")]
198 pub max_connections: usize,
199
200 #[serde(default = "default_webseed_timeout")]
202 pub timeout_seconds: u64,
203
204 #[serde(default = "default_webseed_max_failures")]
206 pub max_failures: u32,
207}
208
209fn default_true() -> bool {
210 true
211}
212
213fn default_webseed_connections() -> usize {
214 4
215}
216
217fn default_webseed_timeout() -> u64 {
218 30
219}
220
221fn default_webseed_max_failures() -> u32 {
222 5
223}
224
225fn default_tick_interval_ms() -> u64 {
226 100
227}
228
229fn default_connect_interval_secs() -> u64 {
230 5
231}
232
233fn default_choking_interval_secs() -> u64 {
234 10
235}
236
237impl Default for WebSeedConfig {
238 fn default() -> Self {
239 Self {
240 enabled: true,
241 max_connections: 4,
242 timeout_seconds: 30,
243 max_failures: 5,
244 }
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
250#[serde(rename_all = "lowercase")]
251pub enum EncryptionPolicy {
252 Disabled,
254 Allowed,
256 #[default]
258 Preferred,
259 Required,
261}
262
263impl std::fmt::Display for EncryptionPolicy {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 match self {
266 Self::Disabled => write!(f, "disabled"),
267 Self::Allowed => write!(f, "allowed"),
268 Self::Preferred => write!(f, "preferred"),
269 Self::Required => write!(f, "required"),
270 }
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct EncryptionConfig {
277 #[serde(default)]
279 pub policy: EncryptionPolicy,
280
281 #[serde(default = "default_true")]
283 pub allow_plaintext: bool,
284
285 #[serde(default = "default_true")]
287 pub allow_rc4: bool,
288
289 #[serde(default)]
291 pub min_padding: usize,
292
293 #[serde(default = "default_max_padding")]
295 pub max_padding: usize,
296}
297
298fn default_max_padding() -> usize {
299 512
300}
301
302impl Default for EncryptionConfig {
303 fn default() -> Self {
304 Self {
305 policy: EncryptionPolicy::Preferred,
306 allow_plaintext: true,
307 allow_rc4: true,
308 min_padding: 0,
309 max_padding: 512,
310 }
311 }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
316#[serde(rename_all = "lowercase")]
317pub enum TransportPolicy {
318 TcpOnly,
320 UtpOnly,
322 #[default]
324 PreferUtp,
325 PreferTcp,
327}
328
329impl std::fmt::Display for TransportPolicy {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 match self {
332 Self::TcpOnly => write!(f, "tcp-only"),
333 Self::UtpOnly => write!(f, "utp-only"),
334 Self::PreferUtp => write!(f, "prefer-utp"),
335 Self::PreferTcp => write!(f, "prefer-tcp"),
336 }
337 }
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct UtpConfigSettings {
343 #[serde(default = "default_true")]
345 pub enabled: bool,
346
347 #[serde(default)]
349 pub policy: TransportPolicy,
350
351 #[serde(default = "default_true")]
353 pub tcp_fallback: bool,
354
355 #[serde(default = "default_target_delay")]
357 pub target_delay_us: u32,
358
359 #[serde(default = "default_max_window")]
361 pub max_window_size: u32,
362
363 #[serde(default = "default_recv_window")]
365 pub recv_window: u32,
366
367 #[serde(default = "default_true")]
369 pub enable_sack: bool,
370}
371
372fn default_target_delay() -> u32 {
373 100_000 }
375
376fn default_max_window() -> u32 {
377 1024 * 1024 }
379
380fn default_recv_window() -> u32 {
381 1024 * 1024 }
383
384impl Default for UtpConfigSettings {
385 fn default() -> Self {
386 Self {
387 enabled: true,
388 policy: TransportPolicy::PreferUtp,
389 tcp_fallback: true,
390 target_delay_us: 100_000,
391 max_window_size: 1024 * 1024,
392 recv_window: 1024 * 1024,
393 enable_sack: true,
394 }
395 }
396}
397
398impl Default for EngineConfig {
399 fn default() -> Self {
400 Self {
401 download_dir: dirs::download_dir().unwrap_or_else(|| PathBuf::from(".")),
402 max_concurrent_downloads: 5,
403 max_connections_per_download: 16,
404 min_segment_size: 1024 * 1024, global_download_limit: None,
406 global_upload_limit: None,
407 schedule_rules: Vec::new(),
408 user_agent: format!("gosh-dl/{}", env!("CARGO_PKG_VERSION")),
409 enable_dht: true,
410 enable_pex: true,
411 enable_lpd: true,
412 max_peers: 55,
413 seed_ratio: 1.0,
414 database_path: None,
415 http: HttpConfig::default(),
416 torrent: TorrentConfig::default(),
417 }
418 }
419}
420
421impl Default for HttpConfig {
422 fn default() -> Self {
423 Self {
424 connect_timeout: 30,
425 read_timeout: 60,
426 max_redirects: 10,
427 max_retries: 5,
428 retry_delay_ms: 1000,
429 max_retry_delay_ms: 30000,
430 accept_invalid_certs: false,
431 proxy_url: None,
432 }
433 }
434}
435
436impl Default for TorrentConfig {
437 fn default() -> Self {
438 Self {
439 listen_port_range: (6881, 6889),
440 dht_bootstrap_nodes: vec![
441 "router.bittorrent.com:6881".to_string(),
442 "router.utorrent.com:6881".to_string(),
443 "dht.transmissionbt.com:6881".to_string(),
444 ],
445 allocation_mode: AllocationMode::None,
446 tracker_update_interval: 1800, peer_timeout: 120,
448 max_pending_requests: 16,
449 enable_endgame: true,
450 tick_interval_ms: 100,
451 connect_interval_secs: 5,
452 choking_interval_secs: 10,
453 webseed: WebSeedConfig::default(),
454 encryption: EncryptionConfig::default(),
455 utp: UtpConfigSettings::default(),
456 }
457 }
458}
459
460impl EngineConfig {
461 pub fn new() -> Self {
463 Self::default()
464 }
465
466 pub fn download_dir(mut self, path: impl Into<PathBuf>) -> Self {
468 self.download_dir = path.into();
469 self
470 }
471
472 pub fn max_concurrent_downloads(mut self, max: usize) -> Self {
474 self.max_concurrent_downloads = max;
475 self
476 }
477
478 pub fn max_connections_per_download(mut self, max: usize) -> Self {
480 self.max_connections_per_download = max;
481 self
482 }
483
484 pub fn download_limit(mut self, limit: Option<u64>) -> Self {
486 self.global_download_limit = limit;
487 self
488 }
489
490 pub fn upload_limit(mut self, limit: Option<u64>) -> Self {
492 self.global_upload_limit = limit;
493 self
494 }
495
496 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
498 self.user_agent = ua.into();
499 self
500 }
501
502 pub fn schedule_rules(mut self, rules: Vec<ScheduleRule>) -> Self {
504 self.schedule_rules = rules;
505 self
506 }
507
508 pub fn add_schedule_rule(mut self, rule: ScheduleRule) -> Self {
510 self.schedule_rules.push(rule);
511 self
512 }
513
514 pub fn database_path(mut self, path: impl Into<PathBuf>) -> Self {
516 self.database_path = Some(path.into());
517 self
518 }
519
520 pub fn validate(&self) -> Result<()> {
522 if !self.download_dir.exists() {
524 return Err(EngineError::invalid_input(
525 "download_dir",
526 format!("Directory does not exist: {:?}", self.download_dir),
527 ));
528 }
529
530 if !self.download_dir.is_dir() {
531 return Err(EngineError::invalid_input(
532 "download_dir",
533 format!("Path is not a directory: {:?}", self.download_dir),
534 ));
535 }
536
537 if self.max_concurrent_downloads == 0 {
539 return Err(EngineError::invalid_input(
540 "max_concurrent_downloads",
541 "Must be at least 1",
542 ));
543 }
544
545 if self.max_connections_per_download == 0 {
546 return Err(EngineError::invalid_input(
547 "max_connections_per_download",
548 "Must be at least 1",
549 ));
550 }
551
552 if self.seed_ratio < 0.0 {
553 return Err(EngineError::invalid_input(
554 "seed_ratio",
555 "Must be non-negative",
556 ));
557 }
558
559 if self.torrent.listen_port_range.0 > self.torrent.listen_port_range.1 {
561 return Err(EngineError::invalid_input(
562 "listen_port_range",
563 "Start port must be <= end port",
564 ));
565 }
566
567 Ok(())
568 }
569
570 pub fn get_database_path(&self) -> PathBuf {
572 self.database_path.clone().unwrap_or_else(|| {
573 dirs::data_dir()
574 .unwrap_or_else(|| PathBuf::from("."))
575 .join("gosh-dl")
576 .join("gosh-dl.db")
577 })
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use tempfile::tempdir;
585
586 #[test]
587 fn test_default_config() {
588 let config = EngineConfig::default();
589 assert_eq!(config.max_concurrent_downloads, 5);
590 assert_eq!(config.max_connections_per_download, 16);
591 assert!(config.enable_dht);
592 }
593
594 #[test]
595 fn test_config_builder() {
596 let config = EngineConfig::new()
597 .max_concurrent_downloads(10)
598 .max_connections_per_download(8)
599 .download_limit(Some(1024 * 1024));
600
601 assert_eq!(config.max_concurrent_downloads, 10);
602 assert_eq!(config.max_connections_per_download, 8);
603 assert_eq!(config.global_download_limit, Some(1024 * 1024));
604 }
605
606 #[test]
607 fn test_config_validation() {
608 let dir = tempdir().unwrap();
609 let config = EngineConfig::new().download_dir(dir.path());
610 assert!(config.validate().is_ok());
611 }
612
613 #[test]
614 fn test_invalid_download_dir() {
615 let config = EngineConfig::new().download_dir("/nonexistent/path/12345");
616 assert!(config.validate().is_err());
617 }
618}