use anyhow::{Context, Result, bail};
use std::io::Read as _;
use std::path::PathBuf;
const REPO: &str = "minishlab/potion-base-8M";
const FILES: &[(&str, u64)] = &[
("model.safetensors", 128 * 1024 * 1024),
("tokenizer.json", 8 * 1024 * 1024),
("config.json", 64 * 1024),
];
fn agent() -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_global(Some(std::time::Duration::from_secs(120)))
.user_agent(concat!("cctop/", env!("CARGO_PKG_VERSION")))
.build()
.into()
}
pub fn model_dir() -> PathBuf {
crate::config::EMBEDDING_MODEL_DIR.join("potion-base-8M")
}
pub fn present() -> bool {
let dir = model_dir();
FILES.iter().all(|(name, _)| dir.join(name).exists())
}
pub fn fetch() -> Result<PathBuf> {
let dir = model_dir();
if present() {
println!("Search model already present at {}.", dir.display());
return Ok(dir);
}
std::fs::create_dir_all(&dir).with_context(|| format!("could not create {}", dir.display()))?;
println!("Fetching the search model ({REPO})…");
for (name, cap) in FILES {
let url = format!("https://huggingface.co/{REPO}/resolve/main/{name}");
let mut body = Vec::new();
agent()
.get(&url)
.call()
.with_context(|| format!("could not download {name}"))?
.body_mut()
.as_reader()
.take(*cap + 1)
.read_to_end(&mut body)
.with_context(|| format!("could not read {name}"))?;
if body.len() as u64 > *cap {
bail!("{name} is larger than expected; refusing it");
}
if body.is_empty() {
bail!("{name} came back empty");
}
let target = dir.join(name);
let tmp = target.with_extension("part");
std::fs::write(&tmp, &body).with_context(|| format!("could not write {name}"))?;
std::fs::rename(&tmp, &target).with_context(|| format!("could not place {name}"))?;
println!(" {name} {:.1} MB", body.len() as f64 / 1_048_576.0);
}
super::Model::load(&dir).context("the downloaded model did not load")?;
println!("Search model ready at {}.", dir.display());
Ok(dir)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn a_partial_download_is_not_a_model() {
let dir = std::env::temp_dir().join("cctop-fetch-partial");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("config.json"), "{}").expect("write");
let complete = |d: &Path| FILES.iter().all(|(n, _)| d.join(n).exists());
assert!(!complete(&dir), "one file of three is not a model");
std::fs::write(dir.join("tokenizer.json"), "{}").expect("write");
assert!(!complete(&dir), "two of three is not a model either");
std::fs::write(dir.join("model.safetensors"), "").expect("write");
assert!(complete(&dir));
let _ = std::fs::remove_dir_all(dir);
}
}