use std::io::Read;
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
fn endpoint() -> String {
std::env::var("HF_ENDPOINT")
.ok()
.map(|e| e.trim_end_matches('/').to_string())
.unwrap_or_else(|| "https://huggingface.co".to_string())
}
fn token() -> Option<String> {
["HF_TOKEN", "HUGGING_FACE_HUB_TOKEN"]
.iter()
.find_map(|k| std::env::var(k).ok())
.filter(|t| !t.trim().is_empty())
}
fn resolve_ipv4_first(netloc: &str) -> std::io::Result<Vec<SocketAddr>> {
let mut addrs: Vec<SocketAddr> = netloc.to_socket_addrs()?.collect();
addrs.sort_by_key(|addr| !addr.is_ipv4());
Ok(addrs)
}
fn agent() -> ureq::Agent {
ureq::AgentBuilder::new()
.timeout_connect(Duration::from_secs(10))
.resolver(resolve_ipv4_first)
.build()
}
fn with_auth(req: ureq::Request) -> ureq::Request {
match token() {
Some(t) => req.set("Authorization", &format!("Bearer {t}")),
None => req,
}
}
pub fn cache_dir() -> std::path::PathBuf {
if let Some(dir) = std::env::var_os("FERROX_CACHE") {
return std::path::PathBuf::from(dir);
}
if let Some(dir) = std::env::var_os("XDG_CACHE_HOME") {
return std::path::PathBuf::from(dir).join("ferrox");
}
match std::env::var_os("HOME") {
Some(home) => std::path::PathBuf::from(home).join(".cache").join("ferrox"),
None => std::path::PathBuf::from(".ferrox-cache"),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HfRef {
pub repo: String,
pub file: Option<String>,
pub quant: Option<String>,
}
impl HfRef {
pub fn parse(spec: &str) -> Self {
match spec.rsplit_once(':') {
Some((repo, quant)) if !repo.is_empty() && !quant.is_empty() => HfRef {
repo: repo.to_string(),
file: None,
quant: Some(quant.to_ascii_uppercase()),
},
Some((repo, _)) if !repo.is_empty() => HfRef {
repo: repo.to_string(),
file: None,
quant: None,
},
_ => HfRef {
repo: spec.to_string(),
file: None,
quant: None,
},
}
}
pub fn pattern(&self) -> String {
match &self.quant {
Some(q) => format!("*{q}*.gguf"),
None => "*.gguf".to_string(),
}
}
pub fn resolve(&self) -> Result<String, String> {
match &self.file {
Some(f) => Ok(f.clone()),
None => resolve_glob_ci(&self.repo, &self.pattern()),
}
}
}
impl HfRef {
pub fn ensure_local(
&self,
on_progress: &mut dyn FnMut(u64, Option<u64>),
) -> Result<(std::path::PathBuf, bool), String> {
let filename = self.resolve()?;
let dir = cache_dir().join("hub").join(self.repo.replace('/', "__"));
let path = dir.join(&filename);
if path.is_file() {
return Ok((path, false));
}
let path = fetch_to_dir_with_progress(&self.repo, &filename, &dir, on_progress)?;
Ok((path, true))
}
}
pub fn resolve_glob_ci(repo: &str, pattern: &str) -> Result<String, String> {
let names = list_files(repo)?;
let lowered = pattern.to_ascii_lowercase();
names
.iter()
.find(|n| glob_matches(&lowered, &n.to_ascii_lowercase()))
.cloned()
.ok_or_else(|| {
let ggufs: Vec<&str> = names
.iter()
.filter(|n| n.to_ascii_lowercase().ends_with(".gguf"))
.map(String::as_str)
.collect();
if ggufs.is_empty() {
format!("no file in {repo} matches '{pattern}', and the repo holds no GGUF at all")
} else {
format!(
"no file in {repo} matches '{pattern}'. That repo publishes: {}",
ggufs.join(", ")
)
}
})
}
fn list_files(repo: &str) -> Result<Vec<String>, String> {
let url = format!("{}/api/models/{repo}", endpoint());
let response = with_auth(agent().get(&url))
.call()
.map_err(|e| format!("listing {repo} on the Hub failed: {e}"))?;
let body = response
.into_string()
.map_err(|e| format!("reading the file list for {repo}: {e}"))?;
let value: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| format!("the Hub's file list for {repo} was not JSON: {e}"))?;
let mut names: Vec<String> = value
.get("siblings")
.and_then(|s| s.as_array())
.map(|entries| {
entries
.iter()
.filter_map(|e| e.get("rfilename")?.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
names.sort();
names.retain(|n| !n.contains('/'));
Ok(names)
}
pub fn resolve_glob(repo: &str, pattern: &str) -> Result<String, String> {
let url = format!("{}/api/models/{repo}", endpoint());
let response = with_auth(agent().get(&url))
.call()
.map_err(|e| format!("listing {repo} on the Hub failed: {e}"))?;
let body = response
.into_string()
.map_err(|e| format!("reading the file list for {repo}: {e}"))?;
let value: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| format!("the Hub's file list for {repo} was not JSON: {e}"))?;
let mut names: Vec<String> = value
.get("siblings")
.and_then(|s| s.as_array())
.map(|entries| {
entries
.iter()
.filter_map(|e| e.get("rfilename")?.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
names.sort();
names
.into_iter()
.filter(|n| !n.contains('/'))
.find(|n| glob_matches(pattern, n))
.ok_or_else(|| format!("no file in {repo} matches '{pattern}'"))
}
pub struct HubFile {
pub body: Box<dyn Read + Send>,
pub total_bytes: Option<u64>,
pub resumed: bool,
}
pub fn open_file(repo: &str, filename: &str, resume_from: u64) -> Result<HubFile, String> {
let url = format!("{}/{repo}/resolve/main/{filename}", endpoint());
let mut req = with_auth(agent().get(&url));
if resume_from > 0 {
req = req.set("Range", &format!("bytes={resume_from}-"));
}
let response = req
.call()
.map_err(|e| format!("downloading {filename} from {repo} failed: {e}"))?;
let status = response.status();
let resumed = status == 206 && resume_from > 0;
let total_bytes = if resumed {
content_range_total(response.header("content-range"))
} else {
response
.header("content-length")
.and_then(|v| v.trim().parse::<u64>().ok())
};
Ok(HubFile {
body: Box::new(response.into_reader()),
total_bytes,
resumed,
})
}
fn content_range_total(header: Option<&str>) -> Option<u64> {
let total = header?.rsplit('/').next()?.trim();
total.parse::<u64>().ok()
}
pub fn glob_matches(pattern: &str, name: &str) -> bool {
let segments: Vec<&str> = pattern.split('*').collect();
if segments.len() == 1 {
return pattern == name;
}
let mut rest = name;
if let Some(first) = segments.first() {
let Some(stripped) = rest.strip_prefix(first) else {
return false;
};
rest = stripped;
}
if let Some(last) = segments.last() {
let Some(stripped) = rest.strip_suffix(last) else {
return false;
};
rest = stripped;
}
for middle in segments
.iter()
.skip(1)
.take(segments.len().saturating_sub(2))
{
match rest.find(*middle) {
Some(at) => rest = &rest[at + middle.len()..],
None => return false,
}
}
true
}
pub fn fetch_to_dir_with_progress(
repo: &str,
pattern: &str,
dir: &std::path::Path,
mut on_progress: impl FnMut(u64, Option<u64>),
) -> Result<std::path::PathBuf, String> {
use std::io::{Read, Write};
let filename = resolve_glob(repo, pattern)?;
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let final_path = dir.join(&filename);
let partial = dir.join(format!("{filename}.partial"));
if final_path.exists() {
return Ok(final_path);
}
let resume_from = std::fs::metadata(&partial).map(|m| m.len()).unwrap_or(0);
let mut hub = open_file(repo, &filename, resume_from)?;
let mut done = if hub.resumed { resume_from } else { 0 };
let mut out = std::fs::OpenOptions::new()
.create(true)
.write(true)
.append(hub.resumed)
.truncate(!hub.resumed)
.open(&partial)
.map_err(|e| format!("{}: {e}", partial.display()))?;
let mut buf = vec![0u8; 1 << 20];
loop {
let n = hub
.body
.read(&mut buf)
.map_err(|e| format!("reading {filename} from {repo} failed: {e}"))?;
if n == 0 {
break;
}
out.write_all(&buf[..n])
.map_err(|e| format!("{}: {e}", partial.display()))?;
done += n as u64;
on_progress(done, hub.total_bytes);
}
out.flush().map_err(|e| e.to_string())?;
out.sync_all().map_err(|e| e.to_string())?;
drop(out);
if let Some(total) = hub.total_bytes {
if done != total {
return Err(format!(
"{filename} stopped at {done} of {total} bytes. The partial file is kept \
at {}, so running this again resumes rather than restarting.",
partial.display()
));
}
}
std::fs::rename(&partial, &final_path).map_err(|e| e.to_string())?;
Ok(final_path)
}
pub fn fetch_to_dir(
repo: &str,
pattern: &str,
dir: &std::path::Path,
) -> Result<std::path::PathBuf, String> {
fetch_to_dir_with_progress(repo, pattern, dir, |_, _| {})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_content_range_yields_the_whole_file_size_not_the_slice() {
assert_eq!(content_range_total(Some("bytes 100-999/1000")), Some(1000));
}
#[test]
fn an_unknown_content_range_total_is_not_invented() {
assert_eq!(content_range_total(Some("bytes 0-99/*")), None);
assert_eq!(content_range_total(None), None);
assert_eq!(content_range_total(Some("garbage")), None);
}
#[test]
fn resolution_puts_every_ipv4_address_ahead_of_every_ipv6_one() {
let addrs = resolve_ipv4_first("localhost:443").expect("localhost must resolve");
assert!(!addrs.is_empty());
let first_v6 = addrs.iter().position(|a| !a.is_ipv4());
let last_v4 = addrs.iter().rposition(|a| a.is_ipv4());
if let (Some(first_v6), Some(last_v4)) = (first_v6, last_v4) {
assert!(
last_v4 < first_v6,
"an IPv6 address was ordered ahead of an IPv4 one: {addrs:?}"
);
}
}
#[test]
fn the_endpoint_has_no_trailing_slash_to_double_up() {
assert!(!endpoint().ends_with('/'));
}
}
#[cfg(test)]
mod hf_ref_tests {
use super::*;
#[test]
fn a_quant_tag_is_split_off_and_becomes_a_glob() {
let r = HfRef::parse("TheBloke/Mixtral-8x7B-Instruct-v0.1-GGUF:Q4_K_M");
assert_eq!(r.repo, "TheBloke/Mixtral-8x7B-Instruct-v0.1-GGUF");
assert_eq!(r.quant.as_deref(), Some("Q4_K_M"));
assert_eq!(r.pattern(), "*Q4_K_M*.gguf");
}
#[test]
fn a_bare_repo_keeps_its_whole_name_and_matches_every_gguf() {
let r = HfRef::parse("bartowski/Llama-3.2-3B-Instruct-GGUF");
assert_eq!(r.repo, "bartowski/Llama-3.2-3B-Instruct-GGUF");
assert_eq!(r.quant, None);
assert_eq!(r.pattern(), "*.gguf");
}
#[test]
fn a_degenerate_colon_is_not_a_tag() {
assert_eq!(HfRef::parse("owner/repo:").quant, None);
assert_eq!(HfRef::parse("owner/repo:").repo, "owner/repo");
assert_eq!(HfRef::parse(":Q4_K_M").quant, None);
}
#[test]
fn a_lowercase_tag_matches_an_uppercase_filename() {
let r = HfRef::parse("owner/repo:q4_k_m");
assert_eq!(r.quant.as_deref(), Some("Q4_K_M"));
assert!(glob_matches(
&r.pattern().to_ascii_lowercase(),
&"SmolLM2-135M-Instruct-Q4_K_M.gguf".to_ascii_lowercase()
));
}
#[test]
fn a_tag_does_not_match_a_longer_quant_by_accident() {
let asked = HfRef::parse("owner/repo:Q4_K_M");
assert!(!glob_matches(
&asked.pattern().to_ascii_lowercase(),
&"model-Q4_K_S.gguf".to_ascii_lowercase()
));
}
#[test]
fn the_cache_path_is_namespaced_under_hub() {
let dir = cache_dir();
assert!(
!dir.as_os_str().is_empty(),
"a cache dir must always resolve, even with no HOME"
);
}
#[test]
fn an_explicit_file_short_circuits_resolution() {
let mut r = HfRef::parse("owner/repo:Q4_K_M");
r.file = Some("exact-name.gguf".into());
assert_eq!(r.resolve().unwrap(), "exact-name.gguf");
}
}