#![doc = include_str!("../README.md")]
mod drivers;
mod process;
mod sink;
mod url_parser;
mod util;
pub use sink::DownloadSink;
use std::fs::OpenOptions;
use std::io;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use crate::drivers::Request;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Downloader {
Curl,
Wget,
PowerShell,
Python3,
Tunnel,
Tcp,
OpenSSL,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Quiet {
Never,
Always,
OnSuccess,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentEncoding {
Gzip,
}
#[derive(Debug, Clone)]
pub struct DownloadResult {
pub status_code: u16,
pub content_encoding: Option<ContentEncoding>,
}
#[derive(Debug, Clone)]
pub struct RequestBuilder {
pub(crate) url: String,
pub(crate) headers: Vec<(String, String)>,
pub(crate) preferred: Vec<Downloader>,
pub(crate) follow_redirects: bool,
pub(crate) quiet: Quiet,
}
impl RequestBuilder {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
headers: Vec::new(),
preferred: Vec::new(),
follow_redirects: true,
quiet: Quiet::Always,
}
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((key.into(), value.into()));
self
}
pub fn preferred_downloader(mut self, preferred: Downloader) -> Self {
self.preferred.push(preferred);
self
}
pub fn follow_redirects(mut self, follow_redirects: bool) -> Self {
self.follow_redirects = follow_redirects;
self
}
pub fn quiet(mut self, quiet: Quiet) -> Self {
self.quiet = quiet;
self
}
pub fn fetch_string(self) -> Result<String, ResponseError> {
String::from_utf8(self.fetch_bytes()?)
.map_err(|e| ResponseError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
}
pub fn fetch_bytes(self) -> Result<Vec<u8>, ResponseError> {
url_parser::Url::new(&self.url)
.map_err(|e| ResponseError::Start(StartError::Url(e.to_string())))?;
let cancel = Arc::new(AtomicBool::new(false));
let buffer = Arc::new(Mutex::new(Vec::new()));
let memory_root = DownloadSink::buffer(buffer.clone());
let join = self
.start_first_backend(Arc::clone(&cancel), memory_root.clone())
.map_err(ResponseError::Start)?;
join.join().map_err(|_| ResponseError::ThreadPanicked)??;
Ok(std::mem::take(&mut *buffer.lock().unwrap()))
}
pub fn start(self, target_path: impl AsRef<Path>) -> Result<RequestHandle, StartError> {
let _ = url_parser::Url::new(&self.url).map_err(|e| StartError::Url(e.to_string()))?;
let target_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&target_path)?;
let cancel = Arc::new(AtomicBool::new(false));
let sink = DownloadSink::file(target_file);
let join = self.start_first_backend(cancel.clone(), sink)?;
Ok(RequestHandle {
cancel,
join: Some(join),
})
}
fn start_first_backend(
&self,
cancel: Arc<AtomicBool>,
sink: DownloadSink,
) -> Result<JoinHandle<Result<DownloadResult, ResponseError>>, StartError> {
let mut saw_non_not_found: Option<io::Error> = None;
let mut saw_any_not_found = false;
let request = Request {
url: url_parser::Url::new(&self.url).map_err(|e| StartError::Url(e.to_string()))?,
headers: self.headers.clone(),
follow_redirects: self.follow_redirects,
quiet: self.quiet,
};
for d in candidate_downloaders(&self.preferred) {
match d
.driver()
.start(request.clone(), sink.clone(), Arc::clone(&cancel))
{
Ok(join) => return Ok(join),
Err(StartError::Url(msg)) => return Err(StartError::Url(msg)),
Err(StartError::NoDriverFound) => {
saw_any_not_found = true;
continue;
}
Err(StartError::IoError(e)) => {
if saw_non_not_found.is_none() {
saw_non_not_found = Some(e);
}
continue;
}
}
}
if let Some(e) = saw_non_not_found {
return Err(StartError::IoError(e));
}
if saw_any_not_found {
return Err(StartError::NoDriverFound);
}
Err(StartError::NoDriverFound)
}
}
impl Downloader {
pub(crate) fn driver(self) -> &'static dyn drivers::Driver {
match self {
Downloader::Curl => &drivers::curl::CurlDriver,
Downloader::Wget => &drivers::wget::WgetDriver,
Downloader::PowerShell => &drivers::powershell::PowerShellDriver,
Downloader::Python3 => &drivers::python3::Python3Driver,
Downloader::Tunnel => &drivers::tunnel::TunnelDriver,
Downloader::Tcp => &drivers::tunnel::TcpDriver,
Downloader::OpenSSL => &drivers::tunnel::OpenSslDriver,
}
}
}
#[derive(Debug)]
pub struct RequestHandle {
cancel: Arc<AtomicBool>,
join: Option<JoinHandle<Result<DownloadResult, ResponseError>>>,
}
impl RequestHandle {
pub fn cancel(&self) {
self.cancel.store(true, Ordering::SeqCst);
}
pub fn join(mut self) -> Result<Response, ResponseError> {
let res = match self.join.take().expect("join called once").join() {
Ok(r) => r,
Err(_) => Err(ResponseError::ThreadPanicked),
}?;
Ok(Response {
status_code: res.status_code,
})
}
}
impl Drop for RequestHandle {
fn drop(&mut self) {
if self.join.is_some() {
self.cancel.store(true, Ordering::SeqCst);
}
}
}
#[derive(Debug, Clone)]
pub struct Response {
pub status_code: u16,
}
#[derive(Debug)]
pub enum StartError {
NoDriverFound,
IoError(io::Error),
Url(String),
}
impl From<io::Error> for StartError {
fn from(value: io::Error) -> Self {
Self::IoError(value)
}
}
#[derive(Debug)]
pub enum ResponseError {
Io(io::Error),
InvalidUrl,
UnsupportedScheme,
Cancelled,
ThreadPanicked,
CommandFailed {
program: &'static str,
exit_code: Option<i32>,
stderr: String,
},
BadStatusCode(String),
GzipFailed {
exit_code: Option<i32>,
stderr: String,
},
Start(StartError),
}
impl From<io::Error> for ResponseError {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
fn candidate_downloaders(preferred: &[Downloader]) -> Vec<Downloader> {
if !preferred.is_empty() {
return preferred.to_vec();
}
vec![
Downloader::Curl,
Downloader::Wget,
Downloader::PowerShell,
Downloader::Python3,
Downloader::Tunnel,
]
}