1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4use tracing::{debug, info, warn};
5
6use crate::constants;
7use crate::engine::bt_choke_manager::{
8 add_peer_to_tracking, check_snubbed_peers, handle_snubbed_peer, on_data_received_from_peer,
9 on_peer_choke, on_peer_unchoke, on_piece_received, select_best_peer_for_request,
10};
11use crate::engine::bt_post_download_handler::HookManager;
12use crate::engine::bt_progress_info_file::BtProgressManager;
13use crate::engine::bt_tracker_comm::announce_to_public_tracker;
14use crate::engine::choking_algorithm::{ChokingAlgorithm, ChokingConfig};
15use crate::engine::http_tracker_client::TrackerState;
16use crate::engine::lpd_manager::LpdManager;
17use crate::engine::multi_file_layout::MultiFileLayout;
18use crate::error::{Aria2Error, FatalError, Result};
19use crate::filesystem::file_lock::DownloadPathLock;
20use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
21
22pub use crate::engine::bt_message_handler::{
23 BLOCK_REQUEST_TIMEOUT_SECS, BLOCK_SIZE, MAX_BLOCK_READ_MESSAGES, MAX_RETRIES,
24};
25pub use crate::engine::bt_peer_interaction::{
26 MAX_UNCHOKE_WAIT_ATTEMPTS, PEER_CONNECTION_DELAY_MS, PEER_MESSAGE_TIMEOUT_SECS,
27};
28pub use crate::engine::bt_piece_selector::ENDGAME_THRESHOLD;
29
30pub(crate) const PUBLIC_TRACKER_PEER_THRESHOLD: usize = 15;
31pub(crate) const MAX_PUBLIC_TRACKERS_TO_TRY: usize = 10;
32
33pub struct BtDownloadCommand {
34 pub(crate) group: Arc<tokio::sync::RwLock<RequestGroup>>,
35 pub(crate) output_path: std::path::PathBuf,
36 pub(crate) started: bool,
37 pub(crate) completed_bytes: u64,
38 pub(crate) torrent_data: Vec<u8>,
39 pub(crate) seed_enabled: bool,
40 pub(crate) seed_time: Option<std::time::Duration>,
41 pub(crate) seed_ratio: Option<f64>,
42 pub(crate) total_uploaded: u64,
43 pub(crate) udp_client: Option<crate::engine::udp_tracker_client::SharedUdpClient>,
44 pub(crate) dht_engine:
45 Option<std::sync::Arc<aria2_protocol::bittorrent::dht::engine::DhtEngine>>,
46 pub(crate) public_trackers:
47 Option<std::sync::Arc<aria2_protocol::bittorrent::tracker::public_list::PublicTrackerList>>,
48 pub choking_algo: Option<ChokingAlgorithm>,
49 pub multi_file_layout: Option<MultiFileLayout>,
50
51 pub(crate) progress_manager: Option<BtProgressManager>,
54 pub(crate) progress_save_interval: Duration,
56 pub(crate) lpd_manager: Option<Arc<LpdManager>>,
58 pub(crate) hook_manager: Option<Arc<HookManager>>,
60
61 pub(crate) pex_known_peers: Vec<aria2_protocol::bittorrent::peer::connection::PeerAddr>,
64 pub(crate) pex_last_send_time: Option<Instant>,
66 pub(crate) pex_send_interval: Duration,
68
69 pub(crate) endgame_state: super::bt_download_execute::EndgameState,
72
73 #[allow(dead_code)]
77 pub(crate) allowed_fast_sent_peers: HashMap<usize, HashSet<u32>>,
78
79 pub(crate) suggest_sent_counts: HashMap<usize, usize>,
81
82 #[allow(dead_code)]
85 pub(crate) tracker_state: TrackerState,
86
87 pub(crate) web_seed_urls: Vec<String>,
90 pub(crate) web_seed_manager: Option<crate::engine::bt_web_seed::WebSeedManager>,
92
93 pub download_path_lock: Option<DownloadPathLock>,
97
98 pub(crate) seed_manager: Option<super::bt_seed_manager::BtSeedManager>,
101
102 pub(crate) is_private: bool,
106}
107
108impl BtDownloadCommand {
109 pub fn new(
110 gid: GroupId,
111 torrent_bytes: &[u8],
112 options: &DownloadOptions,
113 output_dir: Option<&str>,
114 ) -> Result<Self> {
115 let meta = aria2_protocol::bittorrent::torrent::parser::TorrentMeta::parse(torrent_bytes)
116 .map_err(|e| {
117 Aria2Error::Fatal(FatalError::Config(format!("Torrent parse failed: {}", e)))
118 })?;
119
120 let is_private = meta.is_private();
124 if is_private {
125 info!(
126 "[BT] Private torrent detected (BEP 0027): DHT/PEX/LPD and public trackers will be disabled"
127 );
128 }
129
130 let dir = output_dir
131 .map(|d| d.to_string())
132 .or_else(|| options.dir.clone())
133 .unwrap_or_else(|| ".".to_string());
134
135 let filename = meta.info.name.clone();
136 let path = std::path::PathBuf::from(&dir).join(&filename);
137
138 let group = RequestGroup::new(
139 gid,
140 vec![format!("bt://{}", meta.info_hash.as_hex())],
141 options.clone(),
142 );
143
144 group.set_bt_metadata(
146 meta.num_pieces() as u32,
147 meta.info.piece_length,
148 meta.info_hash.as_hex(),
149 );
150
151 let seed_time = options.seed_time.and_then(|t| {
152 if t == 0 {
153 None
154 } else {
155 Some(std::time::Duration::from_secs(t))
156 }
157 });
158 let seed_ratio = options.seed_ratio.filter(|&r| r > 0.0);
159
160 info!(
161 "BtDownloadCommand created: {} -> {} ({} bytes, {} pieces) seed={:?} ratio={:?}",
162 meta.info.name,
163 path.display(),
164 meta.total_size(),
165 meta.num_pieces(),
166 seed_time,
167 seed_ratio
168 );
169
170 let choking_algo = if options.bt_max_upload_slots.is_some()
171 || options.bt_optimistic_unchoke_interval.is_some()
172 || options.bt_snubbed_timeout.is_some()
173 {
174 let config = ChokingConfig {
175 max_upload_slots: options
176 .bt_max_upload_slots
177 .unwrap_or(constants::BT_DEFAULT_MAX_UPLOAD_SLOTS as u32)
178 as usize,
179 optimistic_unchoke_interval_secs: options
180 .bt_optimistic_unchoke_interval
181 .unwrap_or(constants::BT_OPTIMISTIC_UNCHOKE_INTERVAL_SECS),
182 snubbed_timeout_secs: options
183 .bt_snubbed_timeout
184 .unwrap_or(constants::BT_SNUBBED_TIMEOUT_SECS),
185 choke_rotation_interval_secs: constants::BT_CHOKE_ROTATION_INTERVAL_SECS,
186 };
187 Some(ChokingAlgorithm::new(config))
188 } else {
189 None
190 };
191
192 let multi_file_layout = if !meta.is_single_file() {
193 let layout_base_dir = std::path::PathBuf::from(&dir);
194 match MultiFileLayout::from_info_dict(&meta.info, &layout_base_dir) {
195 Ok(layout) => Some(layout),
196 Err(e) => {
197 return Err(Aria2Error::Fatal(FatalError::Config(format!(
198 "MultiFileLayout creation failed: {}",
199 e
200 ))));
201 }
202 }
203 } else {
204 None
205 };
206
207 let effective_output_path = if multi_file_layout.is_some() {
208 std::path::PathBuf::from(&dir)
209 } else {
210 path.clone()
211 };
212
213 info!(
214 "BtDownloadCommand created: {} -> {} ({} bytes, {} pieces) seed={:?} ratio={:?} multi_file={}",
215 meta.info.name,
216 effective_output_path.display(),
217 meta.total_size(),
218 meta.num_pieces(),
219 seed_time,
220 seed_ratio,
221 multi_file_layout.is_some()
222 );
223
224 let download_path_lock =
233 match DownloadPathLock::acquire_for_download(std::path::Path::new(&dir)) {
234 Ok(lock) => Some(lock),
235 Err(e) => {
236 warn!(
237 "Failed to acquire download path lock: {}. Proceeding without lock.",
238 e
239 );
240 None
241 }
242 };
243
244 Ok(Self {
245 group: Arc::new(tokio::sync::RwLock::new(group)),
246 output_path: effective_output_path,
247 started: false,
248 completed_bytes: 0,
249 torrent_data: torrent_bytes.to_vec(),
250 seed_enabled: options.seed_time.unwrap_or(0) > 0
251 || options.seed_ratio.unwrap_or(0.0) > 0.0,
252 seed_time,
253 seed_ratio,
254 total_uploaded: 0,
255 udp_client: None,
256 dht_engine: None,
257 public_trackers: None,
258 choking_algo,
259 multi_file_layout,
260
261 progress_manager: None,
263 progress_save_interval: Duration::from_secs(60),
264 lpd_manager: None,
265 hook_manager: None,
266
267 pex_known_peers: Vec::new(),
269 pex_last_send_time: None,
270 pex_send_interval: Duration::from_secs(60),
271
272 allowed_fast_sent_peers: HashMap::new(),
274 suggest_sent_counts: HashMap::new(),
275
276 endgame_state: super::bt_download_execute::EndgameState::new(),
278
279 tracker_state: TrackerState::new(),
281
282 web_seed_urls: meta.web_seeds.clone(),
284 web_seed_manager: None,
286
287 download_path_lock,
289
290 seed_manager: None,
292
293 is_private,
295 })
296 }
297
298 pub async fn group(&self) -> tokio::sync::RwLockReadGuard<'_, RequestGroup> {
299 self.group.read().await
300 }
301
302 pub fn on_peer_choke(&mut self, peer_idx: usize) {
303 on_peer_choke(&mut self.choking_algo, peer_idx);
304 }
305
306 pub fn on_peer_unchoke(&mut self, peer_idx: usize) {
307 on_peer_unchoke(&mut self.choking_algo, peer_idx);
308 }
309
310 pub fn on_data_received_from_peer(&mut self, peer_idx: usize, bytes: u64) {
311 on_data_received_from_peer(&mut self.choking_algo, peer_idx, bytes);
312 }
313
314 pub fn check_snubbed_peers(&mut self) -> Vec<usize> {
315 check_snubbed_peers(&mut self.choking_algo)
316 }
317
318 pub fn add_peer_to_tracking(&mut self, peer_id: [u8; 8], addr: std::net::SocketAddr) -> usize {
319 add_peer_to_tracking(&mut self.choking_algo, peer_id, addr)
320 }
321
322 pub fn select_best_peer_for_request(&self) -> Option<usize> {
323 select_best_peer_for_request(&self.choking_algo)
324 }
325
326 pub async fn handle_snubbed_peer(&mut self, peer_idx: usize) -> Result<()> {
327 handle_snubbed_peer(&mut self.choking_algo, peer_idx)
328 .await
329 .map_err(|_| {
330 Aria2Error::Fatal(FatalError::Config(format!(
331 "Failed to handle snubbed peer {}",
332 peer_idx
333 )))
334 })
335 }
336
337 pub fn on_piece_received(&mut self, peer_idx: usize, bytes: u64) {
338 on_piece_received(&mut self.choking_algo, peer_idx, bytes);
339 }
340
341 pub fn mark_peer_snubbed(&mut self, peer_idx: usize) {
346 if let Some(algo) = &mut self.choking_algo {
347 algo.mark_peer_snubbed(peer_idx);
348 }
349 }
350
351 pub fn is_explicitly_snubbed(&self, peer_idx: usize) -> bool {
353 self.choking_algo
354 .as_ref()
355 .map(|a| a.is_explicitly_snubbed(peer_idx))
356 .unwrap_or(false)
357 }
358
359 pub async fn announce_to_public_tracker(
360 tracker_url: &str,
361 info_hash: &[u8; 20],
362 peer_id: &[u8; 20],
363 total_size: u64,
364 ) -> std::result::Result<Vec<(String, u16)>, String> {
365 announce_to_public_tracker(tracker_url, info_hash, peer_id, total_size).await
366 }
367
368 pub async fn write_piece_to_multi_files(
370 layout: &MultiFileLayout,
371 piece_idx: u32,
372 piece_data: &[u8],
373 piece_length: u32,
374 ) -> Result<()> {
375 crate::engine::bt_piece_downloader::write_piece_to_multi_files(
376 layout,
377 piece_idx,
378 piece_data,
379 piece_length,
380 )
381 .await
382 }
383
384 pub async fn write_piece_to_multi_files_coalesced(
389 layout: &MultiFileLayout,
390 piece_idx: u32,
391 piece_data: &[u8],
392 piece_length: u32,
393 ) -> Result<()> {
394 crate::engine::bt_piece_downloader::write_piece_to_multi_files_coalesced(
395 layout,
396 piece_idx,
397 piece_data,
398 piece_length,
399 )
400 .await
401 }
402
403 pub fn is_multi_file(&self) -> bool {
404 self.multi_file_layout
405 .as_ref()
406 .is_some_and(|l| l.is_multi_file())
407 }
408
409 pub fn get_multi_file_layout(&self) -> Option<&MultiFileLayout> {
410 self.multi_file_layout.as_ref()
411 }
412
413 pub fn set_progress_manager(&mut self, manager: BtProgressManager) {
439 info!("BT progress manager enabled");
440 self.progress_manager = Some(manager);
441 }
442
443 pub fn set_progress_save_interval(&mut self, interval_secs: u64) {
449 self.progress_save_interval = Duration::from_secs(interval_secs);
450 info!(interval_secs, "Progress save interval updated");
451 }
452
453 pub fn set_lpd_manager(&mut self, manager: Arc<LpdManager>) {
462 info!("LPD manager enabled for local peer discovery");
463 self.lpd_manager = Some(manager);
464 }
465
466 pub fn set_hook_manager(&mut self, manager: Arc<HookManager>) {
491 info!(
492 hook_count = manager.hook_count(),
493 "Hook manager enabled with {} hooks",
494 manager.hook_count()
495 );
496 self.hook_manager = Some(manager);
497 }
498
499 pub fn get_progress_manager(&self) -> Option<&BtProgressManager> {
501 self.progress_manager.as_ref()
502 }
503
504 pub fn get_lpd_manager(&self) -> Option<&Arc<LpdManager>> {
506 self.lpd_manager.as_ref()
507 }
508
509 pub fn get_hook_manager(&self) -> Option<&Arc<HookManager>> {
511 self.hook_manager.as_ref()
512 }
513
514 pub fn add_pex_peer(
518 &mut self,
519 peer_addr: aria2_protocol::bittorrent::peer::connection::PeerAddr,
520 ) {
521 if !self.pex_known_peers.contains(&peer_addr) {
522 debug!(addr = %format!("{}:{}", peer_addr.ip, peer_addr.port), "Adding peer to PEX known list");
523 self.pex_known_peers.push(peer_addr);
524 }
525 }
526
527 pub fn set_pex_known_peers(
529 &mut self,
530 peers: Vec<aria2_protocol::bittorrent::peer::connection::PeerAddr>,
531 ) {
532 self.pex_known_peers = peers;
533 info!(
534 count = self.pex_known_peers.len(),
535 "PEX known peers updated"
536 );
537 }
538
539 pub fn get_pex_known_peers(&self) -> &[aria2_protocol::bittorrent::peer::connection::PeerAddr] {
541 &self.pex_known_peers
542 }
543
544 pub fn set_pex_send_interval(&mut self, interval_secs: u64) {
546 self.pex_send_interval = Duration::from_secs(interval_secs);
547 info!(interval_secs, "PEX send interval updated");
548 }
549
550 pub fn should_send_pex(&self) -> bool {
552 match self.pex_last_send_time {
553 Some(last) => last.elapsed() >= self.pex_send_interval,
554 None => true,
555 }
556 }
557
558 pub fn update_pex_last_send(&mut self) {
560 self.pex_last_send_time = Some(Instant::now());
561 }
562
563 pub fn endgame_state_mut(&mut self) -> &mut super::bt_download_execute::EndgameState {
567 &mut self.endgame_state
568 }
569
570 pub fn endgame_state(&self) -> &super::bt_download_execute::EndgameState {
572 &self.endgame_state
573 }
574
575 #[allow(clippy::result_unit_err)]
596 pub fn record_bad_piece_for_peer(
597 &mut self,
598 peer_idx: usize,
599 piece_index: u32,
600 ) -> std::result::Result<bool, ()> {
601 use crate::engine::peer_stats::BAD_DATA_THRESHOLD;
602
603 if let Some(ref mut algo) = self.choking_algo
604 && let Some(peer) = algo.get_peer_mut(peer_idx)
605 {
606 let should_ban = peer.increment_bad_data();
607
608 warn!(
609 "[BT] Peer {} sent invalid data for piece {} (bad count: {}/{})",
610 peer_idx, piece_index, peer.bad_data_count, BAD_DATA_THRESHOLD
611 );
612
613 if should_ban {
614 let reason = format!(
615 "Too many invalid pieces ({} >= {})",
616 peer.bad_data_count, BAD_DATA_THRESHOLD
617 );
618 warn!("[BT] BANNING peer {}: {}", peer_idx, reason);
619 peer.ban_peer(reason);
620 return Ok(true); }
622
623 return Ok(false); }
625
626 Err(()) }
628
629 pub fn record_valid_piece_for_peer(&mut self, peer_idx: usize) {
638 if let Some(ref mut algo) = self.choking_algo
639 && let Some(peer) = algo.get_peer_mut(peer_idx)
640 {
641 peer.decrement_bad_data();
642 debug!(
643 "[BT] Peer {} sent valid piece, bad count decremented to {}",
644 peer_idx, peer.bad_data_count
645 );
646 }
647 }
648
649 pub fn is_peer_banned(&self, peer_idx: usize) -> bool {
660 self.choking_algo
661 .as_ref()
662 .and_then(|algo| algo.get_peer(peer_idx))
663 .map(|p| p.is_banned)
664 .unwrap_or(true) }
666
667 pub fn get_peer_stats(&self, peer_idx: usize) -> Option<&crate::engine::peer_stats::PeerStats> {
671 self.choking_algo.as_ref()?.get_peer(peer_idx)
672 }
673
674 pub fn init_web_seed_manager(&mut self, piece_length: u32, total_length: u64) {
686 if !self.web_seed_urls.is_empty() && self.web_seed_manager.is_none() {
687 info!(
688 count = self.web_seed_urls.len(),
689 "Initializing web seed manager with {} URL(s)",
690 self.web_seed_urls.len()
691 );
692 self.web_seed_manager = Some(crate::engine::bt_web_seed::WebSeedManager::new(
693 self.web_seed_urls.clone(),
694 piece_length,
695 total_length,
696 ));
697 }
698 }
699
700 pub fn get_web_seed_manager(&self) -> Option<&crate::engine::bt_web_seed::WebSeedManager> {
702 self.web_seed_manager.as_ref()
703 }
704
705 pub fn get_web_seed_manager_mut(
707 &mut self,
708 ) -> Option<&mut crate::engine::bt_web_seed::WebSeedManager> {
709 self.web_seed_manager.as_mut()
710 }
711
712 pub fn has_web_seeds(&self) -> bool {
714 !self.web_seed_urls.is_empty()
715 }
716
717 pub fn web_seed_stats(&self) -> Option<&crate::engine::bt_web_seed::WebSeedStats> {
719 self.web_seed_manager.as_ref().map(|m| m.stats())
720 }
721
722 pub fn check_and_start_seeding(
743 &mut self,
744 piece_picker: &aria2_protocol::bittorrent::piece::picker::PiecePicker,
745 meta: &aria2_protocol::bittorrent::torrent::parser::TorrentMeta,
746 connections: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection>,
747 piece_provider: std::sync::Arc<dyn crate::engine::bt_upload_session::PieceDataProvider>,
748 ) -> Result<bool> {
749 if !piece_picker.is_complete() {
751 debug!("Download not complete, skipping seeding");
752 return Ok(false);
753 }
754
755 if !self.seed_enabled {
757 info!("Seeding disabled, not starting seed manager");
758 return Ok(false);
759 }
760
761 use crate::engine::bt_seed_manager::{BtSeedManager, SeedExitCondition};
763 use crate::engine::bt_upload_session::BtSeedingConfig;
764
765 let seed_ratio = self.seed_ratio.unwrap_or(0.0);
766 let seed_time = self.seed_time.map(|d| d.as_secs());
767
768 let exit_condition =
769 SeedExitCondition::with_time_and_ratio(seed_time.unwrap_or(0), seed_ratio);
770
771 let config = BtSeedingConfig {
772 max_upload_bytes_per_sec: None, max_peers_to_unchoke: 4,
774 optimistic_unchoke_interval_secs: 30,
775 };
776
777 let total_downloaded = meta.total_size();
778
779 let seed_manager = BtSeedManager::new_with_info_hash(
780 meta.info_hash.bytes,
781 connections,
782 piece_provider,
783 config,
784 exit_condition,
785 total_downloaded,
786 );
787
788 self.seed_manager = Some(seed_manager);
789
790 info!(
791 "Seeding started: ratio={}, time={:?}, info_hash={}",
792 seed_ratio,
793 self.seed_time,
794 meta.info_hash.as_hex()
795 );
796
797 Ok(true)
798 }
799
800 pub fn get_seed_manager(&self) -> Option<&super::bt_seed_manager::BtSeedManager> {
802 self.seed_manager.as_ref()
803 }
804
805 pub fn get_seed_manager_mut(&mut self) -> Option<&mut super::bt_seed_manager::BtSeedManager> {
807 self.seed_manager.as_mut()
808 }
809
810 pub fn is_seeding(&self) -> bool {
812 self.seed_manager.is_some()
813 }
814
815 pub fn get_seed_stats(&self) -> Option<SeedStats> {
819 self.seed_manager.as_ref().map(|mgr| {
820 let (total_uploaded, upload_speed) = mgr.get_upload_stats();
821 let total_downloaded = mgr.total_downloaded();
822 let ratio = if total_downloaded > 0 {
823 total_uploaded as f64 / total_downloaded as f64
824 } else {
825 0.0
826 };
827
828 SeedStats {
829 total_uploaded,
830 upload_speed,
831 ratio,
832 elapsed: mgr.seeding_duration(),
833 }
834 })
835 }
836}
837
838#[derive(Debug, Clone)]
840pub struct SeedStats {
841 pub total_uploaded: u64,
843 pub upload_speed: u64,
845 pub ratio: f64,
847 pub elapsed: std::time::Duration,
849}