pub mod hf;
pub mod plan;
pub mod stale;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::{Receiver, Sender, channel};
pub(crate) const HOST: &str = "https://huggingface.co";
pub(crate) const AGENT: &str = crate::identity::USER_AGENT;
#[derive(Clone, Debug)]
pub enum DownloadMsg {
Planned {
revision: String,
files: usize,
total_bytes: u64,
already_have: u64,
},
File {
index: usize,
of: usize,
name: String,
},
Progress {
done: u64,
total: u64,
},
Done {
snapshot: PathBuf,
revision: String,
},
Failed(DownloadError),
Cancelled {
completed: usize,
of: usize,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DownloadError {
Offline(String),
Gated {
repo: String,
had_token: bool,
},
NotFound {
repo: String,
},
RateLimited,
DiskFull,
NotEnoughSpace {
need: u64,
free: u64,
},
NoSafetensors {
repo: String,
},
Http {
repo: String,
status: u16,
},
Io(String),
}
impl DownloadError {
pub fn hint(&self) -> String {
match self {
Self::Offline(e) => format!("no route to huggingface.co ({e}) — check the network"),
Self::Gated {
repo,
had_token: false,
} => format!(
"{repo} requires credentials — set HF_TOKEN, or run `hf auth login` \
(`huggingface-cli login` before huggingface_hub 1.0)"
),
Self::Gated {
repo,
had_token: true,
} => format!(
"this account has not accepted the licence for {repo} — visit https://huggingface.co/{repo}"
),
Self::NotFound { repo } => format!("no such model on the Hub: {repo}"),
Self::RateLimited => "the Hub is rate-limiting this IP — wait a few minutes".into(),
Self::DiskFull => "the disk filled up mid-download — completed files were kept".into(),
Self::NotEnoughSpace { need, free } => format!(
"needs {:.1} GB, {:.1} GB free",
*need as f64 / 1e9,
*free as f64 / 1e9
),
Self::NoSafetensors { repo } => {
format!("{repo} publishes no safetensors — Metrale Engine cannot load it")
}
Self::Http { repo, status } => format!("the Hub answered {status} for {repo}"),
Self::Io(e) => format!("could not write to the cache: {e}"),
}
}
}
pub struct Handle {
pub repo: String,
pub rx: Receiver<DownloadMsg>,
cancel: Arc<AtomicBool>,
}
impl Handle {
pub fn cancel(&self) {
self.cancel
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
pub fn start(repo: &str, cache_root: PathBuf) -> Handle {
let (tx, rx) = channel();
let cancel = Arc::new(AtomicBool::new(false));
let owned = repo.to_string();
let spawned = std::thread::Builder::new()
.name("metrale-download".into())
.spawn({
let tx = tx.clone();
let cancel = Arc::clone(&cancel);
let repo = owned.clone();
move || {
if let Err(e) = run(&repo, &cache_root, &tx, &cancel) {
let _ = tx.send(DownloadMsg::Failed(e));
}
}
});
if let Err(e) = spawned {
let _ = tx.send(DownloadMsg::Failed(DownloadError::Io(format!(
"could not start the download thread: {e}"
))));
}
Handle {
repo: repo.to_string(),
rx,
cancel,
}
}
fn run(
repo: &str,
cache_root: &std::path::Path,
tx: &Sender<DownloadMsg>,
cancel: &AtomicBool,
) -> Result<(), DownloadError> {
let tok = hf::token();
let (revision, listing) = hf::repo_info(repo, tok.as_deref())?;
let sizes = hf::sizes(repo, &revision, tok.as_deref());
let with_sizes: Vec<plan::RemoteFile> = listing
.into_iter()
.map(|mut f| {
f.size = sizes.iter().find(|(p, _)| *p == f.name).map(|(_, s)| *s);
f
})
.collect();
let files = plan::select(&with_sizes);
if !plan::has_weights(&files) {
return Err(DownloadError::NoSafetensors { repo: repo.into() });
}
let total = plan::total_bytes(&files);
let snapshot = hf::repo_dir(cache_root, repo)
.join("snapshots")
.join(&revision);
let already: u64 = files
.iter()
.filter_map(|f| {
std::fs::metadata(snapshot.join(&f.name))
.ok()
.map(|m| m.len())
})
.sum();
if let Some(free) = hf::free_bytes(cache_root) {
fits(total, already, free)?;
}
let _ = tx.send(DownloadMsg::Planned {
revision: revision.clone(),
files: files.len(),
total_bytes: total,
already_have: already,
});
let mut done_bytes = 0u64;
for (i, f) in files.iter().enumerate() {
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
let _ = tx.send(DownloadMsg::Cancelled {
completed: i,
of: files.len(),
});
return Ok(());
}
let dest = snapshot.join(&f.name);
if let Ok(m) = std::fs::metadata(&dest) {
done_bytes += m.len();
let _ = tx.send(DownloadMsg::Progress {
done: done_bytes,
total,
});
continue;
}
let _ = tx.send(DownloadMsg::File {
index: i + 1,
of: files.len(),
name: f.name.clone(),
});
let base = done_bytes;
let mut last_sent = 0u64;
let completed = hf::fetch_file(
repo,
&revision,
&f.name,
&dest,
tok.as_deref(),
cancel,
&mut |n| {
if n.saturating_sub(last_sent) >= 1024 * 1024 {
last_sent = n;
let _ = tx.send(DownloadMsg::Progress {
done: base + n,
total,
});
}
},
)?;
if !completed {
let _ = tx.send(DownloadMsg::Cancelled {
completed: i,
of: files.len(),
});
return Ok(());
}
done_bytes = base + std::fs::metadata(&dest).map(|m| m.len()).unwrap_or(0);
let _ = tx.send(DownloadMsg::Progress {
done: done_bytes,
total,
});
}
hf::publish(cache_root, repo, &revision).map_err(|e| DownloadError::Io(format!("{e:#}")))?;
let _ = tx.send(DownloadMsg::Done { snapshot, revision });
Ok(())
}
fn fits(total: u64, already: u64, free: u64) -> Result<(), DownloadError> {
let need = total.saturating_sub(already);
if need > free {
return Err(DownloadError::NotEnoughSpace { need, free });
}
Ok(())
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "net_tests.rs"]
mod net_tests;