oma_fetch/
lib.rs

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
39// 压缩文件下载顺序:Zstd -> XZ -> Gzip -> Bz2 -> 未压缩
40impl 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    /// Start download
206    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}