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}