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,
pub tag: Option<String>,
}
impl Download {
pub fn new(url: &Url, filename: impl Into<String>) -> Self {
Self {
url: url.clone(),
filename: filename.into(),
tag: None,
}
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
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();
let accept_ranges = match headers.get(ACCEPT_RANGES) {
None => false,
Some(x) if x == "none" => false,
Some(_) => true,
};
if !accept_ranges {
return Ok(false);
}
let content_length = Self::parse_content_length(headers);
Ok(content_length.is_some() && content_length != Some(0))
}
pub fn parse_content_length(headers: &reqwest::header::HeaderMap) -> Option<u64> {
headers
.get(CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.to_string().parse::<u64>().ok())
}
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();
Ok(Self::parse_content_length(headers))
}
}
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(),
tag: None,
})
.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")
}
}