use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use anyhow::{bail, ensure, Context, Result};
use sha2::{Digest, Sha256};
use super::pack::{self, hex};
use super::registry::ModelSpec;
pub const ENV_MIRROR: &str = "HMS_EMBED_MIRROR";
fn download_file(url: &str, dest: &Path, want_sha: &str, max: u64) -> Result<()> {
let mut resp = ureq::get(url)
.call()
.with_context(|| format!("download {url}"))?;
let mut reader = resp.body_mut().with_config().limit(max + 1).reader();
let mut file =
std::fs::File::create(dest).with_context(|| format!("create {}", dest.display()))?;
let mut hash = Sha256::new();
let mut buf = vec![0u8; 1 << 20];
let mut total = 0u64;
loop {
let n = reader
.read(&mut buf)
.with_context(|| format!("read {url}"))?;
if n == 0 {
break;
}
total += n as u64;
ensure!(total <= max, "{url} is larger than the pinned {max} bytes");
hash.update(&buf[..n]);
file.write_all(&buf[..n])?;
}
file.sync_all()?;
let got = hex(&hash.finalize());
ensure!(
got.eq_ignore_ascii_case(want_sha),
"sha256 mismatch for {url}: expected {want_sha}, downloaded {got}; refusing to use it"
);
Ok(())
}
fn install(work: &Path, cache: &Path, name: &str, bytes: &[u8]) -> Result<PathBuf> {
let tmp = work.join(format!(".{name}.tmp"));
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
}
let dest = cache.join(name);
std::fs::rename(&tmp, &dest).with_context(|| format!("install {}", dest.display()))?;
crate::core::durable_file::sync_directory(cache)?;
Ok(dest)
}
pub fn download_pack(spec: &ModelSpec, cache: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(cache)
.with_context(|| format!("create cache directory {}", cache.display()))?;
let work = cache.join(format!(".fetch-{}-{}", spec.id, std::process::id()));
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(&work)?;
let result = (|| -> Result<PathBuf> {
let base = std::env::var(ENV_MIRROR)
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "https://huggingface.co".into());
let base = base.trim_end_matches('/');
for f in spec.files {
let url = format!("{base}/{}/resolve/{}/{}", spec.repo, spec.revision, f.name);
download_file(&url, &work.join(f.name), f.sha256, f.size)?;
}
let bytes = spec.convert(&work)?;
let got = pack::sha256_hex(&bytes);
if !got.eq_ignore_ascii_case(spec.pack_sha256) {
bail!(
"converted pack for {} has sha256 {got}, the registry pins {}; the upstream files verified, so this build's converter differs from the one that produced the pin",
spec.id,
spec.pack_sha256
);
}
install(&work, cache, &spec.pack_name(), &bytes)
})();
let _ = std::fs::remove_dir_all(&work);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_failed_install_leaves_nothing_in_the_cache() {
let cache = tempfile::tempdir().unwrap();
let work = cache.path().join(".fetch-test");
std::fs::create_dir(&work).unwrap();
std::fs::create_dir_all(cache.path().join("m.pack/x")).unwrap();
assert!(install(&work, cache.path(), "m.pack", b"pack").is_err());
std::fs::remove_dir_all(&work).unwrap();
let mut left: Vec<_> = std::fs::read_dir(cache.path())
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
left.sort();
assert_eq!(left, ["m.pack"]);
std::fs::remove_dir_all(cache.path().join("m.pack")).unwrap();
std::fs::create_dir(&work).unwrap();
let dest = install(&work, cache.path(), "m.pack", b"pack").unwrap();
assert_eq!(std::fs::read(dest).unwrap(), b"pack");
}
}