1use async_trait::async_trait;
2use futures::StreamExt;
3use std::sync::Arc;
4use std::time::Duration;
5use tracing::{info, warn};
6
7use crate::engine::active_output_registry::global_registry;
8use crate::engine::command::{Command, CommandStatus};
9use crate::engine::concurrent_segment_manager::ConcurrentSegmentManager;
10use crate::error::{Aria2Error, FatalError, RecoverableError, Result};
11use crate::filesystem::control_file;
12use crate::filesystem::disk_writer::{CachedDiskWriter, SeekableDiskWriter};
13use crate::filesystem::file_allocation;
14use crate::rate_limiter::RateLimiter;
15use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
16
17pub struct ConcurrentDownloadCommand {
18 group: Arc<tokio::sync::RwLock<RequestGroup>>,
19 client: reqwest::Client,
20 output_path: std::path::PathBuf,
21 started: bool,
22 completed_bytes: u64,
23 metalink_data: Vec<u8>,
24 max_connections_per_server: u16,
25 file_allocation: String,
26 secure_falloc: bool,
29 disk_cache_size_mb: Option<usize>,
30}
31
32impl ConcurrentDownloadCommand {
33 pub fn new(
34 gid: GroupId,
35 metalink_bytes: &[u8],
36 options: &DownloadOptions,
37 output_dir: Option<&str>,
38 ) -> Result<Self> {
39 let doc = aria2_protocol::metalink::parser::MetalinkDocument::parse(metalink_bytes)
40 .map_err(|e| {
41 Aria2Error::Fatal(FatalError::Config(format!("Metalink parse failed: {}", e)))
42 })?;
43
44 let file = doc
45 .single_file()
46 .ok_or_else(|| Aria2Error::Fatal(FatalError::Config("No file in Metalink".into())))?;
47
48 if file.urls.is_empty() {
49 return Err(Aria2Error::Fatal(FatalError::Config(
50 "No URLs in Metalink".into(),
51 )));
52 }
53
54 let dir = output_dir
55 .map(|d| d.to_string())
56 .or_else(|| options.dir.clone())
57 .unwrap_or_else(|| ".".to_string());
58
59 let path = std::path::PathBuf::from(&dir).join(&file.name);
60 let urls: Vec<String> = file
61 .get_sorted_urls()
62 .iter()
63 .map(|u| u.url.clone())
64 .collect();
65 let group = RequestGroup::new(gid, urls.clone(), options.clone());
66 let max_conn = options.max_connection_per_server.unwrap_or(2);
67
68 let client = reqwest::Client::builder()
69 .connect_timeout(Duration::from_secs(30))
70 .timeout(Duration::from_secs(300))
71 .user_agent("aria2-rust/0.1.0")
72 .redirect(reqwest::redirect::Policy::limited(5))
73 .build()
74 .map_err(|e| {
75 Aria2Error::Fatal(FatalError::Config(format!(
76 "HTTP client build failed: {}",
77 e
78 )))
79 })?;
80
81 let alloc = "prealloc".to_string();
82 let secure = options.secure_falloc;
83 let cache_mb: Option<usize> = None;
84
85 info!(
86 "ConcurrentDownloadCommand created: {} -> {} ({} mirrors)",
87 file.name,
88 path.display(),
89 urls.len()
90 );
91
92 Ok(Self {
93 group: Arc::new(tokio::sync::RwLock::new(group)),
94 client,
95 output_path: path,
96 started: false,
97 completed_bytes: 0,
98 metalink_data: metalink_bytes.to_vec(),
99 max_connections_per_server: max_conn,
100 file_allocation: alloc,
101 secure_falloc: secure,
102 disk_cache_size_mb: cache_mb,
103 })
104 }
105
106 pub async fn group(&self) -> tokio::sync::RwLockReadGuard<'_, RequestGroup> {
107 self.group.read().await
108 }
109}
110
111#[async_trait]
112impl Command for ConcurrentDownloadCommand {
113 async fn execute(&mut self) -> Result<()> {
114 if !self.started {
115 self.group.write().await.start().await?;
116 self.started = true;
117 }
118
119 let doc = aria2_protocol::metalink::parser::MetalinkDocument::parse(&self.metalink_data)
120 .map_err(|e| {
121 Aria2Error::Fatal(FatalError::Config(format!("Metalink parse error: {}", e)))
122 })?;
123
124 let file = doc
125 .single_file()
126 .ok_or_else(|| Aria2Error::Fatal(FatalError::Config("No available file".into())))?;
127
128 let sorted_urls = file.get_sorted_urls();
129 if sorted_urls.len() < 2 {
130 warn!(
131 "Concurrent download requires 2+ mirrors, got {}. Falling back to sequential.",
132 sorted_urls.len()
133 );
134 }
135
136 let urls: Vec<String> = sorted_urls.iter().map(|u| u.url.clone()).collect();
137 let expected_size = file.size;
138 let hash_entry = file.hashes.first().cloned();
139
140 if let Some(parent) = self.output_path.parent()
141 && !parent.exists()
142 {
143 std::fs::create_dir_all(parent).map_err(|e| {
144 Aria2Error::Fatal(FatalError::Config(format!("mkdir failed: {}", e)))
145 })?;
146 }
147
148 let original_path = self.output_path.clone();
150 self.output_path = global_registry().resolve(&original_path).await;
151 if self.output_path != original_path {
152 info!(
153 "ConcurrentDownloadCommand collision resolved: '{}' -> '{}'",
154 original_path.display(),
155 self.output_path.display()
156 );
157 }
158
159 let release_path = |path: &std::path::Path| {
161 let p = path.to_path_buf();
162 #[allow(clippy::let_underscore_future)]
163 let _ = tokio::spawn(async move {
164 global_registry().release(&p).await;
165 });
166 };
167
168 let mut manager = ConcurrentSegmentManager::new(expected_size.unwrap_or(0), urls, None);
169 manager.set_max_connections_per_mirror(self.max_connections_per_server as usize);
170
171 if manager.num_segments() == 0 && expected_size != Some(0) {
172 return Err(Aria2Error::Fatal(FatalError::Config(
173 "Cannot determine file size for segmentation".into(),
174 )));
175 }
176 if expected_size == Some(0) || manager.num_segments() == 0 {
177 return Ok(());
178 }
179
180 let total_len = expected_size.unwrap_or(0);
181 if total_len > 0 {
182 file_allocation::preallocate_file(
183 &self.output_path,
184 total_len,
185 &self.file_allocation,
186 self.secure_falloc,
187 )
188 .await?;
189 }
190
191 let num_pieces = manager.num_segments().max(1);
192 let ctrl_path = control_file::ControlFile::control_path_for(&self.output_path);
193 let mut ctrl_file =
194 control_file::ControlFile::open_or_create(&ctrl_path, total_len, num_pieces).await?;
195 ctrl_file.save().await.ok();
196
197 let mut writer =
198 CachedDiskWriter::new(&self.output_path, expected_size, self.disk_cache_size_mb);
199
200 let rate_limit = {
201 let g = self.group.read().await;
202 g.options().max_download_limit
203 };
204 let limiter = rate_limit.filter(|&r| r > 0).map(|r| {
205 use crate::rate_limiter::RateLimiterConfig;
206 RateLimiter::new(&RateLimiterConfig::new(Some(r), None))
207 });
208 if let Some(ref limiter) = limiter {
210 let g = self.group.read().await;
211 g.set_rate_limiter(limiter.clone()).await;
212 }
213
214 Self::download_concurrent_to_disk(
215 &self.client,
216 &mut manager,
217 &mut writer,
218 &mut ctrl_file,
219 limiter.as_ref(),
220 )
221 .await
222 .map_err(|e| {
223 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
224 message: format!("Concurrent download failed: {}", e),
225 })
226 })?;
227
228 writer
229 .flush()
230 .await
231 .map_err(|e| Aria2Error::Io(e.to_string()))?;
232
233 if let Some(ref hash) = hash_entry {
234 let file_data = writer
235 .read_all()
236 .await
237 .map_err(|e| Aria2Error::Io(e.to_string()))?;
238 if !Self::verify_hash(&file_data, hash)? {
239 release_path(&self.output_path);
240 return Err(Aria2Error::Recoverable(
241 RecoverableError::TemporaryNetworkFailure {
242 message: "Hash verification failed after concurrent download".into(),
243 },
244 ));
245 }
246 }
247
248 writer
249 .close()
250 .await
251 .map_err(|e| Aria2Error::Io(e.to_string()))?;
252
253 self.completed_bytes = total_len;
254
255 {
256 let mut g = self.group.write().await;
257 g.update_progress(self.completed_bytes).await;
258 g.update_speed(self.completed_bytes, 0).await;
259 g.complete().await?;
260 }
261
262 info!(
263 "Concurrent download done: {} ({} bytes from {} mirrors)",
264 self.output_path.display(),
265 self.completed_bytes,
266 manager.num_mirrors()
267 );
268 release_path(&self.output_path);
269 Ok(())
270 }
271
272 fn status(&self) -> CommandStatus {
273 if self.completed_bytes > 0 {
274 CommandStatus::Running
275 } else {
276 CommandStatus::Pending
277 }
278 }
279
280 fn timeout(&self) -> Option<Duration> {
281 Some(Duration::from_secs(600))
282 }
283}
284
285impl ConcurrentDownloadCommand {
286 async fn download_concurrent_to_disk(
287 client: &reqwest::Client,
288 manager: &mut ConcurrentSegmentManager,
289 writer: &mut CachedDiskWriter,
290 ctrl_file: &mut control_file::ControlFile,
291 limiter: Option<&RateLimiter>,
292 ) -> std::result::Result<(), String> {
293 manager.allocate_segments();
294
295 if manager.num_segments() <= 1 {
296 return Self::download_single_mirror_fallback_to_disk(client, manager, writer, limiter)
297 .await;
298 }
299
300 let mut iteration = 0u32;
301 loop {
302 let mut handles = Vec::new();
303
304 for mi in 0..manager.num_mirrors() {
305 while let Some((seg_idx, offset, length)) =
306 manager.next_pending_segment_for_mirror(mi)
307 {
308 let url = manager.mirror_url(mi).unwrap_or("").to_string();
309 let client_clone = client.clone();
310 let seg_idx_copy = seg_idx;
311
312 let handle = tokio::spawn(async move {
313 Self::download_single_segment(
314 &client_clone,
315 &url,
316 offset,
317 length,
318 seg_idx_copy,
319 )
320 .await
321 });
322 handles.push((mi, seg_idx, offset, handle));
323 }
324 }
325
326 if handles.is_empty() {
327 if manager.is_complete() {
328 break;
329 }
330 if manager.has_failed_segments() {
331 return Err("Some segments exhausted max retries".to_string());
332 }
333 if !manager.any_mirror_available() && manager.has_pending_segments() {
334 return Err("All mirrors unavailable with pending segments".to_string());
335 }
336 iteration += 1;
337 if iteration > 100 {
338 return Err("Download timed out in concurrent loop".to_string());
339 }
340 tokio::time::sleep(Duration::from_millis(50)).await;
341 continue;
342 }
343
344 for (mi, seg_idx, offset, handle) in handles {
345 match handle.await {
346 Ok(Ok(data)) => {
347 writer
348 .write_at(offset, &data)
349 .await
350 .map_err(|e| format!("Disk write error seg{}: {}", seg_idx, e))?;
351 manager.complete_segment(seg_idx, data);
352 ctrl_file.mark_piece_done(seg_idx as usize);
353 ctrl_file.save().await.ok();
354 }
355 Ok(Err(e)) => {
356 warn!("Segment {} from mirror {} failed: {}", seg_idx, mi, e);
357 manager.fail_segment(seg_idx);
358 }
359 Err(join_err) => {
360 warn!("Segment {} task panicked: {}", seg_idx, join_err);
361 manager.fail_segment(seg_idx);
362 }
363 }
364 }
365
366 if manager.is_complete() {
367 break;
368 }
369 tokio::time::sleep(Duration::from_millis(10)).await;
370 }
371
372 Ok(())
373 }
374
375 async fn download_single_segment(
376 client: &reqwest::Client,
377 url: &str,
378 offset: u64,
379 length: u64,
380 _seg_idx: u32,
381 ) -> std::result::Result<bytes::Bytes, String> {
382 let range_header = format!("bytes={}-{}", offset, offset + length.saturating_sub(1));
383
384 let response = client
385 .get(url)
386 .header("Range", &range_header)
387 .send()
388 .await
389 .map_err(|e| format!("HTTP Range request failed: {}", e))?;
390
391 let status = response.status();
392 if status.as_u16() >= 400 {
393 return Err(format!("HTTP error on segment: {}", status));
394 }
395
396 let mut data = bytes::BytesMut::with_capacity(length as usize);
398 let mut stream = response.bytes_stream();
399
400 while let Some(chunk_result) = stream.next().await {
401 match chunk_result {
402 Ok(bytes) => data.extend_from_slice(&bytes),
403 Err(e) => return Err(format!("Stream read error: {}", e)),
404 }
405 }
406
407 if data.is_empty() && length > 0 {
408 return Err("Empty segment data".to_string());
409 }
410
411 Ok(data.freeze())
413 }
414
415 fn verify_hash(
416 data: &[u8],
417 hash: &aria2_protocol::metalink::parser::HashEntry,
418 ) -> Result<bool> {
419 use aria2_protocol::metalink::parser::HashAlgorithm;
420 match hash.algo {
421 HashAlgorithm::Md5 => {
422 let digest = md5::compute(data);
423 Ok(format!("{:x}", digest) == hash.value)
424 }
425 HashAlgorithm::Sha1 => {
426 use sha1::Digest;
427 let mut hasher = sha1::Sha1::new();
428 hasher.update(data);
429 let result = hasher.finalize();
430 Ok(format!("{:x}", result) == hash.value)
431 }
432 HashAlgorithm::Sha256 => {
433 use sha2::Digest;
434 let mut hasher = sha2::Sha256::new();
435 hasher.update(data);
436 let result = hasher.finalize();
437 Ok(format!("{:x}", result) == hash.value)
438 }
439 HashAlgorithm::Sha512 => {
440 use sha2::Digest;
441 let mut hasher = sha2::Sha512::new();
442 hasher.update(data);
443 let result = hasher.finalize();
444 Ok(format!("{:x}", result) == hash.value)
445 }
446 }
447 }
448
449 async fn download_single_mirror_fallback_to_disk(
450 client: &reqwest::Client,
451 manager: &mut ConcurrentSegmentManager,
452 writer: &mut CachedDiskWriter,
453 limiter: Option<&RateLimiter>,
454 ) -> std::result::Result<(), String> {
455 for mi in 0..manager.num_mirrors() {
456 let url = match manager.mirror_url(mi) {
457 Some(u) if !u.is_empty() => u.to_string(),
458 _ => continue,
459 };
460
461 match client.get(&url).send().await {
462 Ok(response) => {
463 let status = response.status();
464 if status.is_success() || status.as_u16() == 206 {
465 let mut offset: u64 = 0;
466 let mut stream = response.bytes_stream();
467 while let Some(chunk_result) = stream.next().await {
468 match chunk_result {
469 Ok(bytes) => {
470 if let Some(lim) = limiter {
471 lim.acquire_download(bytes.len() as u64).await;
472 }
473 writer
474 .write_at(offset, &bytes)
475 .await
476 .map_err(|e| format!("Write error: {}", e))?;
477 offset += bytes.len() as u64;
478 }
479 Err(e) => return Err(format!("Stream read error: {}", e)),
480 }
481 }
482
483 manager.complete_segment(0, bytes::Bytes::new());
484 return Ok(());
485 }
486 }
487 Err(_) => continue,
488 }
489 }
490 Err("All mirrors failed for single-segment download".to_string())
491 }
492}