use std::sync::Arc;
use std::time::Duration;
use crate::dht::BootstrapNode;
use crate::storage::{FileStorageFactory, StorageFactory};
pub type InfoHash = [u8; 20];
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SessionConfig {
pub listen_port: u16,
pub max_connections: u32,
pub max_uploads: u32,
pub download_rate_limit: Option<u64>,
pub upload_rate_limit: Option<u64>,
pub max_active_torrents: usize,
pub max_concurrent_pieces: usize,
pub piece_cache_size: usize,
pub endgame_threshold: usize,
pub request_timeout: Duration,
pub peer_connect_timeout: Duration,
pub peer_max_retries: u32,
pub peer_cooldown: Duration,
pub choke_interval: Duration,
pub snub_timeout: Duration,
pub corrupt_ban_threshold: u32,
pub announce_fallback_interval: Duration,
pub tracker_timeout: Duration,
pub dht_poll_interval: Duration,
pub pex_enabled: bool,
pub pex_interval: Duration,
pub peer_msg_buffer_size: usize,
#[cfg_attr(feature = "serde", serde(skip, default = "default_storage_factory"))]
pub storage_factory: Arc<dyn StorageFactory>,
pub bootstrap_nodes: Option<Vec<BootstrapNode>>,
pub node_id: Option<[u8; 20]>,
}
impl Default for SessionConfig {
fn default() -> Self {
SessionConfig {
listen_port: 6881,
max_connections: 50,
max_uploads: 8,
download_rate_limit: None,
upload_rate_limit: None,
max_active_torrents: 0,
max_concurrent_pieces: 5,
piece_cache_size: 256,
endgame_threshold: 10,
request_timeout: Duration::from_secs(60),
peer_connect_timeout: Duration::from_millis(500),
peer_max_retries: 3,
peer_cooldown: Duration::from_secs(30),
choke_interval: Duration::from_secs(10),
snub_timeout: Duration::from_secs(60),
corrupt_ban_threshold: 10,
announce_fallback_interval: Duration::from_secs(30),
tracker_timeout: Duration::from_secs(15),
dht_poll_interval: Duration::from_secs(30),
pex_enabled: true,
pex_interval: Duration::from_secs(60),
peer_msg_buffer_size: 256,
bootstrap_nodes: Some(vec![
BootstrapNode::from(("router.bittorrent.com", 6881)),
BootstrapNode::from(("dht.transmissionbt.com", 6881)),
]),
node_id: None,
storage_factory: Arc::new(FileStorageFactory),
}
}
}
#[cfg(feature = "serde")]
fn default_storage_factory() -> Arc<dyn StorageFactory> {
Arc::new(FileStorageFactory)
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct TorrentStatus {
pub info_hash: InfoHash,
pub name: String,
pub progress: f64,
pub download_rate: f64,
pub upload_rate: f64,
pub num_peers: usize,
pub num_seeds: usize,
pub state: TorrentState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TorrentState {
Queued,
Downloading,
Seeding,
Paused,
Error,
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use std::time::Duration;
use super::*;
#[test]
fn session_config_roundtrip_default() {
let config = SessionConfig::default();
let json = serde_json::to_string_pretty(&config).unwrap();
let back: SessionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.listen_port, config.listen_port);
assert_eq!(back.max_connections, config.max_connections);
assert_eq!(back.max_uploads, config.max_uploads);
assert_eq!(back.download_rate_limit, config.download_rate_limit);
assert_eq!(back.upload_rate_limit, config.upload_rate_limit);
assert_eq!(back.max_active_torrents, config.max_active_torrents);
assert_eq!(back.max_concurrent_pieces, config.max_concurrent_pieces);
assert_eq!(back.piece_cache_size, config.piece_cache_size);
assert_eq!(back.endgame_threshold, config.endgame_threshold);
assert_eq!(back.request_timeout, config.request_timeout);
assert_eq!(back.peer_connect_timeout, config.peer_connect_timeout);
assert_eq!(back.peer_max_retries, config.peer_max_retries);
assert_eq!(back.peer_cooldown, config.peer_cooldown);
assert_eq!(back.choke_interval, config.choke_interval);
assert_eq!(back.snub_timeout, config.snub_timeout);
assert_eq!(back.corrupt_ban_threshold, config.corrupt_ban_threshold);
assert_eq!(
back.announce_fallback_interval,
config.announce_fallback_interval
);
assert_eq!(back.tracker_timeout, config.tracker_timeout);
assert_eq!(back.node_id, config.node_id);
assert_eq!(back.dht_poll_interval, config.dht_poll_interval);
assert_eq!(back.pex_enabled, config.pex_enabled);
assert_eq!(back.pex_interval, config.pex_interval);
assert_eq!(back.peer_msg_buffer_size, config.peer_msg_buffer_size);
}
#[test]
fn session_config_roundtrip_custom() {
let config = SessionConfig {
listen_port: 12345,
max_connections: 200,
max_uploads: 16,
download_rate_limit: Some(1_048_576),
upload_rate_limit: Some(524_288),
max_active_torrents: 5,
max_concurrent_pieces: 10,
piece_cache_size: 128,
endgame_threshold: 5,
request_timeout: Duration::from_secs(120),
peer_connect_timeout: Duration::from_secs(2),
peer_max_retries: 5,
peer_cooldown: Duration::from_secs(60),
choke_interval: Duration::from_secs(20),
snub_timeout: Duration::from_secs(120),
corrupt_ban_threshold: 5,
announce_fallback_interval: Duration::from_secs(60),
tracker_timeout: Duration::from_secs(30),
bootstrap_nodes: None,
node_id: Some([0xAB; 20]),
dht_poll_interval: Duration::from_secs(60),
pex_enabled: false,
pex_interval: Duration::from_secs(120),
peer_msg_buffer_size: 512,
storage_factory: Arc::new(FileStorageFactory),
};
let json = serde_json::to_string(&config).unwrap();
let back: SessionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.listen_port, 12345);
assert_eq!(back.max_connections, 200);
assert_eq!(back.max_uploads, 16);
assert_eq!(back.download_rate_limit, Some(1_048_576));
assert_eq!(back.upload_rate_limit, Some(524_288));
assert_eq!(back.max_active_torrents, 5);
assert_eq!(back.max_concurrent_pieces, 10);
assert_eq!(back.piece_cache_size, 128);
assert_eq!(back.endgame_threshold, 5);
assert_eq!(back.request_timeout, Duration::from_secs(120));
assert_eq!(back.peer_connect_timeout, Duration::from_secs(2));
assert_eq!(back.peer_max_retries, 5);
assert_eq!(back.peer_cooldown, Duration::from_secs(60));
assert_eq!(back.choke_interval, Duration::from_secs(20));
assert_eq!(back.snub_timeout, Duration::from_secs(120));
assert_eq!(back.corrupt_ban_threshold, 5);
assert_eq!(back.announce_fallback_interval, Duration::from_secs(60));
assert_eq!(back.tracker_timeout, Duration::from_secs(30));
assert!(back.bootstrap_nodes.is_none());
assert_eq!(back.node_id, Some([0xAB; 20]));
assert_eq!(back.dht_poll_interval, Duration::from_secs(60));
assert_eq!(back.pex_enabled, false);
assert_eq!(back.pex_interval, Duration::from_secs(120));
assert_eq!(back.peer_msg_buffer_size, 512);
}
#[test]
fn session_config_duration_fields_use_default_serde() {
let config = SessionConfig::default();
let json = serde_json::to_value(&config).unwrap();
assert!(json["request_timeout"].is_object());
assert_eq!(json["request_timeout"]["secs"], 60);
assert_eq!(json["peer_connect_timeout"]["nanos"], 500_000_000);
}
#[test]
fn torrent_status_serialize() {
let status = TorrentStatus {
info_hash: [0x42; 20],
name: "test.iso".into(),
progress: 0.75,
download_rate: 1_048_576.0,
upload_rate: 512_000.0,
num_peers: 12,
num_seeds: 3,
state: TorrentState::Downloading,
};
let json = serde_json::to_string(&status).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["name"], "test.iso");
assert!((v["progress"].as_f64().unwrap() - 0.75).abs() < 0.001);
assert_eq!(v["num_peers"], 12);
assert_eq!(v["num_seeds"], 3);
assert_eq!(v["state"], "Downloading");
}
#[test]
fn torrent_state_roundtrip() {
let states = [
TorrentState::Queued,
TorrentState::Downloading,
TorrentState::Seeding,
TorrentState::Paused,
TorrentState::Error,
];
for &state in &states {
let json = serde_json::to_string(&state).unwrap();
let back: TorrentState = serde_json::from_str(&json).unwrap();
assert_eq!(back, state);
}
}
}