#![cfg(feature = "gemma-default")]
use std::path::{Path, PathBuf};
use atomr_infer_core::error::{InferenceError, InferenceResult};
#[derive(Debug, Clone)]
pub struct HfCache {
pub home: PathBuf,
pub hub_cache: PathBuf,
pub token_path: PathBuf,
}
impl HfCache {
pub fn resolve() -> InferenceResult<Self> {
let home = if let Some(h) = env_path("HF_HOME") {
h
} else if let Some(xdg) = env_path("XDG_CACHE_HOME") {
xdg.join("huggingface")
} else if let Some(home_dir) = dirs::home_dir() {
home_dir.join(".cache").join("huggingface")
} else {
return Err(InferenceError::Internal(
"hf-cache: no HF_HOME / XDG_CACHE_HOME / $HOME — cannot resolve cache".into(),
));
};
let hub_cache = env_path("HF_HUB_CACHE").unwrap_or_else(|| home.join("hub"));
let token_path = home.join("token");
Ok(Self {
home,
hub_cache,
token_path,
})
}
pub fn discover_token(&self) -> InferenceResult<Option<String>> {
if let Some(t) = env_string("HF_TOKEN") {
return Ok(Some(t));
}
if let Some(t) = env_string("HUGGING_FACE_HUB_TOKEN") {
return Ok(Some(t));
}
if self.token_path.exists() {
let raw = std::fs::read_to_string(&self.token_path).map_err(|e| {
InferenceError::Internal(format!(
"hf-cache: failed to read {}: {e}",
self.token_path.display()
))
})?;
let trimmed = raw.trim();
if !trimmed.is_empty() {
return Ok(Some(trimmed.to_string()));
}
}
Ok(None)
}
pub fn free_bytes(&self) -> Option<u64> {
free_bytes_at(&self.hub_cache)
.or_else(|| free_bytes_at(&self.home))
.or_else(|| free_bytes_at(self.home.parent().unwrap_or(Path::new("/"))))
}
}
fn env_path(var: &str) -> Option<PathBuf> {
env_string(var).map(PathBuf::from)
}
fn env_string(var: &str) -> Option<String> {
std::env::var(var)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
#[cfg(unix)]
fn free_bytes_at(path: &Path) -> Option<u64> {
let p = path.ancestors().find(|p| p.exists())?;
let out = std::process::Command::new("df")
.arg("--output=avail")
.arg("-B1")
.arg(p)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&out.stdout);
stdout.lines().nth(1)?.trim().parse::<u64>().ok()
}
#[cfg(not(unix))]
fn free_bytes_at(_path: &Path) -> Option<u64> {
None
}
#[cfg(test)]
mod tests {
use super::*;
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
#[test]
fn resolves_with_explicit_hf_home() {
let _g = env_lock();
let tmp = tempfile::tempdir().expect("tempdir");
std::env::set_var("HF_HOME", tmp.path());
std::env::remove_var("HF_HUB_CACHE");
let c = HfCache::resolve().expect("resolve");
assert_eq!(c.home, tmp.path());
assert_eq!(c.hub_cache, tmp.path().join("hub"));
assert_eq!(c.token_path, tmp.path().join("token"));
}
#[test]
fn explicit_hub_cache_overrides_default() {
let _g = env_lock();
let home = tempfile::tempdir().expect("tempdir");
let hub = tempfile::tempdir().expect("tempdir");
std::env::set_var("HF_HOME", home.path());
std::env::set_var("HF_HUB_CACHE", hub.path());
let c = HfCache::resolve().expect("resolve");
assert_eq!(c.hub_cache, hub.path());
std::env::remove_var("HF_HUB_CACHE");
}
#[test]
fn token_discovery_env_var_wins() {
let _g = env_lock();
let tmp = tempfile::tempdir().expect("tempdir");
std::env::set_var("HF_HOME", tmp.path());
std::env::set_var("HF_TOKEN", "hf_test123");
let c = HfCache::resolve().expect("resolve");
assert_eq!(c.discover_token().expect("ok"), Some("hf_test123".into()));
std::env::remove_var("HF_TOKEN");
}
#[test]
fn token_discovery_falls_back_to_file() {
let _g = env_lock();
let tmp = tempfile::tempdir().expect("tempdir");
std::env::set_var("HF_HOME", tmp.path());
std::env::remove_var("HF_TOKEN");
std::env::remove_var("HUGGING_FACE_HUB_TOKEN");
let token_path = tmp.path().join("token");
std::fs::write(&token_path, "hf_from_file\n").expect("write");
let c = HfCache::resolve().expect("resolve");
assert_eq!(
c.discover_token().expect("ok"),
Some("hf_from_file".into())
);
}
#[test]
fn token_discovery_returns_none_when_absent() {
let _g = env_lock();
let tmp = tempfile::tempdir().expect("tempdir");
std::env::set_var("HF_HOME", tmp.path());
std::env::remove_var("HF_TOKEN");
std::env::remove_var("HUGGING_FACE_HUB_TOKEN");
let c = HfCache::resolve().expect("resolve");
assert_eq!(c.discover_token().expect("ok"), None);
}
}