1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! Synchronous download queue bookkeeping: track download state, enforce a
//! concurrency budget, and surface which queued downloads may start next.
use std::path::PathBuf;
/// Tracks an active or completed download.
#[derive(Debug, Clone)]
pub struct ManagedDownload {
pub id: usize,
pub url: String,
pub dest: PathBuf,
pub mod_name: Option<String>,
pub state: DownloadState,
}
/// State of a managed download.
#[derive(Debug, Clone)]
pub enum DownloadState {
Queued,
Active { progress: f64 },
Paused { bytes: u64 },
Complete { path: PathBuf },
Failed { error: String },
}
/// Manages a queue of downloads with progress tracking.
///
/// This is a synchronous data structure -- callers drive the actual async
/// downloads and update state through the provided methods.
pub struct DownloadManager {
downloads: Vec<ManagedDownload>,
max_concurrent: usize,
next_id: usize,
}
impl DownloadManager {
/// Create an empty manager allowing up to `max_concurrent` active downloads.
#[must_use]
pub fn new(max_concurrent: usize) -> Self {
Self {
downloads: Vec::new(),
max_concurrent,
next_id: 0,
}
}
/// Add a download to the queue. Returns the assigned download ID.
pub fn enqueue(&mut self, url: String, dest: PathBuf, mod_name: Option<String>) -> usize {
let id = self.next_id;
self.next_id += 1;
self.downloads.push(ManagedDownload {
id,
url,
dest,
mod_name,
state: DownloadState::Queued,
});
id
}
/// Pause an active download, recording how many bytes were fetched so far.
pub fn pause(&mut self, id: usize, bytes_so_far: u64) {
if let Some(dl) = self.get_mut(id)
&& matches!(dl.state, DownloadState::Active { .. })
{
dl.state = DownloadState::Paused {
bytes: bytes_so_far,
};
}
}
/// Move a paused download back to the queue.
pub fn resume(&mut self, id: usize) {
if let Some(dl) = self.get_mut(id)
&& matches!(dl.state, DownloadState::Paused { .. })
{
dl.state = DownloadState::Queued;
}
}
/// Remove a download from the manager entirely.
pub fn cancel(&mut self, id: usize) {
self.downloads.retain(|dl| dl.id != id);
}
/// Number of currently active downloads.
#[must_use]
pub fn active_count(&self) -> usize {
self.downloads
.iter()
.filter(|dl| matches!(dl.state, DownloadState::Active { .. }))
.count()
}
/// Maximum number of concurrent downloads allowed.
#[must_use]
pub fn max_concurrent(&self) -> usize {
self.max_concurrent
}
/// View all tracked downloads.
#[must_use]
pub fn all(&self) -> &[ManagedDownload] {
&self.downloads
}
/// Get a mutable reference to a download by ID.
pub fn get_mut(&mut self, id: usize) -> Option<&mut ManagedDownload> {
self.downloads.iter_mut().find(|dl| dl.id == id)
}
/// Get an immutable reference to a download by ID.
#[must_use]
pub fn get(&self, id: usize) -> Option<&ManagedDownload> {
self.downloads.iter().find(|dl| dl.id == id)
}
/// Returns true if there is room to start another download.
#[must_use]
pub fn can_start_more(&self) -> bool {
self.active_count() < self.max_concurrent
}
/// Return IDs of queued downloads that could be activated, up to the
/// remaining concurrency budget.
#[must_use]
pub fn next_queued(&self) -> Vec<usize> {
let budget = self.max_concurrent.saturating_sub(self.active_count());
self.downloads
.iter()
.filter(|dl| matches!(dl.state, DownloadState::Queued))
.take(budget)
.map(|dl| dl.id)
.collect()
}
}