use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context, Result};
use sha2::{Digest, Sha256};
use super::pack::{self, hex};
use super::registry::ModelSpec;
pub const ENV_MIRROR: &str = "WARM_EMBED_MIRROR";
fn download_file(url: &str, dest: &Path, want_sha: &str, max: u64) -> Result<()> {
let read_limit = max
.checked_add(1)
.context("pinned download size overflow")?;
let mut resp = ureq::get(url)
.call()
.with_context(|| format!("download {url}"))?;
let mut reader = resp.body_mut().with_config().limit(read_limit).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)
}
struct FetchCoordinator {
active: std::sync::Mutex<std::collections::HashSet<PathBuf>>,
ready: std::sync::Condvar,
}
fn coordinator() -> &'static FetchCoordinator {
static STATE: std::sync::OnceLock<FetchCoordinator> = std::sync::OnceLock::new();
STATE.get_or_init(|| FetchCoordinator {
active: std::sync::Mutex::new(Default::default()),
ready: std::sync::Condvar::new(),
})
}
struct FetchLease(PathBuf);
impl FetchLease {
fn acquire(path: PathBuf) -> Self {
let state = coordinator();
let mut active = state.active.lock().unwrap_or_else(|e| e.into_inner());
while active.contains(&path) {
active = state.ready.wait(active).unwrap_or_else(|e| e.into_inner());
}
active.insert(path.clone());
Self(path)
}
}
impl Drop for FetchLease {
fn drop(&mut self) {
let state = coordinator();
state
.active
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&self.0);
state.ready.notify_all();
}
}
fn build_pack(
spec: &ModelSpec,
cache: &Path,
build: impl FnOnce(&Path) -> Result<Vec<u8>>,
) -> Result<PathBuf> {
ensure!(
!spec.id.is_empty()
&& spec
.id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"-_".contains(&b)),
"invalid model cache id"
);
std::fs::create_dir_all(cache)
.with_context(|| format!("create cache directory {}", cache.display()))?;
let cache = cache.canonicalize()?;
let dest = cache.join(spec.pack_name());
let _lease = FetchLease::acquire(dest.clone());
if dest.is_file() && pack::sha256_file(&dest)?.eq_ignore_ascii_case(spec.pack_sha256) {
return Ok(dest);
}
let work = tempfile::Builder::new()
.prefix(".warm-embed-fetch-")
.tempdir_in(&cache)?;
let bytes = build(work.path())?;
let got = pack::sha256_hex(&bytes);
ensure!(
got.eq_ignore_ascii_case(spec.pack_sha256),
"converted pack for {} has sha256 {got}, expected {}; refusing to install it",
spec.id,
spec.pack_sha256
);
install(work.path(), &cache, &spec.pack_name(), &bytes)
}
pub fn download_pack(spec: &ModelSpec, cache: &Path) -> Result<PathBuf> {
build_pack(spec, cache, |work| {
let base = crate::compatibility::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)?;
}
spec.convert(work)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_http_fetch_verifies_bytes_and_single_flights_retry_after_corruption() {
use std::net::TcpListener;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
let cache = tempfile::tempdir().unwrap();
let bytes = super::super::testing::tiny_pack_bytes(
super::super::Quantization::Int8,
super::super::Pooling::Mean,
);
let sha = pack::sha256_hex(&bytes);
let source = &super::super::MODELS[0];
let spec = ModelSpec {
id: "http-fixture",
repo: "test",
revision: "test",
license: "Apache-2.0",
pooling: source.pooling,
query_prefix: "",
passage_prefix: "",
max_length: 32,
quantization: source.quantization,
int8_group: 16,
files: &[],
pack_sha256: Box::leak(sha.clone().into_boxed_str()),
pack_bytes: bytes.len() as u64,
};
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let url = format!("http://{}/fixture.hmsemb", listener.local_addr().unwrap());
let requests = AtomicUsize::new(0);
let barrier = std::sync::Barrier::new(8);
let fetch = || {
build_pack(&spec, cache.path(), |work| {
let path = work.join("download.hmsemb");
download_file(&url, &path, &sha, bytes.len() as u64)?;
Ok(std::fs::read(path)?)
})
};
std::thread::scope(|scope| {
let server = scope.spawn(|| {
let deadline = Instant::now() + Duration::from_secs(10);
while requests.load(Ordering::SeqCst) < 2 && Instant::now() < deadline {
let (mut stream, _) = match listener.accept() {
Ok(connection) => connection,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(1));
continue;
}
Err(error) => panic!("fixture accept: {error}"),
};
stream.set_nonblocking(false).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
stream
.set_write_timeout(Some(Duration::from_secs(2)))
.unwrap();
let mut request = Vec::new();
while !request.ends_with(b"\r\n\r\n") {
let mut byte = [0u8; 1];
stream.read_exact(&mut byte).unwrap();
request.push(byte[0]);
assert!(request.len() <= 1024);
}
assert!(request.starts_with(b"GET /fixture.hmsemb HTTP/1.1\r\n"));
let first = requests.fetch_add(1, Ordering::SeqCst) == 0;
let body: &[u8] = if first { b"corrupt" } else { &bytes };
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
.unwrap();
stream.write_all(body).unwrap();
}
assert_eq!(requests.load(Ordering::SeqCst), 2);
});
let error = fetch().unwrap_err();
assert!(error.to_string().contains("sha256 mismatch"), "{error:#}");
assert_eq!(std::fs::read_dir(cache.path()).unwrap().count(), 0);
let callers: Vec<_> = (0..8)
.map(|_| {
scope.spawn(|| {
barrier.wait();
fetch().unwrap()
})
})
.collect();
for caller in callers {
assert_eq!(pack::sha256_file(&caller.join().unwrap()).unwrap(), sha);
}
server.join().unwrap();
});
assert_eq!(std::fs::read_dir(cache.path()).unwrap().count(), 1);
}
#[test]
fn concurrent_fetches_build_once_and_failures_release_the_reservation() {
use std::sync::atomic::{AtomicUsize, Ordering};
let cache = tempfile::tempdir().unwrap();
let bytes = super::super::testing::tiny_pack_bytes(
super::super::Quantization::Int8,
super::super::Pooling::Mean,
);
let sha = pack::sha256_hex(&bytes);
let source = &super::super::MODELS[0];
let spec = ModelSpec {
id: "test",
repo: "test",
revision: "test",
license: "MIT",
pooling: source.pooling,
query_prefix: "",
passage_prefix: "",
max_length: 64,
quantization: source.quantization,
int8_group: 16,
files: &[],
pack_sha256: Box::leak(sha.into_boxed_str()),
pack_bytes: bytes.len() as u64,
};
assert!(build_pack(&spec, cache.path(), |_| anyhow::bail!("injected failure")).is_err());
let calls = AtomicUsize::new(0);
let barrier = std::sync::Barrier::new(8);
std::thread::scope(|scope| {
let handles: Vec<_> = (0..8)
.map(|_| {
scope.spawn(|| {
barrier.wait();
build_pack(&spec, cache.path(), |_| {
calls.fetch_add(1, Ordering::SeqCst);
Ok(bytes.clone())
})
.unwrap()
})
})
.collect();
for handle in handles {
assert_eq!(
pack::sha256_file(&handle.join().unwrap()).unwrap(),
spec.pack_sha256
);
}
});
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(std::fs::read_dir(cache.path()).unwrap().count(), 1);
}
#[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");
}
}