ml_downloader/lib.rs
1#![doc = include_str!(concat!(env!("OUT_DIR"), "/README-rustdocified.md"))]
2#![deny(missing_docs)]
3#![forbid(unsafe_code)]
4
5use std::{
6 error::Error as StdError,
7 fmt, thread,
8 time::{Duration, Instant},
9};
10
11use bytes::Bytes;
12use digest::DynDigest;
13use reqwest::{
14 blocking::{Client, ClientBuilder},
15 Error as ReqwestError, IntoUrl, StatusCode,
16};
17
18// ======================================================================
19// Error - PUBLIC
20
21/// Represents all possible errors that can occur in this library.
22#[derive(Debug)]
23pub enum Error {
24 /// Got error from [reqwest](https://crates.io/crates/reqwest).
25 Reqwest(
26 /// The error.
27 ReqwestError,
28 ),
29
30 /// HTTP response status is not `OK` (200).
31 StatusNotOk(
32 /// HTTP response status.
33 StatusCode,
34 ),
35
36 /// Hash of downloaded file doesn't match.
37 HashMismatch {
38 /// Hash of downloaded file, lowercase hexadecimal.
39 got: String,
40 /// Hash given to [`RequestBuilder::hash`], lowercase hexadecimal.
41 expected: String,
42 },
43
44 /// Download failed.
45 DownloadFailed(
46 /// Errors, one error for each (re)try.
47 Vec<Error>,
48 ),
49}
50
51// ======================================================================
52// Error - IMPL DISPLAY
53
54impl fmt::Display for Error {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Error::Reqwest(inner) => inner.fmt(f),
58 Error::StatusNotOk(status) => status.fmt(f),
59 Error::HashMismatch { got, expected } => {
60 write!(f, "hash mismatch\nGot :{}\nExpected:{}", got, expected)
61 }
62 Error::DownloadFailed(errors) => {
63 write!(f, "download failed:")?;
64 for (index, error) in errors.iter().enumerate() {
65 write!(f, "\n[{}]: {}", index, error)?;
66 }
67 Ok(())
68 }
69 }
70 }
71}
72
73// ======================================================================
74// Error - IMPL ERROR
75
76impl StdError for Error {}
77
78// ======================================================================
79// Error - IMPL FROM
80
81impl From<ReqwestError> for Error {
82 fn from(error: ReqwestError) -> Self {
83 Self::Reqwest(error)
84 }
85}
86
87// ======================================================================
88// Downloader - PUBLIC
89
90/// Simple blocking downloader.
91///
92/// See [crate index](crate#examples) for examples.
93pub struct Downloader {
94 client: Client,
95 min_delay: Duration,
96 max_delay: Duration,
97 min_interval: Duration,
98 max_interval: Duration,
99 retry_delays: Vec<(Duration, Duration)>,
100 sleep_until: Instant,
101}
102
103impl Downloader {
104 /// Creates [`DownloaderBuilder`] to configure [`Downloader`].
105 ///
106 /// This is same as [`DownloaderBuilder::new`].
107 ///
108 /// See [custom configuration] for an example.
109 ///
110 /// [custom configuration]: crate#custom-configuration
111 pub fn builder() -> DownloaderBuilder {
112 DownloaderBuilder::new()
113 }
114
115 /// Begins building a request to download file from given `url`.
116 ///
117 /// See [simple usage] and [`RequestBuilder::hash`] for examples.
118 ///
119 /// # Errors
120 ///
121 /// If given `url` is invalid then [`RequestBuilder::send`] will fail.
122 ///
123 /// [simple usage]: crate#simple-usage
124 pub fn get<U: IntoUrl>(&mut self, url: U) -> RequestBuilder<'_> {
125 RequestBuilder::new(self, self.client.get(url))
126 }
127
128 /// Creates new [`Downloader`] with default configuration.
129 pub fn new() -> Result<Self, Error> {
130 DownloaderBuilder::new().build()
131 }
132
133 /// Sleeps until ready for next download.
134 ///
135 /// After this the next [`RequestBuilder::send`] will start
136 /// download immediately without sleep.
137 ///
138 /// See [`DownloaderBuilder::delay`] and [`DownloaderBuilder::interval`].
139 ///
140 /// # Examples
141 ///
142 /// ```no_run
143 /// use ml_downloader::Downloader;
144 ///
145 /// let mut downloader = Downloader::builder()
146 /// .interval(1.0, 1.0)
147 /// .build()?;
148 ///
149 /// println!("First download");
150 /// let bytes1 = downloader.get("https://example.com/first").send()?;
151 /// downloader.sleep_until_ready();
152 /// println!("Second download");
153 /// let bytes2 = downloader.get("https://example.com/second").send()?;
154 ///
155 /// # Ok::<(), ml_downloader::Error>(())
156 /// ```
157 pub fn sleep_until_ready(&mut self) {
158 let now = Instant::now();
159 if self.sleep_until > now {
160 std::thread::sleep(self.sleep_until - now);
161 }
162 }
163}
164
165// ======================================================================
166// DownloaderBuilder - PUBLIC
167
168/// A builder to create [`Downloader`] with custom configuration.
169///
170/// See [custom configuration] for an example.
171///
172/// [custom configuration]: crate#custom-configuration
173pub struct DownloaderBuilder {
174 client_builder: ClientBuilder,
175 min_delay: Duration,
176 max_delay: Duration,
177 min_interval: Duration,
178 max_interval: Duration,
179 retry_delays: Vec<(Duration, Duration)>,
180}
181
182impl Default for DownloaderBuilder {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188impl DownloaderBuilder {
189 /// Creates [`Downloader`] using configuration of this [`DownloaderBuilder`].
190 ///
191 /// See [custom configuration] for an example.
192 ///
193 /// [custom configuration]: crate#custom-configuration
194 pub fn build(self) -> Result<Downloader, Error> {
195 Ok(Downloader {
196 client: self.client_builder.build()?,
197 min_interval: self.min_interval,
198 max_interval: self.max_interval,
199 min_delay: self.min_delay,
200 max_delay: self.max_delay,
201 retry_delays: self.retry_delays,
202 sleep_until: Instant::now(),
203 })
204 }
205
206 /// Sets delay between successful downloads in seconds, default is 0.
207 ///
208 /// A random delay between given `min` and `max` is generated
209 /// for each download. If elapsed time since previous download ended
210 /// is less than this delay then [`RequestBuilder::send`] will sleep
211 /// for the remaining duration before starting download.
212 ///
213 /// See also [`DownloaderBuilder::interval`].
214 ///
215 /// # Panics
216 ///
217 /// If `min > max`.
218 ///
219 /// # Examples
220 ///
221 /// Configure `1.0 - 1.1` seconds delay between successful downloads.
222 ///
223 /// ```rust
224 /// use ml_downloader::Downloader;
225 ///
226 /// let mut downloader = Downloader::builder()
227 /// .delay(1.0, 1.1)
228 /// .build()?;
229 ///
230 /// # Ok::<(), ml_downloader::Error>(())
231 /// ```
232 pub fn delay(self, min: f32, max: f32) -> Self {
233 assert!(min <= max);
234 DownloaderBuilder {
235 min_delay: Duration::from_secs_f32(min),
236 max_delay: Duration::from_secs_f32(max),
237 ..self
238 }
239 }
240
241 /// Sets interval between successful downloads in seconds, default is 0.
242 ///
243 /// A random interval between given `min` and `max` is generated
244 /// for each download. If elapsed time since previous download started
245 /// is less than this interval then [`RequestBuilder::send`] will sleep
246 /// for the remaining duration before starting download.
247 ///
248 /// See also [`DownloaderBuilder::delay`].
249 ///
250 /// # Panics
251 ///
252 /// If `min > max`.
253 ///
254 /// # Examples
255 ///
256 /// Configure `1.0 - 1.1` seconds interval between successful downloads.
257 ///
258 /// ```rust
259 /// use ml_downloader::Downloader;
260 ///
261 /// let mut downloader = Downloader::builder()
262 /// .interval(1.0, 1.1)
263 /// .build()?;
264 ///
265 /// # Ok::<(), ml_downloader::Error>(())
266 /// ```
267 pub fn interval(self, min: f32, max: f32) -> Self {
268 assert!(min <= max);
269 DownloaderBuilder {
270 min_interval: Duration::from_secs_f32(min),
271 max_interval: Duration::from_secs_f32(max),
272 ..self
273 }
274 }
275
276 /// Creates [`DownloaderBuilder`] to configure [`Downloader`].
277 ///
278 /// This is same as [`Downloader::builder`].
279 pub fn new() -> Self {
280 Self {
281 client_builder: Client::builder(),
282 min_delay: Duration::ZERO,
283 max_delay: Duration::ZERO,
284 min_interval: Duration::ZERO,
285 max_interval: Duration::ZERO,
286 retry_delays: Vec::new(),
287 }
288 }
289
290 /// Configures underlying [`ClientBuilder`].
291 ///
292 /// # Examples
293 ///
294 /// ```rust
295 /// use ml_downloader::Downloader;
296 ///
297 /// let mut downloader = Downloader::builder()
298 /// .reqwest(|cb| cb.user_agent("foobar/1.0"))
299 /// .build()?;
300 ///
301 /// # Ok::<(), ml_downloader::Error>(())
302 /// ```
303 ///
304 /// [`ClientBuilder`]: reqwest::blocking::ClientBuilder
305 pub fn reqwest<F>(self, f: F) -> Self
306 where
307 F: FnOnce(ClientBuilder) -> ClientBuilder,
308 {
309 DownloaderBuilder {
310 client_builder: f(self.client_builder),
311 ..self
312 }
313 }
314
315 /// Sets retry delays in seconds, default is none.
316 ///
317 /// Each item is a pair of `min` and `max` delays
318 /// and the number of items defines the number of retries.
319 ///
320 /// A random delay between given `min` and `max` is generated for each retry.
321 ///
322 /// # Panics
323 ///
324 /// If any item has `min > max`.
325 ///
326 /// # Examples
327 ///
328 /// Configure two retries after failed download with
329 /// `2.0 - 2.2` seconds delay after initial failure and
330 /// `5.0 - 5.5` seconds delay after 2nd failure.
331 ///
332 /// ```rust
333 /// use ml_downloader::Downloader;
334 ///
335 /// let mut downloader = Downloader::builder()
336 /// .retry_delays(&[(2.0, 2.2), (5.0, 5.5)])
337 /// .build()?;
338 ///
339 /// # Ok::<(), ml_downloader::Error>(())
340 /// ```
341 pub fn retry_delays(self, retry_delays: &[(f32, f32)]) -> Self {
342 let mut vec = Vec::with_capacity(retry_delays.len());
343 for (min, max) in retry_delays {
344 assert!(min <= max);
345 vec.push((Duration::from_secs_f32(*min), Duration::from_secs_f32(*max)));
346 }
347
348 DownloaderBuilder {
349 retry_delays: vec,
350 ..self
351 }
352 }
353}
354
355// ======================================================================
356// RequestBuilder - PUBLIC
357
358/// A builder to configure download request.
359///
360/// See [custom configuration] for an example.
361///
362/// [custom configuration]: crate#custom-configuration
363pub struct RequestBuilder<'a> {
364 downloader: &'a mut Downloader,
365 inner: reqwest::blocking::RequestBuilder,
366 hash: Option<(String, Box<dyn DynDigest>)>,
367}
368
369impl<'a> RequestBuilder<'a> {
370 /// Sets expected file hash and digest used to calculate it.
371 ///
372 /// Hash is given in hexadecimal, uppercase or lowercase.
373 ///
374 /// # Examples
375 ///
376 /// ```no_run
377 /// use ml_downloader::Downloader;
378 /// use sha2::{Digest, Sha256};
379 ///
380 /// let mut downloader = Downloader::new()?;
381 /// let bytes = downloader
382 /// .get("https://example.com/")
383 /// .hash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Sha256::new())
384 /// .send()?;
385 ///
386 /// # Ok::<(), ml_downloader::Error>(())
387 /// ```
388 pub fn hash<D: DynDigest + 'static>(self, expected: &str, digest: D) -> Self {
389 RequestBuilder {
390 hash: Some((expected.to_lowercase(), Box::new(digest))),
391 ..self
392 }
393 }
394
395 /// Creates download request and sends it to target URL, with retries.
396 ///
397 /// - Sleeps before starting download if needed.
398 /// - See [`DownloaderBuilder::interval`] and [`Downloader::sleep_until_ready`].
399 /// - Number of retries and the delays inbetween them is configured with
400 /// [`DownloaderBuilder::retry_delays`].
401 ///
402 /// See [simple usage] and [`RequestBuilder::hash`] for examples.
403 ///
404 /// [simple usage]: crate#simple-usage
405 pub fn send(mut self) -> Result<Bytes, Error> {
406 let request = self.inner.build()?;
407
408 let mut errors = Vec::with_capacity(self.downloader.retry_delays.len());
409
410 self.downloader.sleep_until_ready();
411
412 let delay = random_duration(self.downloader.min_delay, self.downloader.max_delay);
413 let interval = random_duration(self.downloader.min_interval, self.downloader.max_interval);
414
415 let mut retry_count = 0;
416 loop {
417 let start = Instant::now();
418
419 // `try_clone` can return `None` only if body isn't clonable,
420 // but this code never sets body, so this `unwrap` can't fail.
421 match RequestBuilder::send_once(
422 &self.downloader.client,
423 &mut self.hash,
424 request.try_clone().unwrap(),
425 ) {
426 Ok(bytes) => {
427 let end = Instant::now();
428 self.downloader.sleep_until = (start + interval).max(end + delay);
429 return Ok(bytes);
430 }
431 Err(error) => errors.push(error),
432 }
433
434 if retry_count == self.downloader.retry_delays.len() {
435 return Err(Error::DownloadFailed(errors));
436 }
437
438 let (min, max) = self.downloader.retry_delays[retry_count];
439 thread::sleep(random_duration(min, max));
440 retry_count += 1;
441 }
442 }
443}
444
445// ======================================================================
446// RequestBuilder - PRIVATE
447
448impl<'a> RequestBuilder<'a> {
449 fn new(downloader: &'a mut Downloader, inner: reqwest::blocking::RequestBuilder) -> Self {
450 Self {
451 downloader,
452 inner,
453 hash: None,
454 }
455 }
456
457 fn send_once(
458 client: &Client,
459 hash: &mut Option<(String, Box<dyn DynDigest>)>,
460 request: reqwest::blocking::Request,
461 ) -> Result<Bytes, Error> {
462 let response = client.execute(request)?;
463 let status = response.status();
464
465 if status != StatusCode::OK {
466 Err(Error::StatusNotOk(status))
467 } else {
468 let bytes = response.bytes()?;
469 if let Some((expected, digest)) = hash {
470 digest.reset();
471 digest.update(&bytes);
472 let mut got = vec![0; digest.output_size()];
473 digest.finalize_into_reset(got.as_mut()).unwrap();
474 let got = hex::encode(got);
475
476 if &got != expected {
477 return Err(Error::HashMismatch {
478 got,
479 expected: expected.clone(),
480 });
481 }
482 }
483 Ok(bytes)
484 }
485 }
486}
487
488// ======================================================================
489// FUNCTIONS - PRIVATE
490
491fn random_duration(min: Duration, max: Duration) -> Duration {
492 Duration::from_micros(fastrand::u64(
493 min.as_micros() as u64..=max.as_micros() as u64,
494 ))
495}