1use std::{cmp::Ordering, path::PathBuf, time::Duration};
2
3use bon::{Builder, builder};
4use checksum::Checksum;
5use download::{EmptySource, SingleDownloader, SuccessSummary};
6use futures::{Future, StreamExt};
7
8use reqwest::{Client, Method, RequestBuilder};
9use tracing::debug;
10
11pub mod checksum;
12mod download;
13pub use crate::download::SingleDownloadError;
14
15pub use reqwest;
16
17#[derive(Debug, Clone, Default, Builder)]
18pub struct DownloadEntry {
19 pub source: Vec<DownloadSource>,
20 pub filename: String,
21 dir: PathBuf,
22 hash: Option<Checksum>,
23 allow_resume: bool,
24 msg: Option<String>,
25 #[builder(default)]
26 file_type: CompressFile,
27}
28
29#[derive(Debug, Clone, Default, PartialEq, Eq, Copy)]
30pub enum CompressFile {
31 Bz2,
32 Gzip,
33 Xz,
34 Zstd,
35 #[default]
36 Nothing,
37}
38
39impl Ord for CompressFile {
41 fn cmp(&self, other: &Self) -> Ordering {
42 match self {
43 CompressFile::Bz2 => match other {
44 CompressFile::Bz2 => Ordering::Equal,
45 CompressFile::Gzip => Ordering::Less,
46 CompressFile::Xz => Ordering::Less,
47 CompressFile::Zstd => Ordering::Less,
48 CompressFile::Nothing => Ordering::Greater,
49 },
50 CompressFile::Gzip => match other {
51 CompressFile::Bz2 => Ordering::Greater,
52 CompressFile::Gzip => Ordering::Less,
53 CompressFile::Xz => Ordering::Less,
54 CompressFile::Zstd => Ordering::Less,
55 CompressFile::Nothing => Ordering::Greater,
56 },
57 CompressFile::Xz => match other {
58 CompressFile::Bz2 => Ordering::Greater,
59 CompressFile::Gzip => Ordering::Greater,
60 CompressFile::Xz => Ordering::Equal,
61 CompressFile::Zstd => Ordering::Less,
62 CompressFile::Nothing => Ordering::Greater,
63 },
64 CompressFile::Zstd => match other {
65 CompressFile::Bz2 => Ordering::Greater,
66 CompressFile::Gzip => Ordering::Greater,
67 CompressFile::Xz => Ordering::Greater,
68 CompressFile::Zstd => Ordering::Equal,
69 CompressFile::Nothing => Ordering::Greater,
70 },
71 CompressFile::Nothing => match other {
72 CompressFile::Bz2 => Ordering::Less,
73 CompressFile::Gzip => Ordering::Less,
74 CompressFile::Xz => Ordering::Less,
75 CompressFile::Zstd => Ordering::Less,
76 CompressFile::Nothing => Ordering::Equal,
77 },
78 }
79 }
80}
81
82impl PartialOrd for CompressFile {
83 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
84 Some(self.cmp(other))
85 }
86}
87
88impl From<&str> for CompressFile {
89 fn from(s: &str) -> Self {
90 match s {
91 "xz" => CompressFile::Xz,
92 "gz" => CompressFile::Gzip,
93 "bz2" => CompressFile::Bz2,
94 "zst" => CompressFile::Zstd,
95 _ => CompressFile::Nothing,
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
101pub struct DownloadSource {
102 pub url: String,
103 pub source_type: DownloadSourceType,
104}
105
106#[derive(Debug, PartialEq, Eq, Clone)]
107pub enum DownloadSourceType {
108 Http { auth: Option<(String, String)> },
109 Local(bool),
110}
111
112impl PartialOrd for DownloadSourceType {
113 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
114 Some(self.cmp(other))
115 }
116}
117
118impl Ord for DownloadSourceType {
119 fn cmp(&self, other: &Self) -> Ordering {
120 match self {
121 DownloadSourceType::Http { .. } => match other {
122 DownloadSourceType::Http { .. } => Ordering::Equal,
123 DownloadSourceType::Local { .. } => Ordering::Less,
124 },
125 DownloadSourceType::Local { .. } => match other {
126 DownloadSourceType::Http { .. } => Ordering::Greater,
127 DownloadSourceType::Local { .. } => Ordering::Equal,
128 },
129 }
130 }
131}
132
133#[derive(Debug)]
134pub enum Event {
135 ChecksumMismatch {
136 index: usize,
137 filename: String,
138 times: usize,
139 },
140 GlobalProgressAdd(u64),
141 GlobalProgressSub(u64),
142 ProgressDone(usize),
143 NewProgressSpinner {
144 index: usize,
145 msg: String,
146 },
147 NewProgressBar {
148 index: usize,
149 msg: String,
150 size: u64,
151 },
152 ProgressInc {
153 index: usize,
154 size: u64,
155 },
156 NextUrl {
157 index: usize,
158 file_name: String,
159 err: SingleDownloadError,
160 },
161 DownloadDone {
162 index: usize,
163 msg: Box<str>,
164 },
165 Failed {
166 file_name: String,
167 error: SingleDownloadError,
168 },
169 AllDone,
170 NewGlobalProgressBar(u64),
171}
172
173#[derive(Builder)]
174pub struct DownloadManager<'a> {
175 client: &'a Client,
176 download_list: &'a [DownloadEntry],
177 #[builder(default = 4)]
178 threads: usize,
179 #[builder(default = 3)]
180 retry_times: usize,
181 #[builder(default)]
182 total_size: u64,
183 set_permission: Option<u32>,
184 #[builder(default = Duration::from_secs(15))]
185 timeout: Duration,
186}
187
188#[derive(Debug)]
189pub struct Summary {
190 pub success: Vec<SuccessSummary>,
191 pub failed: Vec<String>,
192}
193
194impl Summary {
195 pub fn is_download_success(&self) -> bool {
196 self.failed.is_empty()
197 }
198
199 pub fn has_wrote(&self) -> bool {
200 self.success.iter().any(|x| x.wrote)
201 }
202}
203
204impl DownloadManager<'_> {
205 pub async fn start_download<F, Fut>(&self, callback: F) -> Result<Summary, EmptySource>
207 where
208 F: Fn(Event) -> Fut,
209 Fut: Future<Output = ()>,
210 {
211 let mut tasks = Vec::new();
212 let mut list = vec![];
213 for (i, c) in self.download_list.iter().enumerate() {
214 let msg = c.msg.clone();
215 let single = SingleDownloader::builder()
216 .client(self.client)
217 .maybe_msg(msg)
218 .download_list_index(i)
219 .entry(c)?
220 .progress((i + 1, self.download_list.len()))
221 .retry_times(self.retry_times)
222 .file_type(c.file_type)
223 .maybe_set_permission(self.set_permission)
224 .timeout(self.timeout)
225 .build();
226
227 list.push(single);
228 }
229
230 for single in list {
231 tasks.push(single.try_download(&callback));
232 }
233
234 if self.total_size != 0 {
235 callback(Event::NewGlobalProgressBar(self.total_size)).await;
236 }
237
238 let stream = futures::stream::iter(tasks).buffer_unordered(self.threads);
239 let res = stream.collect::<Vec<_>>().await;
240 callback(Event::AllDone).await;
241
242 let (mut success, mut failed) = (vec![], vec![]);
243
244 for i in res {
245 match i {
246 download::DownloadResult::Success(success_summary) => {
247 success.push(success_summary);
248 }
249 download::DownloadResult::Failed { file_name } => {
250 failed.push(file_name);
251 }
252 }
253 }
254
255 Ok(Summary { success, failed })
256 }
257}
258
259pub fn build_request_with_basic_auth(
260 client: &Client,
261 method: Method,
262 auth: &Option<(String, String)>,
263 url: &str,
264) -> RequestBuilder {
265 let mut req = client.request(method, url);
266
267 if let Some((user, password)) = auth {
268 debug!("auth user: {}", user);
269 req = req.basic_auth(user, Some(password));
270 }
271
272 req
273}