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 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('/'));
}
}