1use std::collections::HashMap;
2use std::net::SocketAddr;
3use std::path::PathBuf;
4
5use bitflags::bitflags;
6use bytes::Bytes;
7use serde::{Deserialize, Serialize};
8use tokio::sync::oneshot;
9
10use irontide_storage::Bitfield;
11use irontide_wire::ExtHandshake;
12
13use crate::choker::{ChokingAlgorithm, SeedChokingAlgorithm};
14
15#[derive(Debug, Clone)]
17pub struct TorrentConfig {
18 pub listen_port: u16,
20 pub max_peers: usize,
22 pub target_request_queue: usize,
24 pub download_dir: PathBuf,
26 pub enable_dht: bool,
28 pub enable_pex: bool,
30 pub enable_fast: bool,
32 pub seed_ratio_limit: Option<f64>,
34 pub strict_end_game: bool,
36 pub upload_rate_limit: u64,
38 pub download_rate_limit: u64,
40 pub encryption_mode: irontide_wire::mse::EncryptionMode,
42 pub enable_utp: bool,
44 pub enable_web_seed: bool,
46 pub enable_holepunch: bool,
48 pub enable_bep40_eviction: bool,
50 pub max_web_seeds: usize,
52 pub super_seeding: bool,
54 pub upload_only_announce: bool,
56 pub hashing_threads: usize,
58 pub sequential_download: bool,
60 pub initial_picker_threshold: u32,
62 pub whole_pieces_threshold: u32,
65 pub snub_timeout_secs: u32,
67 pub readahead_pieces: u32,
69 pub streaming_timeout_escalation: bool,
71 pub max_concurrent_stream_reads: usize,
73 pub proxy: crate::proxy::ProxyConfig,
75 pub anonymous_mode: bool,
77 pub share_mode: bool,
80 pub enable_i2p: bool,
82 pub allow_i2p_mixed: bool,
84 pub ssl_listen_port: u16,
86 pub seed_choking_algorithm: SeedChokingAlgorithm,
88 pub choking_algorithm: ChokingAlgorithm,
90 pub piece_extent_affinity: bool,
92 pub suggest_mode: bool,
94 pub max_suggest_pieces: usize,
96 pub predictive_piece_announce_ms: u64,
98 pub mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm,
100 pub auto_sequential: bool,
102 pub storage_mode: irontide_core::StorageMode,
104 pub preallocate_mode: Option<irontide_storage::PreallocateMode>,
106 pub block_request_timeout_secs: u32,
108 pub enable_lsd: bool,
110 pub force_proxy: bool,
112 pub steal_threshold_ratio: f64,
114 pub steal_threshold_endgame: f64,
116 pub peer_read_timeout_secs: u64,
118 pub peer_write_timeout_secs: u64,
120 pub data_contribution_timeout_secs: u64,
122 pub choke_rotation_max_evictions: u32,
124 pub max_concurrent_connects: u16,
126 pub connect_soft_timeout: u64,
128 pub url_security: crate::url_guard::UrlSecurityConfig,
130 pub peer_connect_timeout: u64,
132 pub peer_dscp: u8,
134 pub initial_queue_depth: usize,
136 pub max_request_queue_depth: usize,
138 pub request_queue_time: f64,
141 pub max_metadata_size: u64,
143 pub max_message_size: usize,
145 pub max_piece_length: u64,
147 pub max_outstanding_requests: usize,
149 pub max_in_flight_pieces: usize,
152 pub min_pipeline_depth: u32,
154 pub max_pipeline_depth: u32,
156 pub target_buffer_secs: f64,
158 pub use_block_stealing: bool,
160 pub steal_stale_piece_secs: u64,
162 pub fixed_pipeline_depth: usize,
164 pub lock_warn_threshold_ms: u64,
166 pub filesystem_direct_io: bool,
168}
169
170impl Default for TorrentConfig {
171 fn default() -> Self {
172 Self {
173 listen_port: 6881,
174 max_peers: 128,
175 target_request_queue: 5,
176 download_dir: PathBuf::from("."),
177 enable_dht: true,
178 enable_pex: true,
179 enable_fast: false,
180 seed_ratio_limit: None,
181 strict_end_game: true,
182 upload_rate_limit: 0,
183 download_rate_limit: 0,
184 encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
185 enable_utp: true,
186 enable_web_seed: true,
187 enable_holepunch: true,
188 enable_bep40_eviction: true,
189 max_web_seeds: 4,
190 super_seeding: false,
191 upload_only_announce: true,
192 hashing_threads: {
193 let cores = std::thread::available_parallelism()
194 .map(|n| n.get())
195 .unwrap_or(4);
196 (cores / 4).clamp(2, 8)
197 },
198 sequential_download: false,
199 initial_picker_threshold: 4,
200 whole_pieces_threshold: 20,
201 snub_timeout_secs: 15,
202 readahead_pieces: 8,
203 streaming_timeout_escalation: true,
204 max_concurrent_stream_reads: 8,
205 proxy: crate::proxy::ProxyConfig::default(),
206 anonymous_mode: false,
207 share_mode: false,
208 enable_i2p: false,
209 allow_i2p_mixed: false,
210 ssl_listen_port: 0,
211 seed_choking_algorithm: SeedChokingAlgorithm::FastestUpload,
212 choking_algorithm: ChokingAlgorithm::FixedSlots,
213 piece_extent_affinity: true,
214 suggest_mode: false,
215 max_suggest_pieces: 10,
216 predictive_piece_announce_ms: 0,
217 mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
218 auto_sequential: true,
219 storage_mode: irontide_core::StorageMode::Auto,
220 preallocate_mode: None,
221 block_request_timeout_secs: 60,
222 enable_lsd: true,
223 force_proxy: false,
224 steal_threshold_ratio: 10.0,
225 steal_threshold_endgame: 3.0,
226 min_pipeline_depth: 16,
227 max_pipeline_depth: 512,
228 target_buffer_secs: 2.0,
229 peer_read_timeout_secs: 10,
230 peer_write_timeout_secs: 10,
231 data_contribution_timeout_secs: 0,
232 choke_rotation_max_evictions: 0,
233 max_concurrent_connects: 128,
234 connect_soft_timeout: 3,
235 url_security: crate::url_guard::UrlSecurityConfig::default(),
236 peer_connect_timeout: 10,
237 peer_dscp: 0x08,
238 initial_queue_depth: 128,
239 max_request_queue_depth: 250,
240 request_queue_time: 3.0,
241 max_metadata_size: 4 * 1024 * 1024,
242 max_message_size: 16 * 1024 * 1024,
243 max_piece_length: 32 * 1024 * 1024,
244 max_outstanding_requests: 500,
245 max_in_flight_pieces: 512,
246 use_block_stealing: true,
247 steal_stale_piece_secs: 2,
248 fixed_pipeline_depth: 128,
249 lock_warn_threshold_ms: 50,
250 filesystem_direct_io: false,
251 }
252 }
253}
254
255impl From<&crate::settings::Settings> for TorrentConfig {
256 fn from(s: &crate::settings::Settings) -> Self {
257 Self {
258 listen_port: 0, max_peers: s.max_peers_per_torrent,
260 target_request_queue: 5,
261 download_dir: s.download_dir.clone(),
262 enable_dht: s.enable_dht,
263 enable_pex: s.enable_pex,
264 enable_fast: s.enable_fast_extension,
265 seed_ratio_limit: s.seed_ratio_limit,
266 strict_end_game: s.strict_end_game,
267 upload_rate_limit: s.upload_rate_limit,
268 download_rate_limit: s.download_rate_limit,
269 encryption_mode: s.encryption_mode,
270 enable_utp: s.enable_utp,
271 enable_web_seed: s.enable_web_seed,
272 enable_holepunch: s.enable_holepunch,
273 enable_bep40_eviction: s.enable_bep40_eviction,
274 max_web_seeds: s.max_web_seeds,
275 super_seeding: s.default_super_seeding,
276 upload_only_announce: s.upload_only_announce,
277 hashing_threads: s.hashing_threads,
278 sequential_download: false,
279 initial_picker_threshold: s.initial_picker_threshold,
280 whole_pieces_threshold: s.whole_pieces_threshold,
281 snub_timeout_secs: s.snub_timeout_secs,
282 readahead_pieces: s.readahead_pieces,
283 streaming_timeout_escalation: s.streaming_timeout_escalation,
284 max_concurrent_stream_reads: s.max_concurrent_stream_reads,
285 proxy: s.proxy.clone(),
286 anonymous_mode: s.anonymous_mode,
287 share_mode: s.default_share_mode,
288 enable_i2p: s.enable_i2p,
289 allow_i2p_mixed: s.allow_i2p_mixed,
290 ssl_listen_port: s.ssl_listen_port,
291 seed_choking_algorithm: s.seed_choking_algorithm,
292 choking_algorithm: s.choking_algorithm,
293 piece_extent_affinity: s.piece_extent_affinity,
294 suggest_mode: s.suggest_mode,
295 max_suggest_pieces: s.max_suggest_pieces,
296 predictive_piece_announce_ms: s.predictive_piece_announce_ms,
297 mixed_mode_algorithm: s.mixed_mode_algorithm,
298 auto_sequential: s.auto_sequential,
299 storage_mode: s.storage_mode,
300 preallocate_mode: s.preallocate_mode,
301 block_request_timeout_secs: s.block_request_timeout_secs,
302 enable_lsd: s.enable_lsd,
303 force_proxy: s.force_proxy,
304 steal_threshold_ratio: s.steal_threshold_ratio,
305 steal_threshold_endgame: s.steal_threshold_endgame,
306 min_pipeline_depth: s.min_pipeline_depth,
307 max_pipeline_depth: s.max_pipeline_depth,
308 target_buffer_secs: s.target_buffer_secs,
309 peer_read_timeout_secs: s.peer_read_timeout_secs,
310 peer_write_timeout_secs: s.peer_write_timeout_secs,
311 data_contribution_timeout_secs: s.data_contribution_timeout_secs,
312 choke_rotation_max_evictions: s.choke_rotation_max_evictions,
313 max_concurrent_connects: s.max_concurrent_connects,
314 connect_soft_timeout: s.connect_soft_timeout,
315 url_security: crate::url_guard::UrlSecurityConfig::from(s),
316 peer_connect_timeout: s.peer_connect_timeout,
317 peer_dscp: s.peer_dscp,
318 initial_queue_depth: s.initial_queue_depth,
319 max_request_queue_depth: s.max_request_queue_depth,
320 request_queue_time: s.request_queue_time,
321 max_metadata_size: s.max_metadata_size,
322 max_message_size: s.max_message_size,
323 max_piece_length: s.max_piece_length,
324 max_outstanding_requests: s.max_outstanding_requests,
325 max_in_flight_pieces: s.max_in_flight_pieces,
326 use_block_stealing: s.use_block_stealing,
327 steal_stale_piece_secs: s.steal_stale_piece_secs,
328 fixed_pipeline_depth: s.fixed_pipeline_depth,
329 lock_warn_threshold_ms: s.lock_warn_threshold_ms,
330 filesystem_direct_io: s.filesystem_direct_io,
331 }
332 }
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337pub enum TorrentState {
338 FetchingMetadata,
340 Checking,
342 Downloading,
344 Complete,
346 Seeding,
348 Paused,
350 Stopped,
352 Sharing,
354}
355
356#[derive(Debug, Clone, Serialize)]
358pub struct TorrentStats {
359 pub state: TorrentState,
362 pub downloaded: u64,
364 pub uploaded: u64,
366 pub pieces_have: u32,
368 pub pieces_total: u32,
370 pub peers_connected: usize,
372 pub peers_available: usize,
374 pub checking_progress: f32,
376 pub peers_by_source: HashMap<crate::peer_state::PeerSource, usize>,
378
379 pub info_hashes: irontide_core::InfoHashes,
382 pub name: String,
384
385 pub has_metadata: bool,
388 pub is_seeding: bool,
390 pub is_finished: bool,
392 pub is_paused: bool,
394 pub auto_managed: bool,
396 pub sequential_download: bool,
398 pub super_seeding: bool,
400 #[serde(default)]
406 pub user_seed_mode: bool,
407 pub has_incoming: bool,
409 pub need_save_resume: bool,
411 pub moving_storage: bool,
413
414 pub progress: f32,
417 pub progress_ppm: u32,
419 pub total_done: u64,
421 pub total: u64,
423 pub total_wanted_done: u64,
425 pub total_wanted: u64,
427 pub block_size: u32,
429
430 pub total_download: u64,
433 pub total_upload: u64,
435 pub total_payload_download: u64,
437 pub total_payload_upload: u64,
439 pub total_failed_bytes: u64,
441 pub total_redundant_bytes: u64,
443
444 pub all_time_download: u64,
447 pub all_time_upload: u64,
449
450 pub download_rate: u64,
453 pub upload_rate: u64,
455 pub download_payload_rate: u64,
457 pub upload_payload_rate: u64,
459
460 pub num_peers: usize,
463 pub num_seeds: usize,
465 pub num_complete: i32,
467 pub num_incomplete: i32,
469 pub list_seeds: usize,
471 pub list_peers: usize,
473 pub connect_candidates: usize,
475 pub num_connections: usize,
477 pub num_uploads: usize,
479 pub unique_peers_attempted: u64,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub pipeline: Option<crate::peer_states::PeerPipelineSnapshot>,
484 pub choke_rotations: u64,
486 pub piece_steals: u64,
488
489 pub connections_limit: usize,
492 pub uploads_limit: usize,
494
495 pub distributed_full_copies: u32,
498 pub distributed_fraction: u32,
500 pub distributed_copies: f32,
502
503 pub current_tracker: String,
506 pub announcing_to_trackers: bool,
508 pub announcing_to_lsd: bool,
510 pub announcing_to_dht: bool,
512
513 pub added_time: i64,
516 pub completed_time: i64,
518 pub last_seen_complete: i64,
520 pub last_upload: i64,
522 pub last_download: i64,
524
525 pub active_duration: i64,
528 pub finished_duration: i64,
530 pub seeding_duration: i64,
532
533 pub save_path: String,
536
537 pub queue_position: i32,
540
541 pub error: String,
544 pub error_file: i32,
546}
547
548impl Default for TorrentStats {
549 fn default() -> Self {
550 Self {
551 state: TorrentState::Paused,
553 downloaded: 0,
554 uploaded: 0,
555 pieces_have: 0,
556 pieces_total: 0,
557 peers_connected: 0,
558 peers_available: 0,
559 checking_progress: 0.0,
560 peers_by_source: HashMap::new(),
561
562 info_hashes: irontide_core::InfoHashes::v1_only(irontide_core::Id20::from([0u8; 20])),
564 name: String::new(),
565
566 has_metadata: false,
568 is_seeding: false,
569 is_finished: false,
570 is_paused: false,
571 auto_managed: false,
572 sequential_download: false,
573 super_seeding: false,
574 user_seed_mode: false,
575 has_incoming: false,
576 need_save_resume: false,
577 moving_storage: false,
578
579 progress: 0.0,
581 progress_ppm: 0,
582 total_done: 0,
583 total: 0,
584 total_wanted_done: 0,
585 total_wanted: 0,
586 block_size: 16384,
587
588 total_download: 0,
590 total_upload: 0,
591 total_payload_download: 0,
592 total_payload_upload: 0,
593 total_failed_bytes: 0,
594 total_redundant_bytes: 0,
595
596 all_time_download: 0,
598 all_time_upload: 0,
599
600 download_rate: 0,
602 upload_rate: 0,
603 download_payload_rate: 0,
604 upload_payload_rate: 0,
605
606 num_peers: 0,
608 num_seeds: 0,
609 num_complete: -1,
610 num_incomplete: -1,
611 list_seeds: 0,
612 list_peers: 0,
613 connect_candidates: 0,
614 num_connections: 0,
615 num_uploads: 0,
616 unique_peers_attempted: 0,
617 pipeline: None,
618 choke_rotations: 0,
619 piece_steals: 0,
620
621 connections_limit: 0,
623 uploads_limit: 0,
624
625 distributed_full_copies: 0,
627 distributed_fraction: 0,
628 distributed_copies: 0.0,
629
630 current_tracker: String::new(),
632 announcing_to_trackers: false,
633 announcing_to_lsd: false,
634 announcing_to_dht: false,
635
636 added_time: 0,
638 completed_time: 0,
639 last_seen_complete: 0,
640 last_upload: 0,
641 last_download: 0,
642
643 active_duration: 0,
645 finished_duration: 0,
646 seeding_duration: 0,
647
648 save_path: String::new(),
650
651 queue_position: -1,
653
654 error: String::new(),
656 error_file: -1,
657 }
658 }
659}
660
661#[derive(Debug, Clone, Serialize)]
666pub struct TorrentSummary {
667 pub info_hash: String,
669 pub name: String,
671 pub state: TorrentState,
673 pub progress: f64,
675 pub download_rate: u64,
677 pub upload_rate: u64,
679 pub total_size: u64,
681 pub num_peers: usize,
683 pub num_seeds: usize,
685 pub all_time_upload: u64,
687 pub all_time_download: u64,
689 pub added_time: i64,
691 pub user_seed_mode: bool,
693 pub checking_progress: f32,
695}
696
697impl From<&TorrentStats> for TorrentSummary {
698 fn from(s: &TorrentStats) -> Self {
699 Self {
700 info_hash: s.info_hashes.v1.map(|h| h.to_hex()).unwrap_or_default(),
701 name: s.name.clone(),
702 state: s.state,
703 progress: s.progress as f64,
704 download_rate: s.download_rate,
705 upload_rate: s.upload_rate,
706 total_size: s.total,
707 num_peers: s.num_peers,
708 num_seeds: s.num_seeds,
709 all_time_upload: s.all_time_upload,
710 all_time_download: s.all_time_download,
711 added_time: s.added_time,
712 user_seed_mode: s.user_seed_mode,
713 checking_progress: s.checking_progress,
714 }
715 }
716}
717
718#[derive(Debug, Clone)]
721pub(crate) struct BlockEntry {
722 pub index: u32, pub begin: u32, pub length: u32, }
726
727#[derive(Debug)]
729#[allow(dead_code)] pub(crate) enum PeerEvent {
731 Bitfield {
732 peer_addr: SocketAddr,
733 bitfield: Bitfield,
734 },
735 Have {
736 peer_addr: SocketAddr,
737 index: u32,
738 },
739 DontHave {
741 peer_addr: SocketAddr,
742 index: u32,
743 },
744 PieceData {
745 peer_addr: SocketAddr,
746 index: u32,
747 begin: u32,
748 data: Bytes,
749 },
750 PieceBlocksBatch {
753 peer_addr: SocketAddr,
754 blocks: Vec<BlockEntry>,
755 },
756 PeerChoking {
757 peer_addr: SocketAddr,
758 choking: bool,
759 },
760 PeerInterested {
761 peer_addr: SocketAddr,
762 interested: bool,
763 },
764 ExtHandshake {
765 peer_addr: SocketAddr,
766 handshake: ExtHandshake,
767 },
768 MetadataPiece {
769 peer_addr: SocketAddr,
770 piece: u32,
771 data: Bytes,
772 total_size: u64,
773 },
774 MetadataReject {
775 peer_addr: SocketAddr,
776 piece: u32,
777 },
778 PexPeers {
779 new_peers: Vec<SocketAddr>,
780 },
781 TrackersReceived {
782 tracker_urls: Vec<String>,
783 },
784 IncomingRequest {
785 peer_addr: SocketAddr,
786 index: u32,
787 begin: u32,
788 length: u32,
789 },
790 RejectRequest {
791 peer_addr: SocketAddr,
792 index: u32,
793 begin: u32,
794 length: u32,
795 },
796 AllowedFast {
797 peer_addr: SocketAddr,
798 index: u32,
799 },
800 SuggestPiece {
801 peer_addr: SocketAddr,
802 index: u32,
803 },
804 TransportIdentified {
806 peer_addr: SocketAddr,
807 transport: crate::rate_limiter::PeerTransport,
808 },
809 HandshakeComplete {
813 peer_addr: SocketAddr,
814 },
815 Disconnected {
816 peer_addr: SocketAddr,
817 reason: Option<String>,
818 },
819 WebSeedPieceData {
820 url: String,
821 index: u32,
822 data: Bytes,
823 },
824 WebSeedError {
825 url: String,
826 piece: u32,
827 message: String,
828 },
829 HashesReceived {
831 peer_addr: SocketAddr,
832 request: irontide_core::HashRequest,
833 hashes: Vec<irontide_core::Id32>,
834 },
835 HashRequestRejected {
837 peer_addr: SocketAddr,
838 request: irontide_core::HashRequest,
839 },
840 IncomingHashRequest {
842 peer_addr: SocketAddr,
843 request: irontide_core::HashRequest,
844 },
845 HolepunchRendezvous {
847 peer_addr: SocketAddr,
848 target: SocketAddr,
849 },
850 HolepunchConnect {
852 peer_addr: SocketAddr,
853 target: SocketAddr,
854 },
855 HolepunchError {
857 peer_addr: SocketAddr,
858 target: SocketAddr,
859 error_code: u32,
860 },
861 MseRetry {
865 peer_addr: SocketAddr,
866 cmd_tx: tokio::sync::mpsc::Sender<PeerCommand>,
867 },
868 PieceReleased {
870 peer_addr: SocketAddr,
871 piece: u32,
872 },
873}
874
875#[derive(Debug)]
877#[allow(dead_code)] pub(crate) enum PeerCommand {
879 Request {
880 index: u32,
881 begin: u32,
882 length: u32,
883 },
884 Cancel {
885 index: u32,
886 begin: u32,
887 length: u32,
888 },
889 SetChoking(bool),
890 SetInterested(bool),
891 Have(u32),
892 RequestMetadata {
893 piece: u32,
894 },
895 RejectRequest {
896 index: u32,
897 begin: u32,
898 length: u32,
899 },
900 AllowedFast(u32),
901 SendPiece {
902 index: u32,
903 begin: u32,
904 data: Bytes,
905 },
906 SendExtHandshake(irontide_wire::ExtHandshake),
908 SuggestPiece(u32),
910 SendHashRequest(irontide_core::HashRequest),
912 SendHashes {
914 request: irontide_core::HashRequest,
915 hashes: Vec<irontide_core::Id32>,
916 },
917 SendHashReject(irontide_core::HashRequest),
919 SendPex {
921 message: crate::pex::PexMessage,
922 },
923 SendHolepunch(irontide_wire::HolepunchMessage),
925 UpdateNumPieces(u32),
927 StopRequesting,
933 StartRequesting {
936 atomic_states: std::sync::Arc<crate::piece_reservation::AtomicPieceStates>,
937 availability_snapshot: std::sync::Arc<crate::piece_reservation::AvailabilitySnapshot>,
938 piece_notify: std::sync::Arc<tokio::sync::Notify>,
939 disk_handle: Option<crate::disk::DiskHandle>,
940 write_error_tx: tokio::sync::mpsc::Sender<crate::disk::DiskWriteError>,
941 lengths: irontide_core::Lengths,
943 block_maps: Option<std::sync::Arc<crate::piece_reservation::BlockMaps>>,
945 steal_candidates: Option<std::sync::Arc<crate::piece_reservation::StealCandidates>>,
947 piece_write_guards: Option<std::sync::Arc<crate::piece_reservation::PieceWriteGuards>>,
949 },
950 SnapshotUpdate {
952 snapshot: std::sync::Arc<crate::piece_reservation::AvailabilitySnapshot>,
953 },
954 Shutdown,
955}
956
957pub(crate) trait AsyncReadWrite:
962 tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send
963{
964}
965
966impl<T> AsyncReadWrite for T where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send {}
967
968pub(crate) struct BoxedAsyncStream(pub Box<dyn AsyncReadWrite>);
972
973impl std::fmt::Debug for BoxedAsyncStream {
974 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
975 f.write_str("BoxedAsyncStream(..)")
976 }
977}
978
979#[derive(Debug)]
981#[allow(dead_code)] pub(crate) enum TorrentCommand {
983 AddPeers {
984 peers: Vec<SocketAddr>,
985 source: crate::peer_state::PeerSource,
986 },
987 Stats {
988 reply: oneshot::Sender<TorrentStats>,
989 },
990 Pause,
991 Resume,
992 Shutdown,
993 SaveResumeData {
994 reply: oneshot::Sender<crate::Result<irontide_core::FastResumeData>>,
995 },
996 SetFilePriority {
997 index: usize,
998 priority: irontide_core::FilePriority,
999 reply: oneshot::Sender<crate::Result<()>>,
1000 },
1001 FilePriorities {
1002 reply: oneshot::Sender<Vec<irontide_core::FilePriority>>,
1003 },
1004 ForceReannounce,
1005 TrackerList {
1006 reply: oneshot::Sender<Vec<crate::tracker_manager::TrackerInfo>>,
1007 },
1008 Scrape {
1009 reply: oneshot::Sender<Option<(String, irontide_tracker::ScrapeInfo)>>,
1010 },
1011 IncomingPeer {
1013 stream: crate::transport::BoxedStream,
1014 addr: SocketAddr,
1015 },
1016 OpenFile {
1018 file_index: usize,
1019 reply: oneshot::Sender<crate::Result<crate::streaming::FileStreamHandle>>,
1020 },
1021 UpdateExternalIp {
1023 ip: std::net::IpAddr,
1024 },
1025 MoveStorage {
1027 new_path: PathBuf,
1028 reply: oneshot::Sender<crate::Result<()>>,
1029 },
1030 SpawnSslPeer {
1034 addr: SocketAddr,
1035 stream: BoxedAsyncStream,
1036 },
1037 SetDownloadLimit {
1039 bytes_per_sec: u64,
1040 reply: oneshot::Sender<()>,
1041 },
1042 SetUploadLimit {
1044 bytes_per_sec: u64,
1045 reply: oneshot::Sender<()>,
1046 },
1047 DownloadLimit {
1049 reply: oneshot::Sender<u64>,
1050 },
1051 UploadLimit {
1053 reply: oneshot::Sender<u64>,
1054 },
1055 SetSequentialDownload {
1057 enabled: bool,
1058 reply: oneshot::Sender<()>,
1059 },
1060 IsSequentialDownload {
1062 reply: oneshot::Sender<bool>,
1063 },
1064 SetSuperSeeding {
1066 enabled: bool,
1067 reply: oneshot::Sender<()>,
1068 },
1069 IsSuperSeeding {
1071 reply: oneshot::Sender<bool>,
1072 },
1073 SetSeedMode {
1079 enabled: bool,
1080 reply: oneshot::Sender<()>,
1081 },
1082 AddTracker {
1084 url: String,
1085 },
1086 ReplaceTrackers {
1088 urls: Vec<String>,
1089 reply: oneshot::Sender<()>,
1090 },
1091 ForceRecheck {
1093 reply: oneshot::Sender<crate::Result<()>>,
1094 },
1095 RenameFile {
1097 file_index: usize,
1098 new_name: String,
1099 reply: oneshot::Sender<crate::Result<()>>,
1100 },
1101 SetMaxConnections {
1103 limit: usize,
1104 reply: oneshot::Sender<()>,
1105 },
1106 MaxConnections {
1108 reply: oneshot::Sender<usize>,
1109 },
1110 SetMaxUploads {
1112 limit: usize,
1113 reply: oneshot::Sender<()>,
1114 },
1115 MaxUploads {
1117 reply: oneshot::Sender<usize>,
1118 },
1119 GetPeerInfo {
1121 reply: oneshot::Sender<Vec<PeerInfo>>,
1122 },
1123 GetDownloadQueue {
1125 reply: oneshot::Sender<Vec<PartialPieceInfo>>,
1126 },
1127 HavePiece {
1129 index: u32,
1130 reply: oneshot::Sender<bool>,
1131 },
1132 PieceAvailability {
1134 reply: oneshot::Sender<Vec<u32>>,
1135 },
1136 FileProgress {
1138 reply: oneshot::Sender<Vec<u64>>,
1139 },
1140 InfoHashes {
1142 reply: oneshot::Sender<irontide_core::InfoHashes>,
1143 },
1144 TorrentFile {
1146 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
1147 },
1148 TorrentFileV2 {
1150 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV2>>,
1151 },
1152 ForceDhtAnnounce,
1154 ReadPiece {
1156 index: u32,
1157 reply: oneshot::Sender<crate::Result<bytes::Bytes>>,
1158 },
1159 FlushCache {
1161 reply: oneshot::Sender<crate::Result<()>>,
1162 },
1163 ClearError,
1165 FileStatus {
1167 reply: oneshot::Sender<Vec<crate::types::FileStatus>>,
1168 },
1169 Flags {
1171 reply: oneshot::Sender<TorrentFlags>,
1172 },
1173 SetFlags {
1175 flags: TorrentFlags,
1176 reply: oneshot::Sender<()>,
1177 },
1178 UnsetFlags {
1180 flags: TorrentFlags,
1181 reply: oneshot::Sender<()>,
1182 },
1183 ConnectPeer {
1185 addr: SocketAddr,
1186 },
1187 ClearSaveResumeFlag,
1189 RestoreResumeBitmap {
1194 pieces: Vec<u8>,
1196 reply: oneshot::Sender<crate::Result<()>>,
1198 },
1199 PreResolvedMetadata {
1206 info_bytes: Vec<u8>,
1208 peers: Vec<SocketAddr>,
1211 },
1212}
1213
1214#[derive(Debug, Clone, Serialize)]
1216pub struct PeerInfo {
1217 pub addr: SocketAddr,
1219 pub client: String,
1221 pub peer_choking: bool,
1223 pub peer_interested: bool,
1225 pub am_choking: bool,
1227 pub am_interested: bool,
1229 pub download_rate: u64,
1231 pub upload_rate: u64,
1233 pub num_pieces: u32,
1235 pub source: crate::peer_state::PeerSource,
1237 pub supports_fast: bool,
1239 pub upload_only: bool,
1241 pub snubbed: bool,
1243 pub connected_duration_secs: u64,
1245 pub num_pending_requests: usize,
1247 pub num_incoming_requests: usize,
1249}
1250
1251#[derive(Debug, Clone, Serialize)]
1253pub struct PartialPieceInfo {
1254 pub piece_index: u32,
1256 pub blocks_in_piece: u32,
1258 pub blocks_assigned: u32,
1260}
1261
1262#[derive(Debug, Clone, Serialize)]
1264pub struct FileInfo {
1265 pub path: PathBuf,
1267 pub length: u64,
1269}
1270
1271#[derive(Debug, Clone, Serialize)]
1273pub struct TorrentInfo {
1274 pub info_hash: irontide_core::Id20,
1276 pub name: String,
1278 pub total_length: u64,
1280 pub piece_length: u64,
1282 pub num_pieces: u32,
1284 pub files: Vec<FileInfo>,
1286 pub private: bool,
1288}
1289
1290#[derive(Debug, Clone, Serialize, Deserialize)]
1292pub struct SessionStats {
1293 pub active_torrents: usize,
1295 pub total_downloaded: u64,
1297 pub total_uploaded: u64,
1299 pub dht_nodes: usize,
1301}
1302
1303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1305pub enum FileMode {
1306 ReadOnly,
1308 ReadWrite,
1310 Closed,
1312}
1313
1314#[derive(Debug, Clone, Serialize)]
1316pub struct FileStatus {
1317 pub open: bool,
1319 pub mode: FileMode,
1321}
1322
1323bitflags! {
1324 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1329 pub struct TorrentFlags: u32 {
1330 const PAUSED = 0x1;
1332 const AUTO_MANAGED = 0x2;
1334 const SEQUENTIAL_DOWNLOAD = 0x4;
1336 const SUPER_SEEDING = 0x8;
1338 const UPLOAD_ONLY = 0x10;
1340 }
1341}
1342
1343pub type StorageFactory = Box<
1345 dyn Fn(
1346 &irontide_core::TorrentMetaV1,
1347 &std::path::Path,
1348 ) -> std::sync::Arc<dyn irontide_storage::TorrentStorage>
1349 + Send
1350 + Sync,
1351>;
1352
1353#[cfg(test)]
1354mod tests {
1355 use super::*;
1356
1357 #[test]
1358 fn torrent_config_strict_end_game_default() {
1359 let config = TorrentConfig::default();
1360 assert!(config.strict_end_game);
1361 }
1362
1363 #[test]
1364 fn torrent_config_bandwidth_defaults() {
1365 let config = TorrentConfig::default();
1366 assert_eq!(config.upload_rate_limit, 0);
1367 assert_eq!(config.download_rate_limit, 0);
1368 }
1369
1370 #[test]
1371 fn torrent_config_encryption_default() {
1372 let cfg = TorrentConfig::default();
1373 assert_eq!(
1374 cfg.encryption_mode,
1375 irontide_wire::mse::EncryptionMode::Disabled
1376 );
1377 }
1378
1379 #[test]
1380 fn torrent_config_utp_default() {
1381 let cfg = TorrentConfig::default();
1382 assert!(cfg.enable_utp);
1383 }
1384
1385 #[test]
1386 fn torrent_config_web_seed_defaults() {
1387 let cfg = TorrentConfig::default();
1388 assert!(cfg.enable_web_seed);
1389 assert_eq!(cfg.max_web_seeds, 4);
1390 }
1391
1392 #[test]
1393 fn torrent_config_super_seeding_default() {
1394 let cfg = TorrentConfig::default();
1395 assert!(!cfg.super_seeding);
1396 assert!(cfg.upload_only_announce);
1397 }
1398
1399 #[test]
1400 fn torrent_config_picker_defaults() {
1401 let cfg = TorrentConfig::default();
1402 assert!(!cfg.sequential_download);
1403 assert_eq!(cfg.initial_picker_threshold, 4);
1404 assert_eq!(cfg.whole_pieces_threshold, 20);
1405 assert_eq!(cfg.snub_timeout_secs, 15);
1406 assert_eq!(cfg.readahead_pieces, 8);
1407 assert!(cfg.streaming_timeout_escalation);
1408 }
1409
1410 #[test]
1411 fn torrent_stats_has_peers_by_source() {
1412 use crate::peer_state::PeerSource;
1413 use std::collections::HashMap;
1414
1415 let stats = TorrentStats {
1416 state: TorrentState::Downloading,
1417 pieces_total: 10,
1418 ..Default::default()
1419 };
1420 assert!(stats.peers_by_source.is_empty());
1421
1422 let mut map = HashMap::new();
1423 map.insert(PeerSource::Tracker, 5);
1424 map.insert(PeerSource::Dht, 3);
1425 let stats2 = TorrentStats {
1426 peers_by_source: map.clone(),
1427 ..stats
1428 };
1429 assert_eq!(stats2.peers_by_source[&PeerSource::Tracker], 5);
1430 assert_eq!(stats2.peers_by_source[&PeerSource::Dht], 3);
1431 }
1432
1433 #[test]
1434 fn torrent_stats_default_values() {
1435 let stats = TorrentStats::default();
1436
1437 assert_eq!(stats.state, TorrentState::Paused);
1439
1440 assert_eq!(stats.downloaded, 0);
1442 assert_eq!(stats.uploaded, 0);
1443 assert_eq!(stats.pieces_have, 0);
1444 assert_eq!(stats.pieces_total, 0);
1445 assert_eq!(stats.peers_connected, 0);
1446 assert_eq!(stats.peers_available, 0);
1447 assert!((stats.checking_progress - 0.0).abs() < f32::EPSILON);
1448 assert!(stats.peers_by_source.is_empty());
1449
1450 assert_eq!(
1452 stats.info_hashes,
1453 irontide_core::InfoHashes::v1_only(irontide_core::Id20::from([0u8; 20]))
1454 );
1455 assert!(stats.name.is_empty());
1456
1457 assert!(!stats.has_metadata);
1459 assert!(!stats.is_seeding);
1460 assert!(!stats.is_finished);
1461 assert!(!stats.is_paused);
1462 assert!(!stats.auto_managed);
1463 assert!(!stats.sequential_download);
1464 assert!(!stats.super_seeding);
1465 assert!(!stats.has_incoming);
1466 assert!(!stats.need_save_resume);
1467 assert!(!stats.moving_storage);
1468
1469 assert!((stats.progress - 0.0).abs() < f32::EPSILON);
1471 assert_eq!(stats.progress_ppm, 0);
1472 assert_eq!(stats.total_done, 0);
1473 assert_eq!(stats.total, 0);
1474 assert_eq!(stats.total_wanted_done, 0);
1475 assert_eq!(stats.total_wanted, 0);
1476 assert_eq!(stats.block_size, 16384);
1477
1478 assert_eq!(stats.num_complete, -1);
1480 assert_eq!(stats.num_incomplete, -1);
1481 assert_eq!(stats.queue_position, -1);
1482 assert_eq!(stats.error_file, -1);
1483
1484 assert!(stats.current_tracker.is_empty());
1486 assert!(stats.save_path.is_empty());
1487 assert!(stats.error.is_empty());
1488
1489 assert_eq!(stats.download_rate, 0);
1491 assert_eq!(stats.upload_rate, 0);
1492 assert_eq!(stats.download_payload_rate, 0);
1493 assert_eq!(stats.upload_payload_rate, 0);
1494
1495 assert_eq!(stats.distributed_full_copies, 0);
1497 assert_eq!(stats.distributed_fraction, 0);
1498 assert!((stats.distributed_copies - 0.0).abs() < f32::EPSILON);
1499 }
1500
1501 #[test]
1502 fn torrent_stats_seeding_flags() {
1503 let stats = TorrentStats {
1504 state: TorrentState::Seeding,
1505 is_seeding: true,
1506 is_finished: true,
1507 has_metadata: true,
1508 progress: 1.0,
1509 progress_ppm: 1_000_000,
1510 ..Default::default()
1511 };
1512 assert_eq!(stats.state, TorrentState::Seeding);
1513 assert!(stats.is_seeding);
1514 assert!(stats.is_finished);
1515 assert!(stats.has_metadata);
1516 assert!((stats.progress - 1.0).abs() < f32::EPSILON);
1517 assert_eq!(stats.progress_ppm, 1_000_000);
1518 assert!(!stats.is_paused);
1520 assert_eq!(stats.downloaded, 0);
1521 }
1522
1523 #[test]
1524 fn torrent_state_sharing_variant() {
1525 let state = TorrentState::Sharing;
1526 assert_ne!(state, TorrentState::Downloading);
1527 assert_ne!(state, TorrentState::Seeding);
1528 let json = serde_json::to_string(&state).unwrap();
1530 assert_eq!(json, "\"Sharing\"");
1531 let decoded: TorrentState = serde_json::from_str(&json).unwrap();
1532 assert_eq!(decoded, TorrentState::Sharing);
1533 }
1534
1535 #[test]
1536 fn torrent_config_i2p_defaults() {
1537 let cfg = TorrentConfig::default();
1538 assert!(!cfg.enable_i2p);
1539 assert!(!cfg.allow_i2p_mixed);
1540 }
1541
1542 #[test]
1543 fn torrent_config_ssl_listen_port_default() {
1544 let cfg = TorrentConfig::default();
1545 assert_eq!(cfg.ssl_listen_port, 0);
1546 }
1547
1548 #[test]
1549 fn torrent_config_ssl_listen_port_from_settings() {
1550 let mut s = crate::settings::Settings::default();
1551 s.ssl_listen_port = 4433;
1552 let tc = TorrentConfig::from(&s);
1553 assert_eq!(tc.ssl_listen_port, 4433);
1554 }
1555
1556 #[test]
1557 fn torrent_config_choking_defaults() {
1558 let cfg = TorrentConfig::default();
1559 assert_eq!(
1560 cfg.seed_choking_algorithm,
1561 SeedChokingAlgorithm::FastestUpload
1562 );
1563 assert_eq!(cfg.choking_algorithm, ChokingAlgorithm::FixedSlots);
1564 }
1565
1566 #[test]
1567 fn torrent_config_m44_defaults() {
1568 let cfg = TorrentConfig::default();
1569 assert!(cfg.piece_extent_affinity);
1570 assert!(!cfg.suggest_mode);
1571 assert_eq!(cfg.max_suggest_pieces, 10);
1572 assert_eq!(cfg.predictive_piece_announce_ms, 0);
1573 }
1574
1575 #[test]
1576 fn torrent_config_from_settings_choking() {
1577 let mut s = crate::settings::Settings::default();
1578 s.seed_choking_algorithm = SeedChokingAlgorithm::RoundRobin;
1579 s.choking_algorithm = ChokingAlgorithm::RateBased;
1580 let cfg = TorrentConfig::from(&s);
1581 assert_eq!(cfg.seed_choking_algorithm, SeedChokingAlgorithm::RoundRobin);
1582 assert_eq!(cfg.choking_algorithm, ChokingAlgorithm::RateBased);
1583 }
1584
1585 #[test]
1586 fn torrent_config_holepunch_default() {
1587 let cfg = TorrentConfig::default();
1588 assert!(cfg.enable_holepunch);
1589
1590 let s = crate::settings::Settings::default();
1592 let tc = TorrentConfig::from(&s);
1593 assert!(tc.enable_holepunch);
1594
1595 let mut s2 = crate::settings::Settings::default();
1597 s2.enable_holepunch = false;
1598 let tc2 = TorrentConfig::from(&s2);
1599 assert!(!tc2.enable_holepunch);
1600 }
1601
1602 #[test]
1603 fn torrent_config_url_security_default() {
1604 let cfg = TorrentConfig::default();
1605 assert!(cfg.url_security.ssrf_mitigation);
1606 assert!(!cfg.url_security.allow_idna);
1607 assert!(cfg.url_security.validate_https_trackers);
1608 }
1609
1610 #[test]
1611 fn torrent_config_url_security_from_settings() {
1612 let mut s = crate::settings::Settings::default();
1613 s.ssrf_mitigation = false;
1614 s.allow_idna = true;
1615 s.validate_https_trackers = false;
1616 let cfg = TorrentConfig::from(&s);
1617 assert!(!cfg.url_security.ssrf_mitigation);
1618 assert!(cfg.url_security.allow_idna);
1619 assert!(!cfg.url_security.validate_https_trackers);
1620 }
1621
1622 #[test]
1623 fn torrent_config_peer_dscp_default() {
1624 let cfg = TorrentConfig::default();
1625 assert_eq!(cfg.peer_dscp, 0x08);
1626 }
1627
1628 #[test]
1629 fn torrent_config_peer_dscp_from_settings() {
1630 let mut s = crate::settings::Settings::default();
1631 s.peer_dscp = 0x2E;
1632 let cfg = TorrentConfig::from(&s);
1633 assert_eq!(cfg.peer_dscp, 0x2E);
1634 }
1635
1636 #[test]
1639 fn summary_from_stats() {
1640 let v1_hash =
1641 irontide_core::Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
1642 let mut stats = TorrentStats::default();
1643 stats.info_hashes = irontide_core::InfoHashes::v1_only(v1_hash);
1644 stats.name = "test torrent".to_string();
1645 stats.state = TorrentState::Downloading;
1646 stats.progress = 0.75;
1647 stats.download_rate = 1_000_000;
1648 stats.upload_rate = 500_000;
1649 stats.total = 100_000_000;
1650 stats.num_peers = 42;
1651 stats.added_time = 1710900000;
1652
1653 let summary = super::TorrentSummary::from(&stats);
1654 assert_eq!(
1655 summary.info_hash,
1656 "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
1657 );
1658 assert_eq!(summary.name, "test torrent");
1659 assert_eq!(summary.state, TorrentState::Downloading);
1660 assert!((summary.progress - 0.75).abs() < f64::EPSILON);
1661 assert_eq!(summary.download_rate, 1_000_000);
1662 assert_eq!(summary.upload_rate, 500_000);
1663 assert_eq!(summary.total_size, 100_000_000);
1664 assert_eq!(summary.num_peers, 42);
1665 assert_eq!(summary.added_time, 1710900000);
1666 }
1667
1668 #[test]
1669 fn summary_from_stats_v2_only() {
1670 let v2_hash = irontide_core::Id32::from_hex(
1671 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1672 )
1673 .unwrap();
1674 let mut stats = TorrentStats::default();
1675 stats.info_hashes = irontide_core::InfoHashes::v2_only(v2_hash);
1676 stats.name = "v2 torrent".to_string();
1677
1678 let summary = super::TorrentSummary::from(&stats);
1679 assert_eq!(summary.info_hash, "");
1681 assert_eq!(summary.name, "v2 torrent");
1682 }
1683
1684 #[test]
1685 fn stats_serializable() {
1686 let stats = TorrentStats::default();
1687 let json = serde_json::to_string(&stats).expect("TorrentStats should serialize to JSON");
1688 assert!(json.contains("\"state\""));
1689 assert!(json.contains("\"info_hashes\""));
1690 assert!(json.contains("\"download_rate\""));
1691 }
1692
1693 #[test]
1694 fn info_hashes_serializable() {
1695 let v1 = irontide_core::Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
1696 let ih = irontide_core::InfoHashes::v1_only(v1);
1697 let json = serde_json::to_string(&ih).expect("InfoHashes should serialize to JSON");
1698 assert!(json.contains("\"v1\""));
1700 assert!(json.contains("\"v2\":null"));
1701 }
1702
1703 #[test]
1704 fn summary_serializable() {
1705 let mut stats = TorrentStats::default();
1706 stats.name = "serialize test".to_string();
1707 stats.state = TorrentState::Seeding;
1708 stats.progress = 1.0;
1709 let summary = super::TorrentSummary::from(&stats);
1710 let json =
1711 serde_json::to_string(&summary).expect("TorrentSummary should serialize to JSON");
1712 assert!(json.contains("\"name\":\"serialize test\""));
1713 assert!(json.contains("\"state\":\"Seeding\""));
1714 assert!(json.contains("\"progress\":1.0"));
1715 }
1716
1717 #[test]
1718 fn test_torrent_summary_includes_seeds_and_totals() {
1719 let mut stats = TorrentStats::default();
1720 stats.num_seeds = 7;
1721 stats.all_time_upload = 1_500_000;
1722 stats.all_time_download = 3_000_000;
1723
1724 let summary = super::TorrentSummary::from(&stats);
1725 assert_eq!(summary.num_seeds, 7);
1726 assert_eq!(summary.all_time_upload, 1_500_000);
1727 assert_eq!(summary.all_time_download, 3_000_000);
1728 }
1729}