use crate::Error;
use reqwest::{
header::{ACCEPT_RANGES, CONTENT_LENGTH},
StatusCode, Url,
};
use reqwest_middleware::ClientWithMiddleware;
use std::convert::TryFrom;
#[derive(Debug, Clone)]
pub struct Download {
pub url: Url,
pub filename: String,
}
impl Download {
pub fn new(url: &Url, filename: &str) -> Self {
Self {
url: url.clone(),
filename: String::from(filename),
}
}
pub async fn is_resumable(
&self,
client: &ClientWithMiddleware,
) -> Result<bool, reqwest_middleware::Error> {
let res = client.head(self.url.clone()).send().await?;
let headers = res.headers();
match headers.get(ACCEPT_RANGES) {
None => Ok(false),
Some(x) if x == "none" => Ok(false),
Some(_) => Ok(true),
}
}
pub async fn content_length(
&self,
client: &ClientWithMiddleware,
) -> Result<Option<u64>, reqwest_middleware::Error> {
let res = client.head(self.url.clone()).send().await?;
let headers = res.headers();
match headers.get(CONTENT_LENGTH) {
None => Ok(None),
Some(header_value) => match header_value.to_str() {
Ok(v) => match v.to_string().parse::<u64>() {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
},
Err(_) => Ok(None),
},
}
}
}
impl TryFrom<&Url> for Download {
type Error = crate::Error;
fn try_from(value: &Url) -> Result<Self, Self::Error> {
value
.path_segments()
.ok_or_else(|| {
Error::InvalidUrl(format!("the url \"{value}\" does not contain a valid path"))
})?
.next_back()
.map(String::from)
.map(|filename| Download {
url: value.clone(),
filename: form_urlencoded::parse(filename.as_bytes())
.map(|(key, val)| [key, val].concat())
.collect(),
})
.ok_or_else(|| {
Error::InvalidUrl(format!("the url \"{value}\" does not contain a filename"))
})
}
}
impl TryFrom<&str> for Download {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Url::parse(value)
.map_err(|e| Error::InvalidUrl(format!("the url \"{value}\" cannot be parsed: {e}")))
.and_then(|u| Download::try_from(&u))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Status {
Fail(String),
NotStarted,
Skipped(String),
Success,
}
#[derive(Debug, Clone)]
pub struct Summary {
download: Download,
statuscode: StatusCode,
size: u64,
status: Status,
resumable: bool,
}
impl Summary {
pub fn new(download: Download, statuscode: StatusCode, size: u64, resumable: bool) -> Self {
Self {
download,
statuscode,
size,
status: Status::NotStarted,
resumable,
}
}
pub fn with_status(self, status: Status) -> Self {
Self { status, ..self }
}
pub fn statuscode(&self) -> StatusCode {
self.statuscode
}
pub fn size(&self) -> u64 {
self.size
}
pub fn download(&self) -> &Download {
&self.download
}
pub fn status(&self) -> &Status {
&self.status
}
pub fn fail(self, msg: impl std::fmt::Display) -> Self {
Self {
status: Status::Fail(format!("{msg}")),
..self
}
}
pub fn set_resumable(&mut self, resumable: bool) {
self.resumable = resumable;
}
#[must_use]
pub fn resumable(&self) -> bool {
self.resumable
}
}
#[cfg(test)]
mod test {
use super::*;
const DOMAIN: &str = "http://domain.com/file.zip";
#[test]
fn test_try_from_url() {
let u = Url::parse(DOMAIN).unwrap();
let d = Download::try_from(&u).unwrap();
assert_eq!(d.filename, "file.zip")
}
#[test]
fn test_try_from_string() {
let d = Download::try_from(DOMAIN).unwrap();
assert_eq!(d.filename, "file.zip")
}
}