1use std::{cmp::Ordering, path::PathBuf, time::Duration};
2
3use bon::{Builder, builder};
4use checksum::Checksum;
5use download::{EmptySource, SingleDownloader, SuccessSummary};
6use futures::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(
207 &self,
208 callback: impl AsyncFn(Event),
209 ) -> Result<Summary, EmptySource> {
210 let mut tasks = Vec::new();
211 let mut list = vec![];
212 for (i, c) in self.download_list.iter().enumerate() {
213 let msg = c.msg.clone();
214 let single = SingleDownloader::builder()
215 .client(self.client)
216 .maybe_msg(msg)
217 .download_list_index(i)
218 .entry(c)?
219 .progress((i + 1, self.download_list.len()))
220 .retry_times(self.retry_times)
221 .file_type(c.file_type)
222 .maybe_set_permission(self.set_permission)
223 .timeout(self.timeout)
224 .build();
225
226 list.push(single);
227 }
228
229 for single in list {
230 tasks.push(single.try_download(&callback));
231 }
232
233 if self.total_size != 0 {
234 callback(Event::NewGlobalProgressBar(self.total_size)).await;
235 }
236
237 let stream = futures::stream::iter(tasks).buffer_unordered(self.threads);
238 let res = stream.collect::<Vec<_>>().await;
239 callback(Event::AllDone).await;
240
241 let (mut success, mut failed) = (vec![], vec![]);
242
243 for i in res {
244 match i {
245 download::DownloadResult::Success(success_summary) => {
246 success.push(success_summary);
247 }
248 download::DownloadResult::Failed { file_name } => {
249 failed.push(file_name);
250 }
251 }
252 }
253
254 Ok(Summary { success, failed })
255 }
256}
257
258pub fn build_request_with_basic_auth(
259 client: &Client,
260 method: Method,
261 auth: &Option<(String, String)>,
262 url: &str,
263) -> RequestBuilder {
264 let mut req = client.request(method, url);
265
266 if let Some((user, password)) = auth {
267 debug!("auth user: {}", user);
268 req = req.basic_auth(user, Some(password));
269 }
270
271 req
272}