1use async_trait::async_trait;
2use futures::StreamExt;
3use futures::stream::FuturesUnordered;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6use tokio::sync::mpsc;
7use tracing::{debug, error, info, warn};
8
9use crate::constants;
10use crate::engine::active_output_registry::global_registry;
11use crate::engine::command::{Command, CommandStatus, ProgressUpdate};
12use crate::engine::concurrent_segment_manager::ConcurrentSegmentManager;
13use crate::engine::download_engine::DownloadEngine;
14use crate::engine::http_segment_downloader::HttpSegmentDownloader;
15use crate::engine::mirror_coordinator::{MirrorConfig, MirrorCoordinator};
16use crate::engine::retry_policy::RetryPolicy;
17use crate::error::{Aria2Error, RecoverableError, Result};
18use crate::filesystem::disk_writer::{
19 CachedDiskWriter, DefaultDiskWriter, DiskWriter, SeekableDiskWriter,
20};
21use crate::filesystem::file_allocation;
22use crate::filesystem::resume_helper::{ResumeHelper, ResumeState};
23use crate::http::client_pool;
24use crate::http::cookie::Cookie;
25use crate::http::cookie_storage::CookieStorage;
26use crate::http::socks_connector::{NoProxyMatcher, ProxyUrl};
27use crate::rate_limiter::{RateLimiter, RateLimiterConfig, ThrottledWriter};
28use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
29use crate::selector::adaptive_uri_selector::AdaptiveUriSelector;
30use crate::selector::server_stat_man::ServerStatMan;
31use crate::util::perf_monitor::{AtomicMetrics, Metrics, PerformanceMonitor};
32
33pub struct DownloadCommand {
34 group: Arc<tokio::sync::RwLock<RequestGroup>>,
35 client: Arc<reqwest::Client>,
36 output_path: std::path::PathBuf,
37 started: bool,
38 completed: bool,
39 completed_bytes: u64,
40 continue_enabled: bool,
41 file_allocation: String,
42 mmap_threshold: u64,
45 secure_falloc: bool,
48 cookie_storage: Arc<CookieStorage>,
49 cookie_file: Option<String>,
50 no_proxy_matcher: Option<NoProxyMatcher>,
51 use_hyper: bool,
56 stat_man: Arc<ServerStatMan>,
59 perf_monitor: Option<Arc<PerformanceMonitor>>,
61 atomic_metrics: Arc<AtomicMetrics>,
63 headers: Vec<(String, String)>,
66 progress_sender: Option<mpsc::UnboundedSender<ProgressUpdate>>,
74 progress_receiver: Option<mpsc::UnboundedReceiver<ProgressUpdate>>,
79 progress_aggregator_handle: Option<tokio::task::JoinHandle<()>>,
84}
85
86type SegmentFetchFuture = std::pin::Pin<
90 Box<dyn std::future::Future<Output = (u32, std::result::Result<bytes::Bytes, String>)> + Send>,
91>;
92
93impl DownloadCommand {
94 pub fn new(
98 gid: GroupId,
99 uri: &str,
100 options: &DownloadOptions,
101 output_dir: Option<&str>,
102 output_name: Option<&str>,
103 ) -> Result<Self> {
104 let group = Arc::new(tokio::sync::RwLock::new(RequestGroup::new(
105 gid,
106 vec![uri.to_string()],
107 options.clone(),
108 )));
109 Self::new_with_group(group, uri, options, output_dir, output_name)
110 }
111
112 pub fn new_with_group(
116 group: Arc<tokio::sync::RwLock<RequestGroup>>,
117 uri: &str,
118 options: &DownloadOptions,
119 output_dir: Option<&str>,
120 output_name: Option<&str>,
121 ) -> Result<Self> {
122 let dir = output_dir
123 .map(|d| d.to_string())
124 .or_else(|| options.dir.clone())
125 .unwrap_or_else(|| constants::DEFAULT_OUTPUT_DIR.to_string());
126
127 let filename = output_name
128 .map(|n| n.to_string())
129 .or_else(|| Self::extract_filename(uri))
130 .unwrap_or_else(|| constants::DEFAULT_FILENAME.to_string());
131
132 let path = std::path::PathBuf::from(&dir).join(&filename);
133
134 let headers = options.parsed_headers();
136
137 let use_hyper = options.http_proxy.is_none() && options.all_proxy.is_none();
143 let client = if use_hyper {
144 client_pool::get_global_client()
147 } else {
148 let mut builder = reqwest::Client::builder()
150 .connect_timeout(Duration::from_secs(
151 constants::HTTP_DEFAULT_CONNECT_TIMEOUT_SECS,
152 ))
153 .timeout(Duration::from_secs(
154 constants::HTTP_DEFAULT_OVERALL_TIMEOUT_SECS,
155 ))
156 .user_agent(constants::USER_AGENT)
157 .redirect(reqwest::redirect::Policy::limited(
158 constants::HTTP_DEFAULT_MAX_REDIRECTS,
159 ))
160 .pool_max_idle_per_host(constants::HTTP_DEFAULT_POOL_MAX_IDLE_PER_HOST)
161 .pool_idle_timeout(Some(std::time::Duration::from_secs(
162 constants::HTTP_DEFAULT_POOL_IDLE_TIMEOUT_SECS,
163 )))
164 .tcp_keepalive(Some(std::time::Duration::from_secs(
165 constants::HTTP_DEFAULT_TCP_KEEPALIVE_SECS,
166 )));
167
168 if let Some(ref proxy) = options.http_proxy
169 && let Ok(proxy_url) = proxy.parse::<reqwest::Url>()
170 && let Ok(p) = reqwest::Proxy::all(proxy_url.to_string())
171 {
172 builder = builder.proxy(p);
173 }
174
175 if options.http_proxy.is_none()
178 && let Some(ref all_proxy) = options.all_proxy
179 {
180 match ProxyUrl::parse(all_proxy) {
181 Ok(parsed) => {
182 match parsed.protocol {
183 crate::http::socks_connector::ProxyProtocol::Http
184 | crate::http::socks_connector::ProxyProtocol::Https => {
185 if let Ok(p) = reqwest::Proxy::all(all_proxy.to_string()) {
186 builder = builder.proxy(p);
187 }
188 }
189 _ => {
190 tracing::info!(
194 "SOCKS proxy configured ({}) - use SocksConnector for direct TCP connections",
195 all_proxy
196 );
197 }
198 }
199 }
200 Err(e) => {
201 warn!("Failed to parse all-proxy URL '{}': {}", all_proxy, e);
202 }
203 }
204 }
205
206 let client = builder.build().map_err(|e| {
207 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
208 "Failed to build HTTP client: {}",
209 e
210 )))
211 })?;
212
213 Arc::new(client)
214 };
215
216 info!("DownloadCommand created: {} -> {}", uri, path.display());
217
218 let cookie_file = options.cookie_file.clone();
219 let cookie_storage = Arc::new(CookieStorage::new());
220
221 if let Some(ref cf) = cookie_file {
222 let p = std::path::Path::new(cf);
223 if p.exists() {
224 match cookie_storage.load_file(p) {
225 Ok(n) => info!("Loaded {} cookies from file: {}", n, cf),
226 Err(e) => warn!("Failed to load cookie file {}: {}", cf, e),
227 }
228 }
229 }
230
231 if let Some(ref cookies_str) = options.cookies {
232 let domain = Self::extract_host(uri);
233 for pair in cookies_str.split(';') {
234 let pair = pair.trim();
235 if pair.is_empty() {
236 continue;
237 }
238 if let Some((name, value)) = pair.split_once('=') {
239 let name = name.trim();
240 let value = value.trim();
241 if !name.is_empty() {
242 cookie_storage.add(Cookie::new(name, value, &domain));
243 }
244 }
245 }
246 if !cookie_storage.is_empty() {
247 info!("Manually set {} cookies", cookie_storage.count());
248 }
249 }
250
251 let (progress_tx, progress_rx) = mpsc::unbounded_channel::<ProgressUpdate>();
255
256 Ok(Self {
257 group,
258 client,
259 output_path: path,
260 started: false,
261 completed: false,
262 completed_bytes: 0,
263 continue_enabled: true,
264 file_allocation: options
265 .file_allocation
266 .clone()
267 .unwrap_or_else(|| constants::DEFAULT_FILE_ALLOCATION.to_string()),
268 mmap_threshold: options.mmap_threshold.unwrap_or(256 * 1024 * 1024),
269 secure_falloc: options.secure_falloc,
270 cookie_storage,
271 cookie_file,
272 no_proxy_matcher: options
273 .no_proxy
274 .as_ref()
275 .map(|np| NoProxyMatcher::from_env_value(np)),
276 use_hyper,
277 stat_man: Arc::new(ServerStatMan::new()),
278 perf_monitor: None, atomic_metrics: Arc::new(AtomicMetrics::new()),
280 headers,
281 progress_sender: Some(progress_tx),
282 progress_receiver: Some(progress_rx),
283 progress_aggregator_handle: None,
284 })
285 }
286
287 pub fn new_with_client(
289 gid: GroupId,
290 uri: &str,
291 options: &DownloadOptions,
292 output_dir: Option<&str>,
293 output_name: Option<&str>,
294 client: Arc<reqwest::Client>,
295 ) -> Result<Self> {
296 let group = Arc::new(tokio::sync::RwLock::new(RequestGroup::new(
297 gid,
298 vec![uri.to_string()],
299 options.clone(),
300 )));
301 Self::new_with_group_and_client(group, uri, options, output_dir, output_name, client)
302 }
303
304 pub fn new_with_group_and_client(
306 group: Arc<tokio::sync::RwLock<RequestGroup>>,
307 uri: &str,
308 options: &DownloadOptions,
309 output_dir: Option<&str>,
310 output_name: Option<&str>,
311 client: Arc<reqwest::Client>,
312 ) -> Result<Self> {
313 let dir = output_dir
314 .map(|d| d.to_string())
315 .or_else(|| options.dir.clone())
316 .unwrap_or_else(|| constants::DEFAULT_OUTPUT_DIR.to_string());
317
318 let filename = output_name
319 .map(|n| n.to_string())
320 .or_else(|| Self::extract_filename(uri))
321 .unwrap_or_else(|| constants::DEFAULT_FILENAME.to_string());
322
323 let path = std::path::PathBuf::from(&dir).join(&filename);
324
325 let headers = options.parsed_headers();
326 info!(
327 "DownloadCommand created (shared client): {} -> {}",
328 uri,
329 path.display()
330 );
331
332 let use_hyper = options.http_proxy.is_none() && options.all_proxy.is_none();
337
338 let cookie_file = options.cookie_file.clone();
339 let cookie_storage = Arc::new(CookieStorage::new());
340
341 if let Some(ref cf) = cookie_file {
342 let p = std::path::Path::new(cf);
343 if p.exists() {
344 match cookie_storage.load_file(p) {
345 Ok(n) => info!("Loaded {} cookies from file: {}", n, cf),
346 Err(e) => warn!("Failed to load cookie file {}: {}", cf, e),
347 }
348 }
349 }
350
351 if let Some(ref cookies_str) = options.cookies {
352 let domain = Self::extract_host(uri);
353 for pair in cookies_str.split(';') {
354 let pair = pair.trim();
355 if pair.is_empty() {
356 continue;
357 }
358 if let Some((name, value)) = pair.split_once('=') {
359 let name = name.trim();
360 let value = value.trim();
361 if !name.is_empty() {
362 cookie_storage.add(Cookie::new(name, value, &domain));
363 }
364 }
365 }
366 if !cookie_storage.is_empty() {
367 info!("Manually set {} cookies", cookie_storage.count());
368 }
369 }
370
371 let (progress_tx, progress_rx) = mpsc::unbounded_channel::<ProgressUpdate>();
375
376 Ok(Self {
377 group,
378 client,
379 output_path: path,
380 started: false,
381 completed: false,
382 completed_bytes: 0,
383 continue_enabled: true,
384 file_allocation: options
385 .file_allocation
386 .clone()
387 .unwrap_or_else(|| constants::DEFAULT_FILE_ALLOCATION.to_string()),
388 mmap_threshold: options.mmap_threshold.unwrap_or(256 * 1024 * 1024),
389 secure_falloc: options.secure_falloc,
390 cookie_storage,
391 cookie_file,
392 no_proxy_matcher: options
393 .no_proxy
394 .as_ref()
395 .map(|np| NoProxyMatcher::from_env_value(np)),
396 use_hyper,
397 stat_man: Arc::new(ServerStatMan::new()),
398 perf_monitor: None, atomic_metrics: Arc::new(AtomicMetrics::new()),
400 headers,
401 progress_sender: Some(progress_tx),
402 progress_receiver: Some(progress_rx),
403 progress_aggregator_handle: None,
404 })
405 }
406
407 pub fn new_with_stat_man(
412 gid: GroupId,
413 uri: &str,
414 options: &DownloadOptions,
415 output_dir: Option<&str>,
416 output_name: Option<&str>,
417 stat_man: Arc<ServerStatMan>,
418 ) -> Result<Self> {
419 let mut cmd = Self::new(gid, uri, options, output_dir, output_name)?;
420 cmd.stat_man = stat_man;
421 Ok(cmd)
422 }
423
424 pub fn enable_perf_monitor(&mut self) {
428 self.perf_monitor = Some(Arc::new(PerformanceMonitor::new()));
429 }
430
431 #[allow(dead_code)]
448 pub(crate) fn with_progress_sender(
449 mut self,
450 sender: mpsc::UnboundedSender<ProgressUpdate>,
451 ) -> Self {
452 self.progress_sender = Some(sender);
453 self.progress_receiver = None;
456 self
457 }
458
459 pub(crate) fn spawn_progress_aggregator(&mut self) {
473 if self.progress_aggregator_handle.is_some() {
474 return;
476 }
477 if let Some(rx) = self.progress_receiver.take() {
478 let handle = DownloadEngine::spawn_progress_aggregator(Arc::clone(&self.group), rx);
479 self.progress_aggregator_handle = Some(handle);
480 }
481 }
486
487 pub(crate) async fn drain_progress_aggregator(&mut self) {
495 self.progress_sender = None;
498 if let Some(handle) = self.progress_aggregator_handle.take() {
499 if let Err(e) = handle.await {
503 warn!("Progress aggregator task ended unexpectedly: {}", e);
504 }
505 }
506 }
507
508 pub fn get_perf_metrics(&self) -> Metrics {
510 self.atomic_metrics.snapshot()
511 }
512
513 pub fn get_perf_report(&self) -> Option<String> {
515 self.perf_monitor.as_ref().map(|m| m.export_text())
516 }
517
518 pub fn get_perf_report_json(&self) -> Option<String> {
520 self.perf_monitor.as_ref().map(|m| m.export_json())
521 }
522
523 fn extract_filename(uri: &str) -> Option<String> {
524 uri.rsplit('/')
525 .next()
526 .filter(|s| !s.is_empty() && *s != "/")
527 .map(|s| s.split('?').next().unwrap_or(s).to_string())
528 }
529
530 fn extract_host(uri: &str) -> String {
531 reqwest::Url::parse(uri)
532 .ok()
533 .and_then(|u| u.host_str().map(|h| h.to_string()))
534 .unwrap_or_else(|| constants::DEFAULT_HOST.to_string())
535 }
536
537 fn save_cookies_if_configured(&self) {
538 if let Some(ref cf) = self.cookie_file {
539 if let Err(e) = self.cookie_storage.save_file(std::path::Path::new(cf)) {
540 warn!("Failed to save cookie file {}: {}", cf, e);
541 } else {
542 info!("Cookies saved to {}", cf);
543 }
544 }
545 }
546
547 pub async fn group(&self) -> tokio::sync::RwLockReadGuard<'_, RequestGroup> {
548 self.group.read().await
549 }
550
551 pub async fn group_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, RequestGroup> {
552 self.group.write().await
553 }
554
555 pub fn no_proxy_matcher(&self) -> Option<&NoProxyMatcher> {
557 self.no_proxy_matcher.as_ref()
558 }
559
560 fn should_use_concurrent(&self, total_length: u64, supports_range: bool) -> bool {
561 if !supports_range {
562 return false;
563 }
564 if total_length < constants::CONCURRENT_MIN_FILE_SIZE as u64 {
565 return false;
566 }
567
568 let split = { self.group.blocking_read().options().split.unwrap_or(1) };
569 split > 1
570 }
571
572 fn create_writer(&self, total_length: u64) -> CachedDiskWriter {
578 let use_mmap = self.file_allocation == "mmap" && total_length >= self.mmap_threshold;
579 CachedDiskWriter::new_with_mmap(&self.output_path, Some(total_length), None, use_mmap)
580 }
581
582 async fn execute_sequential_download(
583 &mut self,
584 uri: &str,
585 resume_state: &crate::filesystem::resume_helper::ResumeState,
586 total_length: u64,
587 ) -> Result<()> {
588 #[cfg(not(target_os = "linux"))]
591 let _ = total_length;
592
593 #[cfg(target_os = "linux")]
597 {
598 if !resume_state.should_resume
599 && total_length > 0
600 && self.use_hyper
601 && !uri.starts_with("https://")
602 && self.headers.is_empty()
603 && self.cookie_storage.is_empty()
604 {
605 match self.try_splice_sequential(uri, total_length).await {
606 Ok(()) => return Ok(()),
607 Err(e) => {
608 debug!(
609 "Splice download failed for {}, falling back to streaming: {}",
610 uri, e
611 );
612 }
613 }
614 }
615 }
616
617 let url_parsed = reqwest::Url::parse(uri).ok();
618 let mut request = if let Some(range_header) = ResumeHelper::build_range_header(resume_state)
619 {
620 debug!("Resume download: {}", range_header);
621 self.client.get(uri).header("Range", range_header)
622 } else {
623 self.client.get(uri)
624 };
625
626 if let Some(ref url) = url_parsed {
627 let host = url.host_str().unwrap_or("");
628 let path = url.path();
629 let secure = url.scheme() == "https";
630 let cookie_hdr = self.cookie_storage.to_header_string(host, path, secure);
631 if !cookie_hdr.is_empty() {
632 request = request.header("Cookie", &cookie_hdr);
633 }
634 }
635
636 for (name, value) in &self.headers {
638 request = request.header(name, value);
639 }
640
641 let response = request.send().await.map_err(|e| {
642 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
643 message: format!("HTTP request failed: {}", e),
644 })
645 })?;
646
647 if let Some(ref url) = url_parsed {
648 let domain = url.host_str().unwrap_or("");
649 let path = url.path();
650 for sc_val in response.headers().get_all("set-cookie").iter() {
651 if let Ok(sc_str) = sc_val.to_str()
652 && let Some(c) = Cookie::from_set_cookie_header(sc_str, domain, path)
653 {
654 self.cookie_storage.add(c);
655 debug!("Received Set-Cookie: {}", sc_str);
656 }
657 }
658 }
659
660 let status = response.status();
661 if !status.is_success() && status.as_u16() != 206 {
662 if status.as_u16() >= 500 {
663 return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
664 code: status.as_u16(),
665 }));
666 }
667 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
668 format!("HTTP error: {}", status),
669 )));
670 }
671
672 let resp_length = response.content_length().unwrap_or(0) as u64;
673 let actual_total = if resume_state.should_resume {
674 resume_state.start_offset + resp_length
675 } else {
676 resp_length
677 };
678 {
679 let mut g = self.group.write().await;
680 g.set_total_length(actual_total).await;
681 g.set_total_length_atomic(actual_total);
683 }
684
685 let start_offset = if resume_state.should_resume {
686 resume_state.start_offset
687 } else {
688 0
689 };
690 self.completed_bytes = start_offset;
691 let rate_limit = { self.group.read().await.options().max_download_limit };
692
693 let raw_writer = DefaultDiskWriter::new(&self.output_path);
694 let mut writer: Box<dyn DiskWriter> = match rate_limit {
695 Some(rate) if rate > 0 => {
696 let cfg = RateLimiterConfig::new(Some(rate), None);
697 let limiter = RateLimiter::new(&cfg);
698 debug!("Download speed limit enabled: {} bytes/s", rate);
699 {
701 let g = self.group.read().await;
702 g.set_rate_limiter(limiter.clone()).await;
703 }
704 Box::new(ThrottledWriter::new(raw_writer, limiter))
705 }
706 _ => Box::new(raw_writer),
707 };
708
709 let mut stream = response.bytes_stream();
710 let mut last_speed_update = Instant::now();
711 let mut last_completed = 0u64;
712 let mut last_progress_update = 0u64; let write_piece = constants::RATE_LIMITER_CHUNK_SIZE;
725
726 while let Some(chunk) = stream.next().await {
727 let data: bytes::Bytes = chunk.map_err(|e| {
728 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
729 message: e.to_string(),
730 })
731 })?;
732
733 let mut offset = 0usize;
734 while offset < data.len() {
735 let end = (offset + write_piece).min(data.len());
736 let piece = &data[offset..end];
737 writer.write(piece).await?;
738 self.completed_bytes += piece.len() as u64;
739 offset = end;
740
741 if self.completed_bytes - last_progress_update
744 >= constants::PROGRESS_UPDATE_BYTES as u64
745 {
746 let elapsed = last_speed_update.elapsed();
750 let speed = if elapsed.as_millis()
751 >= constants::HTTP_SPEED_UPDATE_INTERVAL_MS as u128
752 {
753 let delta = self.completed_bytes - last_completed;
754 let s = (delta as f64 / elapsed.as_secs_f64()) as u64;
755 last_speed_update = Instant::now();
756 last_completed = self.completed_bytes;
757 s
758 } else {
759 0
760 };
761
762 if let Some(ref sender) = self.progress_sender {
763 let _ = sender.send(ProgressUpdate {
767 completed_bytes: self.completed_bytes,
768 download_speed: speed,
769 upload_speed: 0,
770 });
771 } else {
772 let g = self.group.write().await;
774 g.update_progress(self.completed_bytes).await;
775 g.set_completed_length(self.completed_bytes);
777 if speed > 0 {
778 g.update_speed(speed, 0).await;
779 g.set_download_speed_cached(speed);
781 }
782 }
783
784 if speed > 0 {
787 self.atomic_metrics.record_throughput(speed);
788 if let Some(ref monitor) = self.perf_monitor {
789 let metrics = Metrics::new(speed, elapsed.as_millis() as u64, 0, 0)
790 .with_label("download_speed");
791 monitor.record_metric("download_speed", metrics);
792 }
793 }
794 last_progress_update = self.completed_bytes;
795 }
796 }
797 }
798
799 writer.finalize().await.ok();
800
801 let final_speed = {
803 let g = self.group.read().await;
804 let elapsed = g.elapsed_time().await;
805 match elapsed {
806 Some(d) if d.as_secs_f64() > 0.0 => {
807 (self.completed_bytes as f64 / d.as_secs_f64()) as u64
808 }
809 _ => 0,
810 }
811 };
812 {
813 let mut g = self.group.write().await;
814 g.update_progress(self.completed_bytes).await;
815 g.update_speed(final_speed, 0).await;
816 g.set_completed_length(self.completed_bytes);
818 g.set_download_speed_cached(final_speed);
819 g.complete().await?;
820 }
821
822 info!(
823 "Sequential download complete: {} ({} bytes)",
824 self.output_path.display(),
825 self.completed_bytes
826 );
827 self.save_cookies_if_configured();
828 Ok(())
829 }
830
831 #[cfg(target_os = "linux")]
842 async fn try_splice_sequential(&mut self, uri: &str, total_length: u64) -> Result<()> {
843 let file = std::fs::OpenOptions::new()
846 .write(true)
847 .create(true)
848 .truncate(true)
849 .open(&self.output_path)?;
850
851 let bytes = crate::http::splice_http::try_splice_download(uri, 0, total_length, &file, 0)
852 .await
853 .map_err(|e| Aria2Error::Io(format!("splice download failed: {e}")))?;
854
855 self.completed_bytes = bytes;
857
858 let final_speed = {
859 let g = self.group.read().await;
860 let elapsed = g.elapsed_time().await;
861 match elapsed {
862 Some(d) if d.as_secs_f64() > 0.0 => (bytes as f64 / d.as_secs_f64()) as u64,
863 _ => 0,
864 }
865 };
866
867 {
868 let mut g = self.group.write().await;
869 g.set_total_length(bytes).await;
870 g.set_total_length_atomic(bytes);
871 g.update_progress(bytes).await;
872 g.set_completed_length(bytes);
873 g.update_speed(final_speed, 0).await;
874 g.set_download_speed_cached(final_speed);
875 g.complete().await?;
876 }
877
878 info!(
879 "Sequential download (splice) complete: {} ({} bytes)",
880 self.output_path.display(),
881 bytes
882 );
883 self.save_cookies_if_configured();
884 Ok(())
885 }
886
887 #[allow(dead_code)] async fn execute_concurrent_download(&mut self, uri: &str, total_length: u64) -> Result<()> {
889 let options = self.group.read().await.options().clone();
890 let split = options.split.unwrap_or(1) as usize;
891 let max_conn = options
892 .max_connection_per_server
893 .unwrap_or(constants::DEFAULT_MAX_CONNECTION_PER_SERVER as u16)
894 as usize;
895 let seg_size = total_length / split as u64;
896 let mut last_progress_update = 0u64; info!(
899 "Concurrent download started: split={}, max_conn={}, segment_size={} bytes, total={}",
900 split, max_conn, seg_size, total_length
901 );
902
903 let mut manager =
904 ConcurrentSegmentManager::new(total_length, vec![uri.to_string()], Some(seg_size));
905 manager.set_max_connections_per_mirror(max_conn.min(split));
906
907 let url_parsed = reqwest::Url::parse(uri).ok();
908 let cookie_hdr = if let Some(ref url) = url_parsed {
909 self.cookie_storage.to_header_string(
910 url.host_str().unwrap_or(""),
911 url.path(),
912 url.scheme() == "https",
913 )
914 } else {
915 String::new()
916 };
917 let cookie_hdr_for_spawn: Option<String> = if cookie_hdr.is_empty() {
918 None
919 } else {
920 Some(cookie_hdr)
921 };
922
923 let mut writer = self.create_writer(total_length);
924
925 let limiter = options
926 .max_download_limit
927 .filter(|&r| r > 0)
928 .map(|r| RateLimiter::new(&RateLimiterConfig::new(Some(r), None)));
929 if let Some(ref limiter) = limiter {
931 let g = self.group.read().await;
932 g.set_rate_limiter(limiter.clone()).await;
933 }
934
935 let mut active: FuturesUnordered<SegmentFetchFuture> = FuturesUnordered::new();
940 let mut active_segs: std::collections::HashMap<u32, u64> = std::collections::HashMap::new();
943
944 loop {
945 while active.len() < max_conn {
947 match manager.next_pending_segment_for_mirror(0) {
948 Some((seg_idx, offset, length)) => {
949 let url = uri.to_string();
950 let dl = HttpSegmentDownloader::new(&self.client, self.use_hyper);
951 let ch = cookie_hdr_for_spawn.clone();
952 let headers = self.headers.clone();
953 active_segs.insert(seg_idx, offset);
954 let fut = Box::pin(async move {
955 let result = dl
956 .download_range(&url, offset, length, ch.as_deref(), &headers)
957 .await
958 .map_err(|e| e.to_string());
959 (seg_idx, result)
960 });
961 active.push(fut);
962 debug!(
963 seg_idx = seg_idx,
964 offset = offset,
965 length = length,
966 "Spawned segment fetch"
967 );
968 }
969 None => break,
970 }
971 }
972
973 if active.is_empty() {
975 if manager.is_complete() {
976 debug!("All segments complete");
977 break;
978 }
979 if manager.has_failed_segments() && !manager.has_pending_segments() {
980 return Err(Aria2Error::Recoverable(
981 RecoverableError::TemporaryNetworkFailure {
982 message: "Concurrent download: all segments failed".into(),
983 },
984 ));
985 }
986 warn!("Concurrent download stuck: no active or pending segments but not complete");
989 break;
990 }
991
992 if let Some((seg_idx, result)) = active.next().await {
995 let offset = active_segs.remove(&seg_idx).unwrap_or(0);
996 match result {
997 Ok(data) => {
998 let data_len = data.len();
999 if let Some(ref lim) = limiter {
1000 lim.acquire_download(data_len as u64).await;
1001 }
1002 let data_for_manager = data.clone();
1005 writer.write_bytes_at(offset, data).await.map_err(|e| {
1006 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1007 "Write failed: {}",
1008 e
1009 )))
1010 })?;
1011 manager.complete_segment(seg_idx, data_for_manager);
1012 self.completed_bytes += data_len as u64;
1013
1014 if self.completed_bytes - last_progress_update
1016 >= constants::PROGRESS_UPDATE_BYTES as u64
1017 {
1018 if let Some(ref sender) = self.progress_sender {
1019 let _ = sender.send(ProgressUpdate {
1021 completed_bytes: self.completed_bytes,
1022 download_speed: 0,
1023 upload_speed: 0,
1024 });
1025 } else {
1026 let g = self.group.write().await;
1028 g.update_progress(self.completed_bytes).await;
1029 g.set_completed_length(self.completed_bytes);
1031 }
1032 last_progress_update = self.completed_bytes;
1033 }
1034 }
1035 Err(e) => {
1036 warn!(seg_idx = seg_idx, error = %e, "Segment download failed");
1037 manager.fail_segment(seg_idx);
1038 }
1039 }
1040 }
1041 }
1042
1043 writer.flush().await.map_err(|e| {
1044 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1045 "Flush failed: {}",
1046 e
1047 )))
1048 })?;
1049
1050 let final_speed = {
1052 let g = self.group.read().await;
1053 let elapsed = g.elapsed_time().await;
1054 match elapsed {
1055 Some(d) if d.as_secs_f64() > 0.0 => {
1056 (self.completed_bytes as f64 / d.as_secs_f64()) as u64
1057 }
1058 _ => 0,
1059 }
1060 };
1061 {
1062 let mut g = self.group.write().await;
1063 g.update_progress(self.completed_bytes).await;
1064 g.update_speed(final_speed, 0).await;
1065 g.set_completed_length(self.completed_bytes);
1067 g.set_download_speed_cached(final_speed);
1068 g.complete().await?;
1069 }
1070
1071 info!(
1072 "Concurrent download complete: {} ({} bytes)",
1073 self.output_path.display(),
1074 self.completed_bytes
1075 );
1076 self.save_cookies_if_configured();
1077 Ok(())
1078 }
1079
1080 async fn execute_sequential_download_with_retry(
1081 &mut self,
1082 uri: &str,
1083 resume_state: &ResumeState,
1084 total_length: u64,
1085 retry_policy: &RetryPolicy,
1086 ) -> Result<()> {
1087 let mut last_err = None;
1088
1089 for attempt in 0..=retry_policy.max_retries {
1090 if attempt > 0
1091 && let Some(wait) = retry_policy.compute_wait(attempt - 1)
1092 {
1093 info!(
1094 "Sequential download retry #{} (waiting {:?})...",
1095 attempt, wait
1096 );
1097 tokio::time::sleep(wait).await;
1098 }
1099
1100 match self
1101 .execute_sequential_download(uri, resume_state, total_length)
1102 .await
1103 {
1104 Ok(()) => return Ok(()),
1105 Err(e) => {
1106 warn!("Sequential download attempt #{} failed: {}", attempt + 1, e);
1107 last_err = Some(e);
1108 if retry_policy.is_exhausted(attempt)
1109 || !retry_policy
1110 .should_retry_error(&format!("{:?}", last_err.as_ref().unwrap()))
1111 {
1112 break;
1113 }
1114 }
1115 }
1116 }
1117
1118 Err(last_err.unwrap_or_else(|| {
1119 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
1120 message: "All retries failed".into(),
1121 })
1122 }))
1123 }
1124
1125 async fn execute_concurrent_download_with_retry(
1126 &mut self,
1127 uri: &str,
1128 total_length: u64,
1129 resume_state: &ResumeState,
1130 max_retries_per_segment: u32,
1131 ) -> Result<()> {
1132 info!(
1133 "Using concurrent download mode (split={}, max_retries/segment={})",
1134 self.group.read().await.options().split.unwrap_or(1),
1135 max_retries_per_segment
1136 );
1137
1138 let all_uris: Vec<String> = {
1140 let g = self.group.read().await;
1141 g.uris().to_vec()
1142 };
1143
1144 let use_intelligent_selection = all_uris.len() > 1;
1146
1147 if use_intelligent_selection {
1148 info!(
1149 "Intelligent multi-mirror selection enabled: {} mirror sources",
1150 all_uris.len()
1151 );
1152 self.execute_concurrent_download_with_coordinator(
1153 &all_uris,
1154 total_length,
1155 resume_state,
1156 max_retries_per_segment,
1157 )
1158 .await
1159 } else {
1160 self.execute_concurrent_download_single_uri(
1162 uri,
1163 total_length,
1164 resume_state,
1165 max_retries_per_segment,
1166 )
1167 .await
1168 }
1169 }
1170
1171 async fn execute_concurrent_download_with_coordinator(
1176 &mut self,
1177 uris: &[String],
1178 total_length: u64,
1179 resume_state: &ResumeState,
1180 max_retries_per_segment: u32,
1181 ) -> Result<()> {
1182 let split = self.group.read().await.options().split.unwrap_or(1) as u64;
1183 let segment_size = total_length.div_ceil(split);
1184 let max_conn = self
1185 .group
1186 .read()
1187 .await
1188 .options()
1189 .max_connection_per_server
1190 .unwrap_or(constants::DEFAULT_MAX_CONNECTION_PER_SERVER as u16)
1191 as usize;
1192
1193 let mirror_config = MirrorConfig {
1195 max_connections_per_mirror: max_conn.min(split as usize),
1196 max_total_connections: max_conn * uris.len(),
1197 speed_threshold: constants::MIRROR_SPEED_THRESHOLD,
1198 cooldown_secs: constants::MIRROR_COOLDOWN_SECS,
1199 max_retries: max_retries_per_segment,
1200 };
1201
1202 let selector = Box::new(AdaptiveUriSelector::new_with_uris(
1204 Arc::clone(&self.stat_man),
1205 uris.to_vec(),
1206 ));
1207
1208 let segment_manager = ConcurrentSegmentManager::new_with_selector(
1210 total_length,
1211 uris.to_vec(),
1212 Some(segment_size),
1213 Arc::clone(&self.stat_man),
1214 selector,
1215 );
1216
1217 let mut coordinator = MirrorCoordinator::with_segment_manager(
1219 Arc::clone(&self.stat_man),
1220 Box::new(crate::selector::uri_selector::InorderUriSelector::new()),
1221 segment_manager,
1222 mirror_config,
1223 uris.to_vec(),
1224 );
1225
1226 if resume_state.should_resume {
1227 debug!(
1230 "Resume: existing {} bytes, continuing from offset {}",
1231 resume_state.existing_length, resume_state.start_offset
1232 );
1233 }
1234
1235 let mut writer = self.create_writer(total_length);
1236 let mut last_speed_update = Instant::now();
1237 let mut last_completed = 0u64;
1238 let mut last_progress_update = 0u64; while coordinator.has_pending_segments() || !coordinator.is_complete() {
1241 while let Some((mirror_idx, mirror_url, (seg_idx, offset, length))) =
1243 coordinator.select_mirror_for_segment()
1244 {
1245 info!(
1246 "Starting segment {} download: mirror={}, offset={}, size={}",
1247 seg_idx, mirror_idx, offset, length
1248 );
1249
1250 let downloader = HttpSegmentDownloader::new(&self.client, self.use_hyper);
1251 let seg_start = Instant::now();
1252
1253 let url_parsed = reqwest::Url::parse(&mirror_url).ok();
1255 let cookie_hdr = if let Some(ref url) = url_parsed {
1256 self.cookie_storage.to_header_string(
1257 url.host_str().unwrap_or(""),
1258 url.path(),
1259 url.scheme() == "https",
1260 )
1261 } else {
1262 String::new()
1263 };
1264
1265 let result = downloader
1266 .download_range(
1267 &mirror_url,
1268 offset,
1269 length,
1270 if cookie_hdr.is_empty() {
1271 None
1272 } else {
1273 Some(&cookie_hdr)
1274 },
1275 &self.headers,
1276 )
1277 .await;
1278
1279 match result {
1280 Ok(data) => {
1281 let elapsed = seg_start.elapsed();
1282 let speed = if elapsed.as_secs_f64() > 0.0 {
1283 (data.len() as f64 / elapsed.as_secs_f64()) as u64
1284 } else {
1285 0
1286 };
1287
1288 debug!(
1289 "Segment {} complete: {} bytes, speed={} B/s",
1290 seg_idx,
1291 data.len(),
1292 speed
1293 );
1294
1295 let data_for_coordinator = data.clone();
1298 writer.write_bytes_at(offset, data).await.map_err(|e| {
1299 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1300 "Write failed: {}",
1301 e
1302 )))
1303 })?;
1304
1305 coordinator.on_segment_complete(
1307 mirror_idx,
1308 seg_idx,
1309 data_for_coordinator,
1310 speed,
1311 );
1312 }
1313 Err(e) => {
1314 warn!(
1315 "Segment {} download failed (mirror={}): {}",
1316 seg_idx, mirror_idx, e
1317 );
1318
1319 let error_code = constants::HTTP_DEFAULT_ERROR_CODE;
1321
1322 coordinator.on_segment_failed(mirror_idx, seg_idx, error_code);
1324 }
1325 }
1326
1327 self.completed_bytes = {
1329 let total = coordinator.num_segments() as u64;
1331 let progress_pct = coordinator.progress();
1332 if total > 0 {
1333 (progress_pct / 100.0 * total as f64) as u64
1334 } else {
1335 0
1336 }
1337 };
1338
1339 if self.completed_bytes - last_progress_update
1341 >= constants::PROGRESS_UPDATE_BYTES as u64
1342 {
1343 let elapsed = last_speed_update.elapsed();
1345 let speed = if elapsed.as_millis()
1346 >= constants::HTTP_SPEED_UPDATE_INTERVAL_MS as u128
1347 {
1348 let delta = self.completed_bytes - last_completed;
1349 let s = (delta as f64 / elapsed.as_secs_f64()) as u64;
1350 last_speed_update = Instant::now();
1351 last_completed = self.completed_bytes;
1352 s
1353 } else {
1354 0
1355 };
1356
1357 if let Some(ref sender) = self.progress_sender {
1358 let _ = sender.send(ProgressUpdate {
1360 completed_bytes: self.completed_bytes,
1361 download_speed: speed,
1362 upload_speed: 0,
1363 });
1364 } else {
1365 let g = self.group.write().await;
1367 g.update_progress(self.completed_bytes).await;
1368 g.set_completed_length(self.completed_bytes);
1369 if speed > 0 {
1370 g.update_speed(speed, 0).await;
1371 g.set_download_speed_cached(speed);
1372 }
1373 }
1374 last_progress_update = self.completed_bytes;
1375 }
1376 }
1377
1378 if coordinator.is_complete() {
1379 break;
1380 }
1381
1382 if coordinator.has_failed_segments() {
1383 error!("Permanently failed download segments exist");
1384 return Err(Aria2Error::Recoverable(
1385 RecoverableError::TemporaryNetworkFailure {
1386 message: "Some download segments permanently failed".into(),
1387 },
1388 ));
1389 }
1390
1391 tokio::time::sleep(Duration::from_millis(100)).await;
1392 }
1393
1394 writer.flush().await.map_err(|e| {
1395 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1396 "Flush failed: {}",
1397 e
1398 )))
1399 })?;
1400
1401 let final_speed = {
1403 let g = self.group.read().await;
1404 let elapsed = g.elapsed_time().await;
1405 match elapsed {
1406 Some(d) if d.as_secs_f64() > 0.0 => {
1407 (self.completed_bytes as f64 / d.as_secs_f64()) as u64
1408 }
1409 _ => 0,
1410 }
1411 };
1412
1413 {
1414 let mut g = self.group.write().await;
1415 g.set_total_length(self.completed_bytes).await;
1416 g.set_total_length_atomic(self.completed_bytes);
1417 g.set_completed_length(self.completed_bytes);
1418 g.update_speed(final_speed, 0).await;
1419 g.set_download_speed_cached(final_speed);
1420 g.complete().await?;
1421 }
1422
1423 if let Err(e) = self.save_server_stats().await {
1425 warn!("Failed to save server statistics: {}", e);
1426 }
1427
1428 info!(
1429 "Multi-mirror concurrent download complete: {} ({} bytes, {} B/s)",
1430 self.output_path.display(),
1431 self.completed_bytes,
1432 final_speed
1433 );
1434 self.save_cookies_if_configured();
1435 Ok(())
1436 }
1437
1438 async fn execute_concurrent_download_single_uri(
1440 &mut self,
1441 uri: &str,
1442 total_length: u64,
1443 resume_state: &ResumeState,
1444 max_retries_per_segment: u32,
1445 ) -> Result<()> {
1446 let split = self.group.read().await.options().split.unwrap_or(1) as u64;
1447 let segment_size = total_length.div_ceil(split);
1448 let mut manager =
1449 ConcurrentSegmentManager::new(total_length, vec![uri.to_string()], Some(segment_size));
1450 manager.set_max_retries(max_retries_per_segment);
1451
1452 if resume_state.should_resume {
1453 manager.mark_completed_up_to(resume_state.start_offset, resume_state.existing_length);
1454 }
1455
1456 let mut writer = self.create_writer(total_length);
1457
1458 while manager.has_pending_segments() || !manager.is_complete() {
1459 while let Some((seg_idx, offset, length)) = manager.next_pending_segment() {
1460 let seg_idx_u32 = seg_idx;
1461 info!(
1462 "Starting segment {} download: offset={}, size={}",
1463 seg_idx, offset, length
1464 );
1465 let downloader = HttpSegmentDownloader::new(&self.client, self.use_hyper);
1466 let seg_start = Instant::now();
1467 let result = downloader
1468 .download_range(uri, offset, length, None, &self.headers)
1469 .await;
1470
1471 match result {
1472 Ok(data) => {
1473 let elapsed = seg_start.elapsed();
1474 let speed = if elapsed.as_secs_f64() > 0.0 {
1475 (data.len() as f64 / elapsed.as_secs_f64()) as u64
1476 } else {
1477 0
1478 };
1479 debug!(
1480 "Segment {} complete: {} bytes, speed={} B/s",
1481 seg_idx,
1482 data.len(),
1483 speed
1484 );
1485
1486 let data_for_manager = data.clone();
1489 writer.write_bytes_at(offset, data).await.map_err(|e| {
1490 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1491 "Write failed: {}",
1492 e
1493 )))
1494 })?;
1495
1496 manager.report_segment_complete(
1498 seg_idx_u32,
1499 data_for_manager,
1500 speed,
1501 false,
1502 );
1503 }
1504 Err(e) => {
1505 warn!("Segment {} download failed: {}", seg_idx, e);
1506 manager
1507 .report_segment_failed(seg_idx_u32, constants::HTTP_DEFAULT_ERROR_CODE);
1508 }
1509 }
1510
1511 self.completed_bytes = manager.completed_bytes();
1512 let g = self.group.write().await;
1513 g.update_progress(self.completed_bytes).await;
1514 g.set_completed_length(self.completed_bytes);
1516 }
1517
1518 if manager.is_complete() {
1519 break;
1520 }
1521
1522 if manager.has_permanently_failed_segments() {
1523 error!("Permanently failed download segments exist");
1524 return Err(Aria2Error::Recoverable(
1525 RecoverableError::TemporaryNetworkFailure {
1526 message: "Some download segments permanently failed".into(),
1527 },
1528 ));
1529 }
1530
1531 tokio::time::sleep(Duration::from_millis(100)).await;
1532 }
1533
1534 writer.flush().await.map_err(|e| {
1535 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1536 "Flush failed: {}",
1537 e
1538 )))
1539 })?;
1540 self.completed_bytes = manager.completed_bytes();
1541 let mut g = self.group.write().await;
1542 g.set_total_length(self.completed_bytes).await;
1543 g.set_total_length_atomic(self.completed_bytes);
1545 g.set_completed_length(self.completed_bytes);
1546 g.complete().await?;
1547 Ok(())
1548 }
1549
1550 async fn save_server_stats(&self) -> std::result::Result<usize, String> {
1552 Ok(0)
1555 }
1556
1557 pub fn stat_man(&self) -> &Arc<ServerStatMan> {
1559 &self.stat_man
1560 }
1561}
1562
1563#[async_trait]
1564impl Command for DownloadCommand {
1565 async fn execute(&mut self) -> Result<()> {
1566 if !self.started {
1567 self.group.write().await.start().await?;
1568 self.started = true;
1569 }
1570
1571 let uri = {
1572 let g = self.group.read().await;
1573 g.uris().first().cloned().unwrap_or_default()
1574 };
1575
1576 if uri.is_empty() {
1577 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
1578 "Download URI is empty".into(),
1579 )));
1580 }
1581
1582 debug!(
1583 "Starting download: {} -> {}",
1584 uri,
1585 self.output_path.display()
1586 );
1587
1588 if let Some(parent) = self.output_path.parent()
1589 && !parent.exists()
1590 {
1591 std::fs::create_dir_all(parent).map_err(|e| {
1592 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1593 "Failed to create directory: {}",
1594 e
1595 )))
1596 })?;
1597 }
1598
1599 let original_path = self.output_path.clone();
1603 self.output_path = global_registry().resolve(&original_path).await;
1604 if self.output_path != original_path {
1605 info!(
1606 "Filename collision resolved: '{}' -> '{}'",
1607 original_path.display(),
1608 self.output_path.display()
1609 );
1610 }
1611
1612 let release_path = |path: &std::path::Path| {
1614 let p = path.to_path_buf();
1615 #[allow(clippy::let_underscore_future)]
1616 let _ = tokio::spawn(async move {
1617 global_registry().release(&p).await;
1618 });
1619 };
1620
1621 let url_for_head = reqwest::Url::parse(&uri).ok();
1622 let cookie_hdr_head = if let Some(ref url) = url_for_head {
1623 self.cookie_storage.to_header_string(
1624 url.host_str().unwrap_or(""),
1625 url.path(),
1626 url.scheme() == "https",
1627 )
1628 } else {
1629 String::new()
1630 };
1631 let mut head_req = self.client.head(&uri);
1632 if !cookie_hdr_head.is_empty() {
1633 head_req = head_req.header("Cookie", &cookie_hdr_head);
1634 }
1635 for (name, value) in &self.headers {
1638 head_req = head_req.header(name, value);
1639 }
1640 let head_resp = head_req.send().await.ok();
1641 let (total_length, supports_range) = if let Some(ref resp) = head_resp {
1642 let tl = resp.content_length().unwrap_or(0);
1643 let sr = resp
1644 .headers()
1645 .get("Accept-Ranges")
1646 .and_then(|v| v.to_str().ok())
1647 .is_some_and(|v| v.to_lowercase().contains("bytes"));
1648 (tl, sr)
1649 } else {
1650 (0, false)
1651 };
1652
1653 let resume_helper = ResumeHelper::new(&self.output_path, self.continue_enabled);
1654 let resume_state = resume_helper.detect(total_length).await?;
1655
1656 if resume_state.is_complete {
1657 info!(
1658 "File already exists completely, skipping download: {} ({} bytes)",
1659 self.output_path.display(),
1660 resume_state.existing_length
1661 );
1662 self.completed_bytes = resume_state.existing_length;
1663 let mut g = self.group.write().await;
1664 g.set_total_length(self.completed_bytes).await;
1665 g.update_progress(self.completed_bytes).await;
1666 g.set_total_length_atomic(self.completed_bytes);
1668 g.set_completed_length(self.completed_bytes);
1669 g.complete().await?;
1670 self.completed = true;
1671 release_path(&self.output_path);
1672 return Ok(());
1673 }
1674
1675 self.spawn_progress_aggregator();
1681
1682 let download_result: Result<()> = async {
1686 if total_length > 0 {
1687 file_allocation::preallocate_file(
1688 &self.output_path,
1689 total_length,
1690 &self.file_allocation,
1691 self.secure_falloc,
1692 )
1693 .await?;
1694 }
1695
1696 let options = self.group.read().await.options().clone();
1697
1698 if self.should_use_concurrent(total_length, supports_range) {
1699 if resume_state.should_resume {
1700 info!(
1701 "Concurrent mode + resume: existing {} bytes, continuing from offset {}",
1702 resume_state.existing_length, resume_state.start_offset
1703 );
1704 }
1705 let max_retries = options.max_retries;
1706 return self
1707 .execute_concurrent_download_with_retry(
1708 &uri,
1709 total_length,
1710 &resume_state,
1711 max_retries,
1712 )
1713 .await;
1714 }
1715
1716 let retry_policy = RetryPolicy::new(options.max_retries, options.retry_wait * 1000);
1717 self.execute_sequential_download_with_retry(
1718 &uri,
1719 &resume_state,
1720 total_length,
1721 &retry_policy,
1722 )
1723 .await
1724 }
1725 .await;
1726
1727 self.drain_progress_aggregator().await;
1733
1734 if download_result.is_ok() {
1735 self.completed = true;
1736 let g = self.group.write().await;
1740 let total = g.total_length();
1741 g.update_progress(total).await;
1742 g.set_completed_length(total);
1743 }
1744 release_path(&self.output_path);
1745 download_result
1746 }
1747
1748 fn status(&self) -> CommandStatus {
1749 if self.completed {
1750 CommandStatus::Completed
1751 } else if self.completed_bytes > 0 {
1752 CommandStatus::Running
1753 } else {
1754 CommandStatus::Pending
1755 }
1756 }
1757
1758 fn timeout(&self) -> Option<Duration> {
1759 Some(Duration::from_secs(
1760 constants::HTTP_DEFAULT_COMMAND_TIMEOUT_SECS,
1761 ))
1762 }
1763}
1764
1765#[cfg(test)]
1766mod tests {
1767 use super::*;
1768 use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
1769
1770 impl DownloadCommand {
1773 fn has_progress_sender(&self) -> bool {
1775 self.progress_sender.is_some()
1776 }
1777
1778 fn has_progress_receiver(&self) -> bool {
1780 self.progress_receiver.is_some()
1781 }
1782
1783 fn has_progress_aggregator_handle(&self) -> bool {
1785 self.progress_aggregator_handle.is_some()
1786 }
1787
1788 fn send_progress_update(&self, update: ProgressUpdate) {
1790 if let Some(ref sender) = self.progress_sender {
1791 let _ = sender.send(update);
1792 } else {
1793 panic!("test called send_progress_update but no sender is set");
1794 }
1795 }
1796 }
1797
1798 #[test]
1803 fn test_progress_channel_auto_created() {
1804 let cmd = DownloadCommand::new(
1805 GroupId::new(1),
1806 "http://example.com/file.bin",
1807 &DownloadOptions::default(),
1808 None,
1809 None,
1810 )
1811 .expect("DownloadCommand::new should succeed with a valid HTTP URI");
1812
1813 assert!(
1815 cmd.has_progress_sender(),
1816 "progress_sender should be Some after construction (auto-created)"
1817 );
1818 assert!(
1819 cmd.has_progress_receiver(),
1820 "progress_receiver should be Some after construction (held for lazy spawn)"
1821 );
1822 assert!(
1825 !cmd.has_progress_aggregator_handle(),
1826 "progress_aggregator_handle should be None until execute() spawns it"
1827 );
1828 }
1829
1830 #[tokio::test]
1835 async fn test_progress_updates_flow_through_channel() {
1836 let group = Arc::new(tokio::sync::RwLock::new(RequestGroup::new(
1838 GroupId::new(2),
1839 vec!["http://example.com/file.bin".to_string()],
1840 DownloadOptions::default(),
1841 )));
1842 let group_clone = Arc::clone(&group);
1843
1844 let mut cmd = DownloadCommand::new_with_group(
1845 group,
1846 "http://example.com/file.bin",
1847 &DownloadOptions::default(),
1848 None,
1849 None,
1850 )
1851 .expect("DownloadCommand::new_with_group should succeed");
1852
1853 assert!(cmd.has_progress_sender());
1855 assert!(cmd.has_progress_receiver());
1856
1857 cmd.spawn_progress_aggregator();
1859 assert!(cmd.has_progress_aggregator_handle());
1860 assert!(!cmd.has_progress_receiver());
1862
1863 cmd.send_progress_update(ProgressUpdate {
1865 completed_bytes: 4096,
1866 download_speed: 0,
1867 upload_speed: 0,
1868 });
1869
1870 cmd.drain_progress_aggregator().await;
1872 assert!(!cmd.has_progress_sender());
1873 assert!(!cmd.has_progress_aggregator_handle());
1874
1875 let completed = { group_clone.read().await.get_completed_length() };
1878 assert_eq!(
1879 completed, 4096,
1880 "aggregator should have applied the progress update to RequestGroup"
1881 );
1882 }
1883}