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::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(
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}