use crate::error::{EngineError, Result};
use crate::scheduler::ScheduleRule;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EngineConfig {
pub download_dir: PathBuf,
pub max_concurrent_downloads: usize,
pub max_connections_per_download: usize,
pub min_segment_size: u64,
pub global_download_limit: Option<u64>,
pub global_upload_limit: Option<u64>,
#[serde(default)]
pub schedule_rules: Vec<ScheduleRule>,
pub user_agent: String,
pub enable_dht: bool,
pub enable_pex: bool,
pub enable_lpd: bool,
pub max_peers: usize,
pub seed_ratio: f64,
pub database_path: Option<PathBuf>,
pub http: HttpConfig,
pub torrent: TorrentConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpConfig {
pub connect_timeout: u64,
pub read_timeout: u64,
pub max_redirects: usize,
pub max_retries: usize,
pub retry_delay_ms: u64,
pub max_retry_delay_ms: u64,
pub accept_invalid_certs: bool,
pub proxy_url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum AllocationMode {
#[default]
None,
Sparse,
Full,
}
impl std::fmt::Display for AllocationMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Sparse => write!(f, "sparse"),
Self::Full => write!(f, "full"),
}
}
}
impl std::str::FromStr for AllocationMode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"none" => Ok(Self::None),
"sparse" => Ok(Self::Sparse),
"full" | "preallocate" => Ok(Self::Full),
_ => Err(format!("Invalid allocation mode: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TorrentConfig {
pub listen_port_range: (u16, u16),
pub dht_bootstrap_nodes: Vec<String>,
#[serde(default)]
pub allocation_mode: AllocationMode,
pub tracker_update_interval: u64,
pub peer_timeout: u64,
pub max_pending_requests: usize,
pub enable_endgame: bool,
#[serde(default = "default_tick_interval_ms")]
pub tick_interval_ms: u64,
#[serde(default = "default_connect_interval_secs")]
pub connect_interval_secs: u64,
#[serde(default = "default_choking_interval_secs")]
pub choking_interval_secs: u64,
#[serde(default)]
pub webseed: WebSeedConfig,
#[serde(default)]
pub encryption: EncryptionConfig,
#[serde(default)]
pub utp: UtpConfigSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebSeedConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_webseed_connections")]
pub max_connections: usize,
#[serde(default = "default_webseed_timeout")]
pub timeout_seconds: u64,
#[serde(default = "default_webseed_max_failures")]
pub max_failures: u32,
}
fn default_true() -> bool {
true
}
fn default_webseed_connections() -> usize {
4
}
fn default_webseed_timeout() -> u64 {
30
}
fn default_webseed_max_failures() -> u32 {
5
}
fn default_tick_interval_ms() -> u64 {
100
}
fn default_connect_interval_secs() -> u64 {
5
}
fn default_choking_interval_secs() -> u64 {
10
}
impl Default for WebSeedConfig {
fn default() -> Self {
Self {
enabled: true,
max_connections: 4,
timeout_seconds: 30,
max_failures: 5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum EncryptionPolicy {
Disabled,
Allowed,
#[default]
Preferred,
Required,
}
impl std::fmt::Display for EncryptionPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disabled => write!(f, "disabled"),
Self::Allowed => write!(f, "allowed"),
Self::Preferred => write!(f, "preferred"),
Self::Required => write!(f, "required"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionConfig {
#[serde(default)]
pub policy: EncryptionPolicy,
#[serde(default = "default_true")]
pub allow_plaintext: bool,
#[serde(default = "default_true")]
pub allow_rc4: bool,
#[serde(default)]
pub min_padding: usize,
#[serde(default = "default_max_padding")]
pub max_padding: usize,
}
fn default_max_padding() -> usize {
512
}
impl Default for EncryptionConfig {
fn default() -> Self {
Self {
policy: EncryptionPolicy::Preferred,
allow_plaintext: true,
allow_rc4: true,
min_padding: 0,
max_padding: 512,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TransportPolicy {
TcpOnly,
UtpOnly,
#[default]
PreferUtp,
PreferTcp,
}
impl std::fmt::Display for TransportPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TcpOnly => write!(f, "tcp-only"),
Self::UtpOnly => write!(f, "utp-only"),
Self::PreferUtp => write!(f, "prefer-utp"),
Self::PreferTcp => write!(f, "prefer-tcp"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UtpConfigSettings {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub policy: TransportPolicy,
#[serde(default = "default_true")]
pub tcp_fallback: bool,
#[serde(default = "default_target_delay")]
pub target_delay_us: u32,
#[serde(default = "default_max_window")]
pub max_window_size: u32,
#[serde(default = "default_recv_window")]
pub recv_window: u32,
#[serde(default = "default_true")]
pub enable_sack: bool,
}
fn default_target_delay() -> u32 {
100_000 }
fn default_max_window() -> u32 {
1024 * 1024 }
fn default_recv_window() -> u32 {
1024 * 1024 }
impl Default for UtpConfigSettings {
fn default() -> Self {
Self {
enabled: false,
policy: TransportPolicy::PreferUtp,
tcp_fallback: true,
target_delay_us: 100_000,
max_window_size: 1024 * 1024,
recv_window: 1024 * 1024,
enable_sack: true,
}
}
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
download_dir: dirs::download_dir().unwrap_or_else(|| PathBuf::from(".")),
max_concurrent_downloads: 5,
max_connections_per_download: 16,
min_segment_size: 1024 * 1024, global_download_limit: None,
global_upload_limit: None,
schedule_rules: Vec::new(),
user_agent: format!("gosh-dl/{}", env!("CARGO_PKG_VERSION")),
enable_dht: true,
enable_pex: true,
enable_lpd: true,
max_peers: 55,
seed_ratio: 1.0,
database_path: None,
http: HttpConfig::default(),
torrent: TorrentConfig::default(),
}
}
}
impl Default for HttpConfig {
fn default() -> Self {
Self {
connect_timeout: 30,
read_timeout: 60,
max_redirects: 10,
max_retries: 5,
retry_delay_ms: 1000,
max_retry_delay_ms: 30000,
accept_invalid_certs: false,
proxy_url: None,
}
}
}
impl Default for TorrentConfig {
fn default() -> Self {
Self {
listen_port_range: (6881, 6889),
dht_bootstrap_nodes: vec![
"router.bittorrent.com:6881".to_string(),
"router.utorrent.com:6881".to_string(),
"dht.transmissionbt.com:6881".to_string(),
],
allocation_mode: AllocationMode::None,
tracker_update_interval: 1800, peer_timeout: 120,
max_pending_requests: 16,
enable_endgame: true,
tick_interval_ms: 100,
connect_interval_secs: 5,
choking_interval_secs: 10,
webseed: WebSeedConfig::default(),
encryption: EncryptionConfig::default(),
utp: UtpConfigSettings::default(),
}
}
}
impl EngineConfig {
pub fn new() -> Self {
Self::default()
}
pub fn download_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.download_dir = path.into();
self
}
pub fn max_concurrent_downloads(mut self, max: usize) -> Self {
self.max_concurrent_downloads = max;
self
}
pub fn max_connections_per_download(mut self, max: usize) -> Self {
self.max_connections_per_download = max;
self
}
pub fn download_limit(mut self, limit: Option<u64>) -> Self {
self.global_download_limit = limit;
self
}
pub fn upload_limit(mut self, limit: Option<u64>) -> Self {
self.global_upload_limit = limit;
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = ua.into();
self
}
pub fn schedule_rules(mut self, rules: Vec<ScheduleRule>) -> Self {
self.schedule_rules = rules;
self
}
pub fn add_schedule_rule(mut self, rule: ScheduleRule) -> Self {
self.schedule_rules.push(rule);
self
}
pub fn database_path(mut self, path: impl Into<PathBuf>) -> Self {
self.database_path = Some(path.into());
self
}
pub fn validate(&self) -> Result<()> {
if !self.download_dir.exists() {
return Err(EngineError::invalid_input(
"download_dir",
format!("Directory does not exist: {:?}", self.download_dir),
));
}
if !self.download_dir.is_dir() {
return Err(EngineError::invalid_input(
"download_dir",
format!("Path is not a directory: {:?}", self.download_dir),
));
}
if self.max_concurrent_downloads == 0 {
return Err(EngineError::invalid_input(
"max_concurrent_downloads",
"Must be at least 1",
));
}
if self.max_connections_per_download == 0 {
return Err(EngineError::invalid_input(
"max_connections_per_download",
"Must be at least 1",
));
}
if self.seed_ratio < 0.0 {
return Err(EngineError::invalid_input(
"seed_ratio",
"Must be non-negative",
));
}
if self.torrent.listen_port_range.0 > self.torrent.listen_port_range.1 {
return Err(EngineError::invalid_input(
"listen_port_range",
"Start port must be <= end port",
));
}
Ok(())
}
pub fn get_database_path(&self) -> PathBuf {
self.database_path.clone().unwrap_or_else(|| {
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("gosh-dl")
.join("gosh-dl.db")
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_default_config() {
let config = EngineConfig::default();
assert_eq!(config.max_concurrent_downloads, 5);
assert_eq!(config.max_connections_per_download, 16);
assert!(config.enable_dht);
}
#[test]
fn test_config_builder() {
let config = EngineConfig::new()
.max_concurrent_downloads(10)
.max_connections_per_download(8)
.download_limit(Some(1024 * 1024));
assert_eq!(config.max_concurrent_downloads, 10);
assert_eq!(config.max_connections_per_download, 8);
assert_eq!(config.global_download_limit, Some(1024 * 1024));
}
#[test]
fn test_config_validation() {
let dir = tempdir().unwrap();
let config = EngineConfig::new().download_dir(dir.path());
assert!(config.validate().is_ok());
}
#[test]
fn test_invalid_download_dir() {
let config = EngineConfig::new().download_dir("/nonexistent/path/12345");
assert!(config.validate().is_err());
}
}