blob: 805770dde8de42d6c85eaff3a78fb2ae4df5bb5e [file] [log] [blame]
use std::{
io::Seek,
path::{Path, PathBuf},
};
use anyhow::{bail, Context, Result};
use byte_unit::Byte;
use clap::Args;
use std::io::Read;
mod default_downloads;
#[derive(Args, Debug)]
pub(crate) struct Download {
#[clap(long)]
/// Directory to download the files into
#[clap(default_value_os_t = default_directory())]
pub directory: PathBuf,
/// Set of files to download. Use `name@url` format to specify a file prefix
downloads: Option<Vec<String>>,
/// Whether to automatically install the default set of files
#[clap(long)]
auto: bool,
/// The size limit for each individual file (ignored if the default files are downloaded)
#[clap(long, default_value = "10 MB")]
size_limit: Byte,
}
fn default_directory() -> PathBuf {
let mut result = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("assets");
result.push("downloads");
result
}
impl Download {
pub fn action(&self) -> Result<()> {
let mut to_download = vec![];
if let Some(downloads) = &self.downloads {
to_download = downloads
.iter()
.map(|it| Self::parse_download(&it))
.collect();
} else {
let mut accepted = self.auto;
let downloads = default_downloads::default_downloads()
.into_iter()
.filter(|it| {
let file = it.file_path(&self.directory);
!file.exists()
})
.collect::<Vec<_>>();
if !accepted {
if downloads.len() != 0 {
println!(
"Would you like to download a set of default svg files? These files are:"
);
for download in &downloads {
let builtin = download.builtin.as_ref().unwrap();
println!(
"{} ({}) under license {} from {}",
download.name,
byte_unit::Byte::from_bytes(builtin.expected_size.into())
.get_appropriate_unit(false),
builtin.license,
builtin.info
);
}
// For rustfmt, split prompt into its own line
const PROMPT: &str =
"Would you like to download a set of default svg files, as explained above?";
accepted = dialoguer::Confirm::new()
.with_prompt(PROMPT)
.wait_for_newline(true)
.interact()?;
} else {
println!("Nothing to download! All default downloads already created");
}
}
if accepted {
to_download = downloads;
}
}
for (index, download) in to_download.iter().enumerate() {
println!(
"{index}: Downloading {} from {}",
download.name, download.url
);
download.fetch(&self.directory, self.size_limit)?
}
println!("{} downloads complete", to_download.len());
Ok(())
}
fn parse_download(value: &str) -> SVGDownload {
if let Some(at_index) = value.find('@') {
let name = &value[0..at_index];
let url = &value[at_index + 1..];
SVGDownload {
name: name.to_string(),
url: url.to_string(),
builtin: None,
}
} else {
let end_index = value.rfind(".svg").unwrap_or(value.len());
let url_with_name = &value[0..end_index];
let name = url_with_name
.rfind('/')
.map(|v| &url_with_name[v + 1..])
.unwrap_or(url_with_name);
SVGDownload {
name: name.to_string(),
url: value.to_string(),
builtin: None,
}
}
}
}
struct SVGDownload {
name: String,
url: String,
builtin: Option<BuiltinSvgProps>,
}
impl SVGDownload {
fn file_path(&self, directory: &Path) -> PathBuf {
directory.join(&self.name).with_extension("svg")
}
fn fetch(&self, directory: &Path, size_limit: Byte) -> Result<()> {
let mut size_limit = size_limit.get_bytes().try_into()?;
let mut limit_exact = false;
if let Some(builtin) = &self.builtin {
size_limit = builtin.expected_size;
limit_exact = true;
}
let mut file = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&self.file_path(directory))
.context("Creating file")?;
let mut reader = ureq::get(&self.url).call()?.into_reader();
std::io::copy(
// ureq::into_string() has a limit of 10MiB so we must use the reader
&mut (&mut reader).take(size_limit),
&mut file,
)?;
if reader.read_exact(&mut [0]).is_ok() {
bail!("Size limit exceeded");
}
if limit_exact {
if file
.seek(std::io::SeekFrom::Current(0))
.context("Checking file limit")?
!= size_limit
{
bail!("Builtin downloaded file was not as expected");
}
}
Ok(())
}
}
struct BuiltinSvgProps {
expected_size: u64,
license: &'static str,
info: &'static str,
}