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(crate) 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| crate::admin::glob_matches(pattern, n))
.ok_or_else(|| format!("no file in {repo} matches '{pattern}'"))
}
pub(crate) struct HubFile {
pub(crate) body: Box<dyn Read + Send>,
pub(crate) total_bytes: Option<u64>,
pub(crate) resumed: bool,
}
pub(crate) 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()
}
#[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('/'));
}
}