1use async_trait::async_trait;
2use futures::StreamExt;
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5use tracing::{debug, info, warn};
6
7use crate::engine::active_output_registry::global_registry;
8use crate::engine::command::{Command, CommandStatus};
9use crate::error::{Aria2Error, FatalError, RecoverableError, Result};
10use crate::filesystem::disk_writer::{DefaultDiskWriter, DiskWriter};
11use crate::rate_limiter::{RateLimiter, RateLimiterConfig, ThrottledWriter};
12use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
13use aria2_protocol::metalink::parser::UrlEntry;
14
15pub struct MetalinkDownloadCommand {
16 group: Arc<tokio::sync::RwLock<RequestGroup>>,
17 client: reqwest::Client,
18 output_path: std::path::PathBuf,
19 started: bool,
20 completed: bool,
21 completed_bytes: u64,
22 metalink_data: Vec<u8>,
23}
24
25impl MetalinkDownloadCommand {
26 pub fn new(
27 gid: GroupId,
28 metalink_bytes: &[u8],
29 options: &DownloadOptions,
30 output_dir: Option<&str>,
31 ) -> Result<Self> {
32 let doc = aria2_protocol::metalink::parser::MetalinkDocument::parse(metalink_bytes)
33 .map_err(|e| {
34 Aria2Error::Fatal(FatalError::Config(format!("Metalink parse failed: {}", e)))
35 })?;
36
37 let file = doc.single_file().ok_or_else(|| {
38 Aria2Error::Fatal(FatalError::Config(
39 "Metalink contains multiple files or no files".into(),
40 ))
41 })?;
42
43 if file.urls.is_empty() {
44 return Err(Aria2Error::Fatal(FatalError::Config(
45 "Metalink file has no download URLs".into(),
46 )));
47 }
48
49 let dir = output_dir
50 .map(|d| d.to_string())
51 .or_else(|| options.dir.clone())
52 .unwrap_or_else(|| ".".to_string());
53
54 let filename = file.name.clone();
55 let path = std::path::PathBuf::from(&dir).join(&filename);
56
57 let urls: Vec<String> = file
58 .get_sorted_urls()
59 .iter()
60 .map(|u| u.url.clone())
61 .collect();
62 let group = RequestGroup::new(gid, urls, options.clone());
63
64 let client = reqwest::Client::builder()
65 .connect_timeout(Duration::from_secs(30))
66 .timeout(Duration::from_secs(300))
67 .user_agent("aria2-rust/0.1.0")
68 .redirect(reqwest::redirect::Policy::limited(5))
69 .build()
70 .map_err(|e| {
71 Aria2Error::Fatal(FatalError::Config(format!(
72 "HTTP client build failed: {}",
73 e
74 )))
75 })?;
76
77 info!(
78 "MetalinkDownloadCommand created: {} -> {} ({} mirrors)",
79 file.name,
80 path.display(),
81 file.urls.len()
82 );
83
84 Ok(Self {
85 group: Arc::new(tokio::sync::RwLock::new(group)),
86 client,
87 output_path: path,
88 started: false,
89 completed: false,
90 completed_bytes: 0,
91 metalink_data: metalink_bytes.to_vec(),
92 })
93 }
94
95 pub async fn group(&self) -> tokio::sync::RwLockReadGuard<'_, RequestGroup> {
96 self.group.read().await
97 }
98}
99
100#[async_trait]
101impl Command for MetalinkDownloadCommand {
102 async fn execute(&mut self) -> Result<()> {
103 if !self.started {
104 self.group.write().await.start().await?;
105 self.started = true;
106 }
107
108 let doc = aria2_protocol::metalink::parser::MetalinkDocument::parse(&self.metalink_data)
109 .map_err(|e| {
110 Aria2Error::Fatal(FatalError::Config(format!("Metalink parse error: {}", e)))
111 })?;
112
113 let file = doc.single_file().ok_or_else(|| {
114 Aria2Error::Fatal(FatalError::Config("No available file after parsing".into()))
115 })?;
116
117 let sorted_urls = file.get_sorted_urls();
118 if sorted_urls.is_empty() {
119 return Err(Aria2Error::Fatal(FatalError::Config(
120 "No download mirrors available".into(),
121 )));
122 }
123
124 if let Some(parent) = self.output_path.parent()
125 && !parent.exists()
126 {
127 std::fs::create_dir_all(parent).map_err(|e| {
128 Aria2Error::Fatal(FatalError::Config(format!("mkdir failed: {}", e)))
129 })?;
130 }
131
132 let resolved_output_path = global_registry().resolve(&self.output_path).await;
136
137 let release_path = |path: &std::path::Path| {
139 let p = path.to_path_buf();
140 #[allow(clippy::let_underscore_future)]
142 let _ = tokio::spawn(async move {
143 global_registry().release(&p).await;
144 });
145 };
146
147 let expected_size = file.size;
148 let hash_entry = file.hashes.first().cloned();
149
150 let mut last_error = None;
151
152 for url_entry in &sorted_urls {
153 debug!(
154 "Trying mirror [priority={}] : {}",
155 url_entry.priority, url_entry.url
156 );
157
158 match self.try_download_url(&url_entry.url, expected_size).await {
159 Ok(data) => {
160 if let Some(ref hash) = hash_entry
161 && !self.verify_hash(&data, hash)?
162 {
163 warn!(
164 "Hash verification failed [{}]: trying next mirror",
165 hash.algo.as_standard_name()
166 );
167 last_error = Some(Aria2Error::Recoverable(
168 RecoverableError::TemporaryNetworkFailure {
169 message: format!(
170 "Hash verification failed: {}",
171 hash.algo.as_standard_name()
172 ),
173 },
174 ));
175 continue;
176 }
177
178 let raw_writer = DefaultDiskWriter::new(&resolved_output_path);
179 let rate_limit = {
180 let g = self.group.read().await;
181 g.options().max_download_limit
182 };
183 let mut writer: Box<dyn DiskWriter> = match rate_limit {
184 Some(rate) if rate > 0 => Box::new(ThrottledWriter::new(
185 raw_writer,
186 RateLimiter::new(&RateLimiterConfig::new(Some(rate), None)),
187 )),
188 _ => Box::new(raw_writer),
189 };
190 writer.write(&data).await?;
191 writer.finalize().await.ok();
192
193 self.completed_bytes = data.len() as u64;
194
195 {
196 let mut g = self.group.write().await;
197 g.update_progress(self.completed_bytes).await;
198 g.update_speed(self.completed_bytes, 0).await;
199 g.complete().await?;
200 }
201
202 info!(
203 "Metalink download done: {} ({} bytes from {})",
204 resolved_output_path.display(),
205 self.completed_bytes,
206 url_entry.url
207 );
208 self.completed = true;
209 release_path(&resolved_output_path);
210 return Ok(());
211 }
212 Err(e) => {
213 warn!("Mirror download failed {}: {}", url_entry.url, e);
214 last_error = Some(e);
215 }
216 }
217 }
218
219 release_path(&resolved_output_path);
220 Err(last_error
221 .unwrap_or_else(|| Aria2Error::Fatal(FatalError::Config("All mirrors failed".into()))))
222 }
223
224 fn status(&self) -> CommandStatus {
225 if self.completed {
226 CommandStatus::Completed
227 } else if self.completed_bytes > 0 {
228 CommandStatus::Running
229 } else {
230 CommandStatus::Pending
231 }
232 }
233
234 fn timeout(&self) -> Option<Duration> {
235 Some(Duration::from_secs(600))
236 }
237}
238
239impl MetalinkDownloadCommand {
240 async fn try_download_url(&mut self, url: &str, expected_size: Option<u64>) -> Result<Vec<u8>> {
241 let response = self.client.get(url).send().await.map_err(|e| {
242 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
243 message: format!("HTTP request failed: {}", e),
244 })
245 })?;
246
247 let status = response.status();
248 if !status.is_success() && status.as_u16() != 206 {
249 if status.as_u16() >= 500 {
250 return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
251 code: status.as_u16(),
252 }));
253 }
254 return Err(Aria2Error::Fatal(FatalError::Config(format!(
255 "HTTP error: {}",
256 status
257 ))));
258 }
259
260 let total_length = response
266 .headers()
267 .get(reqwest::header::CONTENT_LENGTH)
268 .and_then(|v| v.to_str().ok())
269 .and_then(|v| v.parse::<u64>().ok())
270 .unwrap_or(0);
271
272 {
273 let mut g = self.group.write().await;
274 g.set_total_length(total_length.max(expected_size.unwrap_or(0)))
275 .await;
276 }
277
278 let mut data = Vec::with_capacity(total_length as usize);
279 let mut stream = response.bytes_stream();
280 let _start_time = Instant::now();
281 let mut last_speed_update = Instant::now();
282 let mut last_completed = 0u64;
283
284 while let Some(chunk_result) = stream.next().await {
285 let bytes: bytes::Bytes = chunk_result.map_err(|e: reqwest::Error| {
286 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
287 message: e.to_string(),
288 })
289 })?;
290 data.extend_from_slice(&bytes);
291 self.completed_bytes = data.len() as u64;
292
293 let elapsed = last_speed_update.elapsed();
294 if elapsed.as_millis() >= 500 {
295 let delta = self.completed_bytes - last_completed;
296 let speed = (delta as f64 / elapsed.as_secs_f64()) as u64;
297 let g = self.group.write().await;
298 g.update_progress(self.completed_bytes).await;
299 g.update_speed(speed, 0).await;
300 last_speed_update = Instant::now();
301 last_completed = self.completed_bytes;
302 }
303 }
304
305 Ok(data)
306 }
307
308 fn verify_hash(
309 &self,
310 data: &[u8],
311 hash: &aria2_protocol::metalink::parser::HashEntry,
312 ) -> Result<bool> {
313 use aria2_protocol::metalink::parser::HashAlgorithm;
314
315 match hash.algo {
316 HashAlgorithm::Md5 => {
317 let digest = md5::compute(data);
318 Ok(format!("{:x}", digest) == hash.value)
319 }
320 HashAlgorithm::Sha1 => {
321 use sha1::Digest;
322 let mut hasher = sha1::Sha1::new();
323 hasher.update(data);
324 let result = hasher.finalize();
325 Ok(format!("{:x}", result) == hash.value)
326 }
327 HashAlgorithm::Sha256 => {
328 use sha2::Digest;
329 let mut hasher = sha2::Sha256::new();
330 hasher.update(data);
331 let result = hasher.finalize();
332 Ok(format!("{:x}", result) == hash.value)
333 }
334 HashAlgorithm::Sha512 => {
335 use sha2::Digest;
336 let mut hasher = sha2::Sha512::new();
337 hasher.update(data);
338 let result = hasher.finalize();
339 Ok(format!("{:x}", result) == hash.value)
340 }
341 }
342 }
343}
344
345pub fn select_mirrors_by_priority<'a>(
382 resources: &'a [UrlEntry],
383 location_preference: &str,
384) -> Vec<&'a UrlEntry> {
385 let mut sorted: Vec<&'a UrlEntry> = resources.iter().collect();
386
387 sorted.sort_by(|a, b| {
388 let pri_cmp = b.priority.cmp(&a.priority);
390 if pri_cmp != std::cmp::Ordering::Equal {
391 return pri_cmp;
392 }
393
394 if !location_preference.is_empty() {
396 let a_matches = a
397 .location
398 .as_ref()
399 .map(|l| {
400 l.contains(location_preference) || location_preference.contains(l.as_str())
401 })
402 .unwrap_or(false);
403 let b_matches = b
404 .location
405 .as_ref()
406 .map(|l| {
407 l.contains(location_preference) || location_preference.contains(l.as_str())
408 })
409 .unwrap_or(false);
410
411 if a_matches != b_matches {
413 return b_matches.cmp(&a_matches);
414 }
415 }
416
417 std::cmp::Ordering::Equal
418 });
419
420 sorted
421}
422
423pub async fn try_mirrors_with_failover<F, Fut>(
465 sorted_urls: &[&UrlEntry],
466 download_fn: F,
467) -> std::result::Result<Vec<u8>, String>
468where
469 F: Fn(&str) -> Fut,
470 Fut: std::future::Future<Output = std::result::Result<Vec<u8>, String>>,
471{
472 for (i, url_res) in sorted_urls.iter().enumerate() {
473 info!(
474 index = i,
475 url = %url_res.url,
476 priority = url_res.priority,
477 "Trying mirror"
478 );
479
480 match download_fn(&url_res.url).await {
481 Ok(data) => {
482 info!(
483 index = i,
484 size = data.len(),
485 url = %url_res.url,
486 "Mirror succeeded"
487 );
488 return Ok(data);
489 }
490 Err(e) => {
491 warn!(
492 index = i,
493 url = %url_res.url,
494 error = %e,
495 "Mirror failed, trying next"
496 );
497 continue;
498 }
499 }
500 }
501
502 Err(format!("All {} mirrors failed", sorted_urls.len()))
503}
504
505#[cfg(test)]
510mod tests {
511 use super::*;
512 use aria2_protocol::metalink::parser::UrlEntry;
513
514 #[test]
519 fn test_priority_descending_order() {
520 let urls = vec![
521 UrlEntry::new("http://mirror3.example.com/file.bin").with_priority(1),
522 UrlEntry::new("http://mirror1.example.com/file.bin").with_priority(3),
523 UrlEntry::new("http://mirror2.example.com/file.bin").with_priority(2),
524 ];
525
526 let sorted = select_mirrors_by_priority(&urls, "");
527
528 assert_eq!(sorted.len(), 3, "Should return all URLs");
530 assert_eq!(
531 sorted[0].priority, 3,
532 "First URL should have highest priority (3)"
533 );
534 assert_eq!(
535 sorted[1].priority, 2,
536 "Second URL should have medium priority (2)"
537 );
538 assert_eq!(
539 sorted[2].priority, 1,
540 "Third URL should have lowest priority (1)"
541 );
542
543 assert!(
545 sorted[0].url.contains("mirror1"),
546 "First should be mirror1 (priority 3)"
547 );
548 assert!(
549 sorted[1].url.contains("mirror2"),
550 "Second should be mirror2 (priority 2)"
551 );
552 assert!(
553 sorted[2].url.contains("mirror3"),
554 "Third should be mirror3 (priority 1)"
555 );
556 }
557
558 #[test]
563 fn test_location_preference_boosts_matching() {
564 let urls = vec![
565 UrlEntry::new("http://us-mirror1.example.com/file.bin")
566 .with_priority(5)
567 .with_location("us"),
568 UrlEntry::new("http://eu-mirror1.example.com/file.bin")
569 .with_priority(5)
570 .with_location("eu"),
571 UrlEntry::new("http://eu-mirror2.example.com/file.bin")
572 .with_priority(5)
573 .with_location("eu"),
574 UrlEntry::new("http://jp-mirror1.example.com/file.bin")
575 .with_priority(5)
576 .with_location("jp"),
577 ];
578
579 let sorted = select_mirrors_by_priority(&urls, "eu");
581
582 assert_eq!(sorted.len(), 4, "Should return all URLs");
583
584 let eu_urls: Vec<_> = sorted
586 .iter()
587 .filter(|u| u.location.as_deref() == Some("eu"))
588 .collect();
589
590 assert_eq!(eu_urls.len(), 2, "Should find 2 EU mirrors");
591
592 let first_non_eu_idx = sorted
594 .iter()
595 .position(|u| u.location.as_deref() != Some("eu"))
596 .expect("Should find at least one non-EU mirror");
597
598 let last_eu_idx = sorted
599 .iter()
600 .rposition(|u| u.location.as_deref() == Some("eu"))
601 .expect("Should find EU mirrors");
602
603 assert!(
604 last_eu_idx < first_non_eu_idx,
605 "EU mirrors should come before non-EU mirrors"
606 );
607
608 let sorted_us = select_mirrors_by_priority(&urls, "us");
610 let us_first = &sorted_us[0];
611 assert_eq!(
612 us_first.location.as_deref(),
613 Some("us"),
614 "US mirror should be first when preferring US"
615 );
616 }
617
618 #[tokio::test]
623 async fn test_failover_tries_all_then_errors() {
624 let urls = [
625 UrlEntry::new("http://mirror1.fail/file.bin").with_priority(3),
626 UrlEntry::new("http://mirror2.fail/file.bin").with_priority(2),
627 UrlEntry::new("http://mirror3.fail/file.bin").with_priority(1),
628 ];
629
630 let fail_fn = |url: &str| -> std::pin::Pin<
632 Box<dyn std::future::Future<Output = std::result::Result<Vec<u8>, String>> + '_>,
633 > {
634 let url_owned = url.to_string();
635 Box::pin(async move { Err(format!("Connection refused to {}", url_owned)) })
636 };
637
638 let url_refs: Vec<&UrlEntry> = urls.iter().collect();
639
640 let result = try_mirrors_with_failover(&url_refs, fail_fn).await;
641
642 assert!(result.is_err(), "Should return error when all mirrors fail");
643
644 let error_msg = result.unwrap_err();
645 assert!(
646 error_msg.contains("All 3 mirrors failed"),
647 "Error message should indicate all 3 mirrors failed"
648 );
649 }
650
651 #[tokio::test]
656 async fn test_single_mirror_no_failover_needed() {
657 let urls =
658 [UrlEntry::new("http://working-mirror.example.com/success.bin").with_priority(10)];
659
660 let expected_data = b"Downloaded file content".to_vec();
661
662 let data_shared = std::sync::Arc::new(expected_data.clone());
665 let success_fn = move |_url: &str| {
666 let data = data_shared.clone();
667 async move { Ok((*data).clone()) }
668 };
669
670 let result = try_mirrors_with_failover(&urls.iter().collect::<Vec<_>>(), &success_fn).await;
671
672 assert!(result.is_ok(), "Single working mirror should succeed");
673
674 let downloaded_data = result.unwrap();
675 assert_eq!(
676 downloaded_data, expected_data,
677 "Downloaded data should match expected content"
678 );
679 assert_eq!(
680 downloaded_data.len(),
681 expected_data.len(),
682 "Should download exactly {} bytes",
683 expected_data.len()
684 );
685 }
686
687 #[test]
693 fn test_priority_overrides_location() {
694 let urls = vec![
695 UrlEntry::new("http://low-eu.example.com/file.bin")
696 .with_priority(1)
697 .with_location("eu"), UrlEntry::new("http://high-us.example.com/file.bin")
699 .with_priority(10)
700 .with_location("us"), ];
702
703 let sorted = select_mirrors_by_priority(&urls, "eu");
704
705 assert_eq!(
707 sorted[0].priority, 10,
708 "Higher priority URL should come first regardless of location match"
709 );
710 assert_eq!(
711 sorted[0].url, "http://high-us.example.com/file.bin",
712 "First should be high-priority US URL"
713 );
714 assert_eq!(
715 sorted[1].priority, 1,
716 "Lower priority URL should come second"
717 );
718 }
719
720 #[test]
722 fn test_empty_resources_returns_empty() {
723 let urls: Vec<UrlEntry> = Vec::new();
724 let sorted = select_mirrors_by_priority(&urls, "");
725
726 assert!(sorted.is_empty(), "Empty input should produce empty output");
727 }
728
729 #[tokio::test]
731 async fn test_failover_succeeds_on_second_mirror() {
732 let urls = [
733 UrlEntry::new("http://failing-mirror.example.com/file.bin").with_priority(5),
734 UrlEntry::new("http://working-mirror.example.com/file.bin").with_priority(3),
735 ];
736
737 let attempt_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
738 let count_clone = attempt_count.clone();
739 let fallback_fn = move |url: &str| {
740 let url_owned = url.to_string();
741 let count = count_clone.clone();
742 async move {
743 count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
744 if url_owned.contains("failing") {
745 Err("Connection timeout".to_string())
746 } else {
747 Ok(b"Success data".to_vec())
748 }
749 }
750 };
751
752 let result =
753 try_mirrors_with_failover(&urls.iter().collect::<Vec<_>>(), &fallback_fn).await;
754
755 assert!(result.is_ok(), "Should succeed on second mirror");
756 assert_eq!(
757 attempt_count.load(std::sync::atomic::Ordering::SeqCst),
758 2,
759 "Should have attempted 2 mirrors"
760 );
761
762 let data = result.unwrap();
763 assert_eq!(
764 data, b"Success data",
765 "Data from second mirror should be returned"
766 );
767 }
768}