Skip to main content

aria2_core/engine/
concurrent_download_command.rs

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    /// When `true`, zero-fill allocated blocks after `fallocate` on platforms
27    /// that don't zero-fill (macOS, Windows). See `file_allocation::fallocate`.
28    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        // Resolve filename collision against other active downloads.
149        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        // Ensure the resolved path is released when execute() returns.
160        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        // Register clone with RequestGroup for runtime rate updates
209        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        // Use BytesMut for efficient stream accumulation
397        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        // Freeze to immutable Bytes (zero-cost)
412        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}