use std::path::Path;
use futures::stream::StreamExt;
use tokio::fs;
use tokio::prelude::*;
use tokio::task;
use anyhow::{anyhow, bail, Context, Result};
use nix_base32::to_nix_base32;
use path_clean::PathClean;
use sha2::{Digest, Sha256};
pub fn filename_to_narinfo_hash(filename: &str) -> Result<&str> {
filename
.split('-')
.next()
.ok_or_else(|| anyhow!("failed to parse narinfo hash: {}", filename))
}
pub fn store_path_to_narinfo_hash(store_path: &str) -> Result<&str> {
store_path
.split('/')
.nth(3)
.ok_or_else(|| anyhow!("failed to parse store_path: {}", store_path))
.and_then(filename_to_narinfo_hash)
}
pub async fn download_atomically(
client: &reqwest::Client,
url: String,
destination: &Path,
hash: Option<&str>,
) -> Result<fs::File> {
let mut resp_stream = client
.get(&url)
.send()
.await?
.error_for_status()?
.bytes_stream();
let mut ctx = hash.map(|_| Sha256::new());
let destination_dir = destination.parent().unwrap();
let result: Result<_> = task::block_in_place(|| {
let tempfile = tempfile::NamedTempFile::new_in(&destination_dir)?;
let f: fs::File = tempfile.reopen()?.into();
Ok((tempfile, f))
});
let (tempfile, mut async_file) = result?;
while let Some(bytes) = resp_stream.next().await {
let bytes = bytes?;
if let Some(ctx) = ctx.as_mut() {
ctx.update(&bytes);
}
async_file.write_all(&bytes).await?;
}
async_file.shutdown().await?;
if let Some(ctx) = ctx {
let hash = hash.unwrap();
let computed = to_nix_base32(&ctx.finalize().as_ref());
if computed != hash {
bail!(
"hash of file: {:?} failed, expected: {}, got: {}",
destination,
hash,
computed
);
}
}
let f = task::block_in_place(|| tempfile.persist(&destination))?;
let mut f = fs::File::from(f);
f.seek(std::io::SeekFrom::Start(0)).await?;
Ok(f)
}
pub async fn handle_narinfo(
client: &reqwest::Client,
cache_url: &String,
mirror_dir: &Path,
narinfo_hash: String,
) -> Result<Vec<String>> {
let mut narinfo_filename = mirror_dir.join(&narinfo_hash);
narinfo_filename.set_extension("narinfo");
let narinfo_filename = narinfo_filename.clean();
let narinfo_file = if let Ok(f) = fs::File::open(&narinfo_filename).await {
f
} else {
let url = format!("{}/{}.narinfo", cache_url, &narinfo_hash);
download_atomically(client, url, &narinfo_filename, None).await?
};
let narinfo_file = io::BufReader::new(narinfo_file);
let mut lines = narinfo_file.lines();
let mut url = Err(anyhow!("failed to find URL"));
let mut references = Vec::new();
let mut filehash = Err(anyhow!("failed to find filehash"));
while let Some(line) = lines.next().await {
let line = line?;
let mut split = line.splitn(2, ": ");
let key = split.next().context("failed to find key")?;
let val = split.next().context("failed to find val")?;
match key {
"URL" => url = Ok(String::from(val)),
"References" => {
references = val
.split_whitespace()
.flat_map(|x| x.split("-").next())
.map(String::from)
.collect()
}
"FileHash" => {
filehash = val
.split(':')
.nth(1)
.map(String::from)
.context("invalid filehash")
}
_ => {}
}
}
let url = url?;
let filehash = filehash?;
let filename = mirror_dir.join(&url).clean();
if fs::File::open(&filename).await.is_err() {
let url = format!("{}/{}", cache_url, &url);
download_atomically(client, url, &filename, Some(&filehash)).await?;
}
Ok(references.into_iter().map(String::from).collect())
}