1use std::collections::HashSet;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::{Duration, Instant};
18use tracing::{debug, warn};
19
20#[derive(Debug, Default)]
24pub struct WebSeedStats {
25 pub total_bytes: AtomicU64,
27 pub start_time: Option<Instant>,
29 pub current_second_bytes: AtomicU64,
31 pub current_second_start: Option<Instant>,
33}
34
35impl WebSeedStats {
36 pub fn new() -> Self {
38 Self {
39 total_bytes: AtomicU64::new(0),
40 start_time: Some(Instant::now()),
41 current_second_bytes: AtomicU64::new(0),
42 current_second_start: Some(Instant::now()),
43 }
44 }
45
46 pub fn record_bytes(&self, bytes: u64) {
48 self.total_bytes.fetch_add(bytes, Ordering::Relaxed);
49 self.current_second_bytes
50 .fetch_add(bytes, Ordering::Relaxed);
51 }
52
53 pub fn total_bytes_downloaded(&self) -> u64 {
55 self.total_bytes.load(Ordering::Relaxed)
56 }
57
58 pub fn average_speed(&self) -> u64 {
60 if let Some(start) = self.start_time {
61 let elapsed = start.elapsed().as_secs();
62 return self
63 .total_bytes
64 .load(Ordering::Relaxed)
65 .checked_div(elapsed)
66 .unwrap_or(0);
67 }
68 0
69 }
70
71 pub fn current_speed(&self) -> u64 {
73 if let Some(start) = self.current_second_start {
74 let elapsed = start.elapsed();
75 if elapsed >= Duration::from_secs(1) {
76 let bytes = self.current_second_bytes.swap(0, Ordering::Relaxed);
78 let secs = elapsed.as_secs_f64();
79 return (bytes as f64 / secs) as u64;
80 }
81 let bytes = self.current_second_bytes.load(Ordering::Relaxed);
83 let secs = elapsed.as_secs_f64();
84 if secs > 0.0 {
85 return (bytes as f64 / secs) as u64;
86 }
87 }
88 0
89 }
90}
91
92pub struct WebSeedClient {
97 base_url: String,
99 client: reqwest::Client,
101 active_requests: Arc<std::sync::Mutex<HashSet<u32>>>,
105 stats: Arc<WebSeedStats>,
107}
108
109impl WebSeedClient {
110 pub fn new(base_url: &str) -> Self {
123 debug!(url = base_url, "Creating WebSeedClient");
124
125 let client = reqwest::Client::builder()
127 .timeout(std::time::Duration::from_secs(120))
128 .connect_timeout(std::time::Duration::from_secs(10))
129 .pool_max_idle_per_host(4)
130 .build()
131 .unwrap_or_else(|_| reqwest::Client::new());
132
133 Self {
134 base_url: base_url.to_string(),
135 client,
136 active_requests: Arc::new(std::sync::Mutex::new(HashSet::new())),
137 stats: Arc::new(WebSeedStats::new()),
138 }
139 }
140
141 pub fn with_shared_stats(base_url: &str, stats: Arc<WebSeedStats>) -> Self {
143 debug!(url = base_url, "Creating WebSeedClient with shared stats");
144
145 let client = reqwest::Client::builder()
146 .timeout(std::time::Duration::from_secs(120))
147 .connect_timeout(std::time::Duration::from_secs(10))
148 .pool_max_idle_per_host(4)
149 .build()
150 .unwrap_or_else(|_| reqwest::Client::new());
151
152 Self {
153 base_url: base_url.to_string(),
154 client,
155 active_requests: Arc::new(std::sync::Mutex::new(HashSet::new())),
156 stats,
157 }
158 }
159
160 pub fn can_request(&self, piece_index: u32) -> bool {
162 let active = self
163 .active_requests
164 .lock()
165 .unwrap_or_else(|e| e.into_inner());
166 !active.contains(&piece_index)
167 }
168
169 pub fn mark_requesting(&self, piece_index: u32) {
171 let mut active = self
172 .active_requests
173 .lock()
174 .unwrap_or_else(|e| e.into_inner());
175 active.insert(piece_index);
176 }
177
178 pub fn clear_request(&self, piece_index: u32) {
180 let mut active = self
181 .active_requests
182 .lock()
183 .unwrap_or_else(|e| e.into_inner());
184 active.remove(&piece_index);
185 }
186
187 pub fn active_request_count(&self) -> usize {
189 let active = self
190 .active_requests
191 .lock()
192 .unwrap_or_else(|e| e.into_inner());
193 active.len()
194 }
195
196 pub fn stats(&self) -> &WebSeedStats {
198 &self.stats
199 }
200
201 pub async fn download_piece(
218 &self,
219 piece_index: u32,
220 _piece_length: u64,
221 piece_offset: u64,
222 length: u64,
223 ) -> Result<Vec<u8>, String> {
224 let range_end = piece_offset + length.saturating_sub(1);
225 let range_header = format!("bytes={}-{}", piece_offset, range_end);
226
227 debug!(
228 piece_index,
229 offset = piece_offset,
230 length,
231 url = self.base_url,
232 range = %range_header,
233 "Web-seed HTTP Range request"
234 );
235
236 let response = self
237 .client
238 .get(&self.base_url)
239 .header("Range", &range_header)
240 .header("User-Agent", "aria2-rust/1.0")
241 .send()
242 .await
243 .map_err(|e| format!("HTTP request failed: {}", e))?;
244
245 let status = response.status().as_u16();
246
247 if status != 200 && status != 206 {
249 return Err(format!("Unexpected HTTP status {} from web-seed", status));
250 }
251
252 let data = response
253 .bytes()
254 .await
255 .map_err(|e| format!("Failed to read response body: {}", e))?
256 .to_vec();
257
258 self.stats.record_bytes(data.len() as u64);
260
261 if data.len() != length as usize {
262 warn!(
263 expected = length,
264 actual = data.len(),
265 piece_index,
266 "Web-seed response size mismatch"
267 );
268 }
269
270 Ok(data)
271 }
272
273 pub async fn request_piece(
292 &self,
293 piece_index: u32,
294 piece_length: u32,
295 total_length: u64,
296 ) -> Result<Vec<u8>, String> {
297 if !self.can_request(piece_index) {
299 return Err(format!("Piece {} already being requested", piece_index));
300 }
301
302 self.mark_requesting(piece_index);
304
305 let piece_offset = piece_index as u64 * piece_length as u64;
307 let remaining = total_length.saturating_sub(piece_offset);
308 let actual_length = std::cmp::min(piece_length as u64, remaining);
309
310 let result = self
312 .download_piece(
313 piece_index,
314 piece_length as u64,
315 piece_offset,
316 actual_length,
317 )
318 .await;
319
320 self.clear_request(piece_index);
322
323 result
324 }
325
326 pub fn is_available(&self) -> bool {
331 true
332 }
333
334 pub fn url(&self) -> &str {
336 &self.base_url
337 }
338}
339
340pub struct WebSeedManager {
345 clients: Vec<WebSeedClient>,
347 stats: Arc<WebSeedStats>,
349 piece_length: u32,
351 total_length: u64,
353}
354
355impl WebSeedManager {
356 pub fn new(urls: Vec<String>, piece_length: u32, total_length: u64) -> Self {
378 debug!(
379 count = urls.len(),
380 "Creating WebSeedManager with {} seed(s)",
381 urls.len()
382 );
383
384 let stats = Arc::new(WebSeedStats::new());
385
386 let clients = urls
387 .into_iter()
388 .map(|url| WebSeedClient::with_shared_stats(&url, stats.clone()))
389 .collect();
390
391 Self {
392 clients,
393 stats,
394 piece_length,
395 total_length,
396 }
397 }
398
399 pub fn stats(&self) -> &WebSeedStats {
401 &self.stats
402 }
403
404 pub async fn request_piece(&self, piece_index: u32) -> Result<Vec<u8>, String> {
417 if self.clients.is_empty() {
418 return Err("No web-seeds configured".to_string());
419 }
420
421 let mut last_error = String::new();
422
423 for (i, client) in self.clients.iter().enumerate() {
424 if !client.is_available() || !client.can_request(piece_index) {
425 debug!(
426 index = i,
427 url = client.url(),
428 "Skipping unavailable or busy web-seed"
429 );
430 continue;
431 }
432
433 match client
434 .request_piece(piece_index, self.piece_length, self.total_length)
435 .await
436 {
437 Ok(data) => {
438 debug!(
439 piece_index,
440 seed_index = i,
441 url = client.url(),
442 size = data.len(),
443 "Piece downloaded from web-seed"
444 );
445 return Ok(data);
446 }
447 Err(e) => {
448 warn!(
449 piece_index,
450 seed_index = i,
451 url = client.url(),
452 error = %e,
453 "Web-seed download failed, trying next"
454 );
455 last_error = format!("seed[{}]={}: {}", i, client.url(), e);
456 }
457 }
458 }
459
460 Err(format!("All web-seeds failed: {}", last_error))
461 }
462
463 pub async fn try_download_piece(
480 &self,
481 piece_index: u32,
482 piece_length: u64,
483 piece_offset: u64,
484 length: u64,
485 ) -> Result<Vec<u8>, String> {
486 if self.clients.is_empty() {
487 return Err("No web-seeds configured".to_string());
488 }
489
490 let mut last_error = String::new();
491
492 for (i, client) in self.clients.iter().enumerate() {
493 if !client.is_available() {
494 debug!(
495 index = i,
496 url = client.url(),
497 "Skipping unavailable web-seed"
498 );
499 continue;
500 }
501
502 match client
503 .download_piece(piece_index, piece_length, piece_offset, length)
504 .await
505 {
506 Ok(data) => {
507 debug!(
508 piece_index,
509 seed_index = i,
510 url = client.url(),
511 size = data.len(),
512 "Piece downloaded from web-seed"
513 );
514 return Ok(data);
515 }
516 Err(e) => {
517 warn!(
518 piece_index,
519 seed_index = i,
520 url = client.url(),
521 error = %e,
522 "Web-seed download failed, trying next"
523 );
524 last_error = format!("seed[{}]={}: {}", i, client.url(), e);
525 }
526 }
527 }
528
529 Err(format!("All web-seeds failed: {}", last_error))
530 }
531
532 pub fn len(&self) -> usize {
534 self.clients.len()
535 }
536
537 pub fn is_empty(&self) -> bool {
539 self.clients.is_empty()
540 }
541
542 pub fn clients(&self) -> &[WebSeedClient] {
544 &self.clients
545 }
546}
547
548pub fn parse_url_list(
573 meta: &aria2_protocol::bittorrent::torrent::parser::TorrentMeta,
574) -> Vec<String> {
575 meta.web_seeds.clone()
576}
577
578pub fn parse_url_list_from_bytes(torrent_bytes: &[u8]) -> Vec<String> {
591 use aria2_protocol::bittorrent::bencode::codec::BencodeValue;
592
593 let (root, _) = match BencodeValue::decode(torrent_bytes) {
594 Ok(result) => result,
595 Err(_) => return Vec::new(),
596 };
597
598 match root.dict_get(b"url-list") {
599 Some(BencodeValue::Bytes(url_bytes)) => {
600 match std::str::from_utf8(url_bytes) {
602 Ok(url) => vec![url.to_string()],
603 Err(_) => Vec::new(),
604 }
605 }
606 Some(BencodeValue::List(items)) => {
607 items
609 .iter()
610 .filter_map(|item| item.as_str())
611 .map(|s| s.to_string())
612 .collect()
613 }
614 _ => Vec::new(), }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use aria2_protocol::bittorrent::bencode::codec::BencodeValue;
622 use std::collections::BTreeMap;
623
624 #[test]
627 fn test_parse_url_list_single() {
628 let mut root = BTreeMap::new();
629 root.insert(
630 b"announce".to_vec(),
631 BencodeValue::Bytes(b"http://tracker.example.com/announce".to_vec()),
632 );
633 root.insert(
634 b"url-list".to_vec(),
635 BencodeValue::Bytes(b"http://webseed.example.com/file.bin".to_vec()),
636 );
637
638 let mut info = BTreeMap::new();
640 info.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
641 info.insert(b"length".to_vec(), BencodeValue::Int(1024));
642 info.insert(b"piece length".to_vec(), BencodeValue::Int(512));
643 info.insert(b"pieces".to_vec(), BencodeValue::Bytes(vec![0u8; 40]));
644 root.insert(b"info".to_vec(), BencodeValue::Dict(info));
645
646 let encoded = BencodeValue::Dict(root).encode();
647 let urls = parse_url_list_from_bytes(&encoded);
648
649 assert_eq!(urls.len(), 1);
650 assert_eq!(urls[0], "http://webseed.example.com/file.bin");
651 }
652
653 #[test]
654 fn test_parse_url_list_multiple() {
655 let mut root = BTreeMap::new();
656 root.insert(
657 b"announce".to_vec(),
658 BencodeValue::Bytes(b"http://tracker.example.com/announce".to_vec()),
659 );
660 root.insert(
661 b"url-list".to_vec(),
662 BencodeValue::List(vec![
663 BencodeValue::Bytes(b"http://seed1.example.com/file.bin".to_vec()),
664 BencodeValue::Bytes(b"http://seed2.example.com/file.bin".to_vec()),
665 BencodeValue::Bytes(b"https://seed3.example.com/file.bin".to_vec()),
666 ]),
667 );
668
669 let mut info = BTreeMap::new();
670 info.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
671 info.insert(b"length".to_vec(), BencodeValue::Int(2048));
672 info.insert(b"piece length".to_vec(), BencodeValue::Int(512));
673 info.insert(b"pieces".to_vec(), BencodeValue::Bytes(vec![0u8; 80]));
674 root.insert(b"info".to_vec(), BencodeValue::Dict(info));
675
676 let encoded = BencodeValue::Dict(root).encode();
677 let urls = parse_url_list_from_bytes(&encoded);
678
679 assert_eq!(urls.len(), 3);
680 assert_eq!(urls[0], "http://seed1.example.com/file.bin");
681 assert_eq!(urls[1], "http://seed2.example.com/file.bin");
682 assert_eq!(urls[2], "https://seed3.example.com/file.bin");
683 }
684
685 #[test]
686 fn test_parse_url_list_missing() {
687 let mut root = BTreeMap::new();
688 root.insert(
689 b"announce".to_vec(),
690 BencodeValue::Bytes(b"http://tracker.example.com/announce".to_vec()),
691 );
692 let mut info = BTreeMap::new();
695 info.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
696 info.insert(b"length".to_vec(), BencodeValue::Int(512));
697 info.insert(b"piece length".to_vec(), BencodeValue::Int(256));
698 info.insert(b"pieces".to_vec(), BencodeValue::Bytes(vec![0u8; 20]));
699 root.insert(b"info".to_vec(), BencodeValue::Dict(info));
700
701 let encoded = BencodeValue::Dict(root).encode();
702 let urls = parse_url_list_from_bytes(&encoded);
703
704 assert!(urls.is_empty());
705 }
706
707 #[test]
710 fn test_range_request_format() {
711 let _client = WebSeedClient::new("http://example.com/file.bin");
713
714 let offset = 0u64;
717 let length = 16384u64;
718 let range_end = offset + length.saturating_sub(1);
719 let expected = format!("bytes={}-{}", offset, range_end);
720
721 assert_eq!(expected, "bytes=0-16383");
722
723 let offset2 = 524288u64;
725 let length2 = 262144u64;
726 let range_end2 = offset2 + length2.saturating_sub(1);
727 let expected2 = format!("bytes={}-{}", offset2, range_end2);
728
729 assert_eq!(expected2, "bytes=524288-786431");
730 }
731
732 #[test]
733 fn test_web_seed_manager_fallback() {
734 let urls = vec![
736 "http://seed1.example.com/file.iso".to_string(),
737 "http://seed2.example.com/file.iso".to_string(),
738 ];
739
740 let manager = WebSeedManager::new(urls, 16384, 1048576);
741
742 assert_eq!(manager.len(), 2);
743 assert!(!manager.is_empty());
744 assert_eq!(manager.clients().len(), 2);
745
746 assert_eq!(
748 manager.clients()[0].url(),
749 "http://seed1.example.com/file.iso"
750 );
751 assert_eq!(
752 manager.clients()[1].url(),
753 "http://seed2.example.com/file.iso"
754 );
755 }
756
757 #[test]
758 fn test_web_seed_client_creation() {
759 let client = WebSeedClient::new("https://cdn.example.com/releases/v1.tar.gz");
760
761 assert_eq!(client.url(), "https://cdn.example.com/releases/v1.tar.gz");
762 assert!(client.is_available());
763 }
764
765 #[test]
766 fn test_web_seed_manager_empty() {
767 let manager = WebSeedManager::new(Vec::new(), 16384, 1048576);
768
769 assert_eq!(manager.len(), 0);
770 assert!(manager.is_empty());
771 }
772
773 #[test]
774 fn test_parse_url_list_invalid_utf8() {
775 let mut root = BTreeMap::new();
776 root.insert(
777 b"url-list".to_vec(),
778 BencodeValue::Bytes(vec![0xFF, 0xFE]), );
780
781 let encoded = BencodeValue::Dict(root).encode();
782 let urls = parse_url_list_from_bytes(&encoded);
783
784 assert!(urls.is_empty());
786 }
787
788 #[test]
789 fn test_parse_url_list_mixed_valid_invalid() {
790 let mut root = BTreeMap::new();
791 root.insert(
792 b"url-list".to_vec(),
793 BencodeValue::List(vec![
794 BencodeValue::Bytes(b"http://valid.example.com/file.bin".to_vec()),
795 BencodeValue::Int(42), BencodeValue::Bytes(b"http://also-valid.example.com/file.bin".to_vec()),
797 ]),
798 );
799
800 let encoded = BencodeValue::Dict(root).encode();
801 let urls = parse_url_list_from_bytes(&encoded);
802
803 assert_eq!(urls.len(), 2);
805 assert_eq!(urls[0], "http://valid.example.com/file.bin");
806 assert_eq!(urls[1], "http://also-valid.example.com/file.bin");
807 }
808
809 #[test]
812 fn test_web_seed_stats() {
813 let stats = WebSeedStats::new();
814
815 stats.record_bytes(1000);
817 stats.record_bytes(500);
818
819 assert_eq!(stats.total_bytes_downloaded(), 1500);
820 }
821
822 #[test]
823 fn test_web_seed_stats_average_speed() {
824 let stats = WebSeedStats::new();
825 stats.record_bytes(10000);
826
827 let _speed = stats.average_speed();
829 }
830
831 #[test]
834 fn test_active_requests_tracking() {
835 let client = WebSeedClient::new("http://example.com/file.bin");
836
837 assert!(client.can_request(0));
839 assert!(client.can_request(1));
840 assert!(client.can_request(2));
841
842 client.mark_requesting(0);
844 assert!(!client.can_request(0)); assert!(client.can_request(1)); assert_eq!(client.active_request_count(), 1);
847
848 client.mark_requesting(1);
850 assert!(!client.can_request(0));
851 assert!(!client.can_request(1));
852 assert!(client.can_request(2));
853 assert_eq!(client.active_request_count(), 2);
854
855 client.clear_request(0);
857 assert!(client.can_request(0)); assert!(!client.can_request(1)); assert_eq!(client.active_request_count(), 1);
860 }
861
862 #[test]
863 fn test_web_seed_manager_stats() {
864 let urls = vec![
865 "http://seed1.example.com/file.bin".to_string(),
866 "http://seed2.example.com/file.bin".to_string(),
867 ];
868
869 let manager = WebSeedManager::new(urls, 16384, 1048576);
870
871 let stats = manager.stats();
873 assert_eq!(stats.total_bytes_downloaded(), 0);
874 }
875}