1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use std::sync::Arc;
use crate::{
DownloadResult, Error, RetryHandle,
nonblocking::imp::{
reqwest_client::ReqwestClient, tokio_file::TokioFile, tokio_system::TokioSystem,
tokio_token_bucket::TokioTokenBucket,
},
shared::{DownloadConfig, DownloadInner, LazyHead, config_proxy},
};
/// Represents a file about to be downloaded.
pub struct Download {
/// The client to use to download the file.
client: ReqwestClient,
/// Rate limiter.
limiter: Arc<TokioTokenBucket>,
/// How do we want to download this file?
config: DownloadConfig,
/// Information about the remote file, if we need to retrieve it.
head: LazyHead,
}
impl Download {
/// Create a new download for the given URL.
pub(crate) fn new(
client: ReqwestClient,
limiter: Arc<TokioTokenBucket>,
config: DownloadConfig,
) -> Self {
Download {
client,
limiter,
config,
head: LazyHead::default(),
}
}
// Expose functions to update self.config.
config_proxy! {}
/// Provide a callback to be called whenever a download is retried. This can
/// be used to customize the retry time, or abort the download. The default
/// is to use exponential backoff:
///
/// # Examples
///
/// ```no_run
/// # use std::time::Duration;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = downlowd::Client::new();
/// let result = client.get("https://example.com/file.txt")
/// .destination("file.txt")
/// .on_retry(|r| {
/// if matches!(r.error(), downlowd::Error::FileChanged { .. }) {
/// // No delay if the file changed.
/// r.set_delay(Duration::ZERO);
/// } else {
/// r.set_delay(downlowd::exponential_backoff(
/// Duration::from_secs(5),
/// Duration::from_secs(120),
/// r.retries(),
/// ));
/// }
/// })
/// .send()
/// .await?;
/// # Ok(())
/// # }
/// ```
///
/// This can also be used to abort the download on a retry by calling `r.cancel()`:
///
/// ```
/// # use std::time::Duration;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = downlowd::Client::new();
/// let result = client.get("http://localhost:8089/i_do_not_exist.txt")
/// .destination("file.txt")
/// .on_retry(|r| r.cancel())
/// .send()
/// .await;
///
/// assert!(matches!(result, Err(downlowd::Error::UnexpectedStatus { status: 404, .. })));
/// # Ok(())
/// # }
/// ```
///
pub fn on_retry(mut self, retry: impl FnMut(&mut RetryHandle) + Send + 'static) -> Self {
self.config.on_retry(retry);
self
}
/// Returns the filename that downlowd will use when downloading the file.
/// This is determined by making a HEAD request to the server, and looking
/// at the `Content-Disposition` header, if present, or falling back to the
/// last part of the URL path.
pub async fn get_remote_file_name(&mut self) -> &str {
// FIXME: If someone calls this, and then changes the headers or other parameters of this call,
// if might change the result of the `HEAD` request, which might invalidate our LazyHead.
self.head
.get(&self.client, &self.config.uri, &self.config.headers)
.await
.get_remote_file_name()
}
/// Send the download request to the server.
pub async fn send(self) -> Result<DownloadResult, Error> {
if let Some(e) = self.config.err {
return Err(e);
}
// Work out where we're ultimately going to save the file.
let destination = self.config.configured_destination()?;
let inner: DownloadInner<_, TokioFile, _> = DownloadInner::new(
self.client,
self.limiter,
self.config,
destination,
self.head,
)
.await?;
inner.download::<TokioSystem>().await
}
}