use crate::{Download, DownloadSummary, Error, Result};
use crate::progress::Factory;
fn validate_downloads(
downloads: &[Download],
download_folder: &std::path::Path,
factory: &dyn crate::progress::Factory,
) -> Result<Vec<Download>> {
let mut known_urls = std::collections::HashSet::new();
let mut known_download_paths = std::collections::HashSet::new();
let mut result = Vec::with_capacity(downloads.len());
for d in downloads {
if d.urls.is_empty() {
return Err(Error::DownloadDefinition(String::from(
"No URL found to download.",
)));
}
for u in &d.urls {
if !known_urls.insert(u) {
return Err(Error::DownloadDefinition(format!(
"Download URL \"{u}\" is used more than once.",
)));
}
}
let urls = d.urls.clone();
if d.file_name.to_string_lossy().is_empty() {
return Err(Error::DownloadDefinition(String::from(
"No download file name was provided.",
)));
}
let file_name = download_folder.join(&d.file_name);
if d.file_name.to_string_lossy().is_empty() {
return Err(Error::DownloadDefinition(String::from(
"Failed to get full download path.",
)));
}
if !known_download_paths.insert(&d.file_name) {
return Err(Error::DownloadDefinition(format!(
"Download file name \"{}\" is used more than once.",
d.file_name.to_string_lossy(),
)));
}
let progress = if d.progress.is_none() {
factory.create_reporter()
} else {
d.progress.as_ref().expect("Was Some just now...").clone()
};
result.push(Download {
urls,
file_name,
progress: Some(progress),
verify_callback: d.verify_callback.clone(),
});
}
Ok(result)
}
pub struct Downloader {
client: reqwest::Client,
parallel_requests: u16,
retries: u16,
download_folder: std::path::PathBuf,
}
impl Downloader {
#[must_use]
pub fn builder() -> Builder {
Builder::default()
}
pub fn download(&mut self, downloads: &[Download]) -> Result<Vec<Result<DownloadSummary>>> {
#[cfg(feature = "tui")]
let factory = crate::progress::Tui::default();
#[cfg(not(feature = "tui"))]
let factory = crate::progress::Noop::default();
let to_process = validate_downloads(downloads, &self.download_folder, &factory)?;
if to_process.is_empty() {
return Ok(Vec::new());
}
Ok(crate::backend::run(
&mut self.client,
to_process,
self.retries,
self.parallel_requests,
&move || {
factory.join();
},
))
}
}
pub struct Builder {
user_agent: String,
connect_timeout: std::time::Duration,
timeout: std::time::Duration,
parallel_requests: u16,
retries: u16,
download_folder: std::path::PathBuf,
}
impl Builder {
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
self.user_agent = user_agent.into();
self
}
pub fn connect_timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
self.connect_timeout = timeout;
self
}
pub fn timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
self.timeout = timeout;
self
}
pub fn parallel_requests(&mut self, count: u16) -> &mut Self {
self.parallel_requests = count;
self
}
pub fn retries(&mut self, count: u16) -> &mut Self {
self.retries = count;
self
}
pub fn download_folder(&mut self, folder: &std::path::Path) -> &mut Self {
self.download_folder = folder.to_path_buf();
self
}
fn build_client(&self) -> crate::Result<reqwest::Client> {
reqwest::Client::builder()
.user_agent(self.user_agent.clone())
.connect_timeout(self.connect_timeout)
.timeout(self.timeout)
.build()
.map_err(|e| Error::Setup(format!("Failed to set up backend: {e}")))
}
pub fn build_with_client(&mut self, client: reqwest::Client) -> crate::Result<Downloader> {
let download_folder = &self.download_folder;
if download_folder.to_string_lossy().is_empty() {
return Err(crate::Error::Setup(
"Required \"download_folder\" was not set.".into(),
));
}
if !download_folder.is_dir() {
return Err(Error::Setup(format!(
"Required \"download_folder\" with value \"{}\" is not a folder.",
download_folder.to_string_lossy()
)));
}
Ok(Downloader {
client,
parallel_requests: self.parallel_requests,
retries: self.retries,
download_folder: download_folder.clone(),
})
}
pub fn build(&mut self) -> crate::Result<Downloader> {
let client = self.build_client()?;
self.build_with_client(client)
}
}
impl Default for Builder {
fn default() -> Self {
let download_folder =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(""));
let download_folder = if download_folder.to_string_lossy().is_empty() {
std::path::PathBuf::from(
std::env::var_os("HOME").unwrap_or_else(|| std::ffi::OsString::from("/")),
)
} else {
download_folder
};
Self {
user_agent: format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
connect_timeout: std::time::Duration::from_secs(30),
timeout: std::time::Duration::from_secs(300),
parallel_requests: 32,
retries: 3,
download_folder,
}
}
}