1use crate::Error;
4use reqwest::{
5 header::{ACCEPT_RANGES, CONTENT_LENGTH},
6 StatusCode, Url,
7};
8use reqwest_middleware::ClientWithMiddleware;
9use std::convert::TryFrom;
10
11#[derive(Debug, Clone)]
13pub struct Download {
14 pub url: Url,
16 pub filename: String,
18}
19
20impl Download {
21 pub fn new(url: &Url, filename: &str) -> Self {
43 Self {
44 url: url.clone(),
45 filename: String::from(filename),
46 }
47 }
48
49 pub async fn is_resumable(
51 &self,
52 client: &ClientWithMiddleware,
53 ) -> Result<bool, reqwest_middleware::Error> {
54 let res = client.head(self.url.clone()).send().await?;
55 let headers = res.headers();
56 match headers.get(ACCEPT_RANGES) {
57 None => Ok(false),
58 Some(x) if x == "none" => Ok(false),
59 Some(_) => Ok(true),
60 }
61 }
62
63 pub async fn content_length(
68 &self,
69 client: &ClientWithMiddleware,
70 ) -> Result<Option<u64>, reqwest_middleware::Error> {
71 let res = client.head(self.url.clone()).send().await?;
72 let headers = res.headers();
73 match headers.get(CONTENT_LENGTH) {
74 None => Ok(None),
75 Some(header_value) => match header_value.to_str() {
76 Ok(v) => match v.to_string().parse::<u64>() {
77 Ok(v) => Ok(Some(v)),
78 Err(_) => Ok(None),
79 },
80 Err(_) => Ok(None),
81 },
82 }
83 }
84}
85
86impl TryFrom<&Url> for Download {
87 type Error = crate::Error;
88
89 fn try_from(value: &Url) -> Result<Self, Self::Error> {
90 value
91 .path_segments()
92 .ok_or_else(|| {
93 Error::InvalidUrl(format!("the url \"{value}\" does not contain a valid path"))
94 })?
95 .next_back()
96 .map(String::from)
97 .map(|filename| Download {
98 url: value.clone(),
99 filename: form_urlencoded::parse(filename.as_bytes())
100 .map(|(key, val)| [key, val].concat())
101 .collect(),
102 })
103 .ok_or_else(|| {
104 Error::InvalidUrl(format!("the url \"{value}\" does not contain a filename"))
105 })
106 }
107}
108
109impl TryFrom<&str> for Download {
110 type Error = crate::Error;
111
112 fn try_from(value: &str) -> Result<Self, Self::Error> {
113 Url::parse(value)
114 .map_err(|e| Error::InvalidUrl(format!("the url \"{value}\" cannot be parsed: {e}")))
115 .and_then(|u| Download::try_from(&u))
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum Status {
121 Fail(String),
122 NotStarted,
123 Skipped(String),
124 Success,
125}
126#[derive(Debug, Clone)]
128pub struct Summary {
129 download: Download,
131 statuscode: StatusCode,
133 size: u64,
135 status: Status,
137 resumable: bool,
139}
140
141impl Summary {
142 pub fn new(download: Download, statuscode: StatusCode, size: u64, resumable: bool) -> Self {
144 Self {
145 download,
146 statuscode,
147 size,
148 status: Status::NotStarted,
149 resumable,
150 }
151 }
152
153 pub fn with_status(self, status: Status) -> Self {
155 Self { status, ..self }
156 }
157
158 pub fn statuscode(&self) -> StatusCode {
160 self.statuscode
161 }
162
163 pub fn size(&self) -> u64 {
165 self.size
166 }
167
168 pub fn download(&self) -> &Download {
170 &self.download
171 }
172
173 pub fn status(&self) -> &Status {
175 &self.status
176 }
177
178 pub fn fail(self, msg: impl std::fmt::Display) -> Self {
179 Self {
180 status: Status::Fail(format!("{msg}")),
181 ..self
182 }
183 }
184
185 pub fn set_resumable(&mut self, resumable: bool) {
187 self.resumable = resumable;
188 }
189
190 #[must_use]
192 pub fn resumable(&self) -> bool {
193 self.resumable
194 }
195}
196
197#[cfg(test)]
198mod test {
199 use super::*;
200
201 const DOMAIN: &str = "http://domain.com/file.zip";
202
203 #[test]
204 fn test_try_from_url() {
205 let u = Url::parse(DOMAIN).unwrap();
206 let d = Download::try_from(&u).unwrap();
207 assert_eq!(d.filename, "file.zip")
208 }
209
210 #[test]
211 fn test_try_from_string() {
212 let d = Download::try_from(DOMAIN).unwrap();
213 assert_eq!(d.filename, "file.zip")
214 }
215}