use {
anyhow::{anyhow, Context, Result},
fs2::FileExt,
log::warn,
sha2::Digest,
std::{fs::File, io::Read, path::Path},
url::Url,
};
pub struct RemoteContent {
pub name: String,
pub url: String,
pub sha256: String,
}
fn sha256_path<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
let mut hasher = sha2::Sha256::new();
let fh = std::fs::File::open(&path)?;
let mut reader = std::io::BufReader::new(fh);
let mut buffer = [0; 32768];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
hasher.update(&buffer[..count]);
}
Ok(hasher.finalize().to_vec())
}
pub fn get_http_client() -> reqwest::Result<reqwest::blocking::Client> {
let mut builder = reqwest::blocking::ClientBuilder::new();
for (key, value) in std::env::vars() {
let key = key.to_lowercase();
if key.ends_with("_proxy") {
let end = key.len() - "_proxy".len();
let schema = &key[..end];
if let Ok(url) = Url::parse(&value) {
if let Some(Ok(proxy)) = match schema {
"http" => Some(reqwest::Proxy::http(url.as_str())),
"https" => Some(reqwest::Proxy::https(url.as_str())),
_ => None,
} {
builder = builder.proxy(proxy);
}
}
}
}
builder.build()
}
pub fn download_and_verify(entry: &RemoteContent) -> Result<Vec<u8>> {
let url =
std::env::var(format!("{}_URL", &entry.name)).unwrap_or_else(|_err| entry.url.to_string());
warn!("downloading {}", url);
let url = Url::parse(&url)?;
let client = get_http_client()?;
let mut data: Vec<u8> = Vec::new();
if url.scheme() == "file" {
let file_path = url
.to_file_path()
.map_err(|_err: ()| anyhow!("bad url for {}: {}", entry.name, url))?;
let mut file = File::open(file_path)?;
file.read_to_end(&mut data)?;
} else {
let mut response = client.get(url).send()?;
response.read_to_end(&mut data)?;
}
let mut hasher = sha2::Sha256::new();
hasher.update(&data);
let url_hash = hasher.finalize().to_vec();
let expected_hash = hex::decode(&entry.sha256)?;
if expected_hash == url_hash {
warn!("verified SHA-256 is {}", entry.sha256);
Ok(data)
} else {
Err(anyhow!("hash mismatch of downloaded file"))
}
}
pub fn download_to_path<P: AsRef<Path>>(entry: &RemoteContent, dest_path: P) -> Result<()> {
let dest_path = dest_path.as_ref();
if let Some(dest_dir) = dest_path.parent() {
std::fs::create_dir_all(dest_dir)
.with_context(|| format!("creating directory {}", dest_dir.display()))?;
}
let expected_hash = hex::decode(&entry.sha256)?;
let lock_path = dest_path.with_extension("lock");
let lock = std::fs::File::create(&lock_path)
.with_context(|| format!("creating {}", lock_path.display()))?;
lock.lock_exclusive().context("obtaining lock")?;
if dest_path.exists() {
let file_hash = sha256_path(dest_path)?;
if file_hash == expected_hash {
lock.unlock().context("unlocking")?;
return Ok(());
}
std::fs::remove_file(dest_path)?;
}
let data = download_and_verify(entry).context("downloading with verification")?;
let temp_path = dest_path.with_file_name(format!(
"{}.tmp",
dest_path
.file_name()
.ok_or_else(|| anyhow!("unable to obtain file name"))?
.to_string_lossy()
));
std::fs::write(&temp_path, data).context("writing data to temporary file")?;
std::fs::rename(&temp_path, dest_path).with_context(|| {
format!(
"renaming {} to {}",
temp_path.display(),
dest_path.display()
)
})?;
lock.unlock().context("unlocking")?;
Ok(())
}