1use hf_hub::progress::{DownloadEvent, ProgressEvent, ProgressHandler};
4use hf_hub::{
5 HFClientSync, HFError, HFRepositorySync, HFResult, RepoType, RepoTypeDataset, RepoTypeModel,
6};
7use std::io::{IsTerminal, Write};
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11#[derive(Clone, Debug)]
13pub struct Api(HFClientSync);
14
15impl Api {
16 pub fn new() -> HFResult<Self> {
18 Ok(Self(HFClientSync::new()?))
19 }
20
21 pub fn with_cache_dir<P: Into<PathBuf>>(cache_dir: P) -> HFResult<Self> {
22 let client = hf_hub::HFClient::builder().cache_dir(cache_dir).build()?;
23 Ok(Self(HFClientSync::from_inner(client)?))
24 }
25
26 pub fn model(&self, id: impl AsRef<str>) -> Repo<RepoTypeModel> {
28 let (owner, name) = hf_hub::split_id(id.as_ref());
29 Repo::new(self.0.model(owner, name))
30 }
31
32 pub fn dataset(&self, id: impl AsRef<str>) -> Repo<RepoTypeDataset> {
34 let (owner, name) = hf_hub::split_id(id.as_ref());
35 Repo::new(self.0.dataset(owner, name))
36 }
37}
38
39impl AsRef<HFClientSync> for Api {
40 fn as_ref(&self) -> &HFClientSync {
41 &self.0
42 }
43}
44
45#[derive(Clone, Debug)]
47pub struct Repo<T: RepoType> {
48 repo: HFRepositorySync<T>,
49 revision: Option<String>,
50}
51
52impl<T: RepoType> Repo<T> {
53 pub fn new(repo: HFRepositorySync<T>) -> Self {
54 Self {
55 repo,
56 revision: None,
57 }
58 }
59
60 pub fn with_revision<S: AsRef<str>>(mut self, revision: S) -> Self {
62 self.revision = Some(revision.as_ref().to_string());
63 self
64 }
65
66 pub fn revision(&self) -> Option<&str> {
67 self.revision.as_deref()
68 }
69
70 pub fn get<S: AsRef<str>>(&self, filename: S) -> HFResult<PathBuf> {
74 let filename = filename.as_ref();
75 let download = self
76 .repo
77 .download_file()
78 .filename(filename)
79 .maybe_revision(self.revision.clone());
80 match download.clone().local_files_only(true).send() {
81 Err(HFError::LocalEntryNotFound { .. }) => {
82 download.progress(StderrProgress::new(filename)).send()
83 }
84 cached => cached,
85 }
86 }
87}
88
89impl<T: RepoType> AsRef<HFRepositorySync<T>> for Repo<T> {
90 fn as_ref(&self) -> &HFRepositorySync<T> {
91 &self.repo
92 }
93}
94
95struct StderrProgress {
100 filename: String,
101 downloaded: AtomicU64,
102 total: AtomicU64,
103 drawn: AtomicU64,
105 tty: bool,
107}
108
109const NOT_DRAWN: u64 = u64::MAX;
110
111impl StderrProgress {
112 fn new(filename: &str) -> Self {
113 Self {
114 filename: filename.to_string(),
115 downloaded: AtomicU64::new(0),
116 total: AtomicU64::new(0),
117 drawn: AtomicU64::new(NOT_DRAWN),
118 tty: std::io::stderr().is_terminal(),
119 }
120 }
121
122 fn draw(&self) {
123 let total = self.total.load(Ordering::Relaxed);
124 if total == 0 {
125 return;
126 }
127 let downloaded = self.downloaded.load(Ordering::Relaxed).min(total);
128 let percent = downloaded * 100 / total;
129 let step = if self.tty { percent } else { percent / 10 };
130 if self.drawn.swap(step, Ordering::Relaxed) == step {
131 return;
132 }
133 let line = progress_line(self.tty, &self.filename, percent, downloaded, total);
134 let mut stderr = std::io::stderr().lock();
135 let _ = stderr.write_all(line.as_bytes());
136 let _ = stderr.flush();
137 }
138}
139
140fn progress_line(tty: bool, filename: &str, percent: u64, downloaded: u64, total: u64) -> String {
142 let (unit, scale) = human_unit(total);
143 let (lead, trail) = if tty { ("\r", " ") } else { ("", "\n") };
144 format!(
145 "{lead}{filename}: {percent:>3}% ({:.1}/{:.1} {unit}){trail}",
146 downloaded as f64 / scale,
147 total as f64 / scale,
148 )
149}
150
151fn human_unit(bytes: u64) -> (&'static str, f64) {
153 const KIB: f64 = 1024.;
154 const MIB: f64 = KIB * 1024.;
155 const GIB: f64 = MIB * 1024.;
156 match bytes as f64 {
157 b if b >= GIB => ("GiB", GIB),
158 b if b >= MIB => ("MiB", MIB),
159 b if b >= KIB => ("KiB", KIB),
160 _ => ("B", 1.),
161 }
162}
163
164impl ProgressHandler for StderrProgress {
165 fn on_progress(&self, event: &ProgressEvent) {
166 let ProgressEvent::Download(event) = event else {
167 return;
168 };
169 match event {
170 DownloadEvent::Start { total_bytes, .. } => {
171 self.total.store(*total_bytes, Ordering::Relaxed);
172 }
173 DownloadEvent::Progress { files } => {
174 if let Some(file) = files.first() {
175 self.downloaded
176 .store(file.bytes_completed, Ordering::Relaxed);
177 }
178 self.draw();
179 }
180 DownloadEvent::AggregateProgress {
181 bytes_completed,
182 total_bytes,
183 ..
184 } => {
185 self.downloaded.store(*bytes_completed, Ordering::Relaxed);
186 self.total.store(*total_bytes, Ordering::Relaxed);
187 self.draw();
188 }
189 DownloadEvent::Complete => {
190 if self.tty && self.drawn.load(Ordering::Relaxed) != NOT_DRAWN {
191 let _ = writeln!(std::io::stderr());
192 }
193 }
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::{human_unit, progress_line};
201
202 #[test]
203 fn terminal_progress_redraws_in_place() {
204 let line = progress_line(
205 true,
206 "model.safetensors",
207 42,
208 42 * 1024 * 1024,
209 100 * 1024 * 1024,
210 );
211 assert_eq!(line, "\rmodel.safetensors: 42% (42.0/100.0 MiB) ");
212 assert!(!line.contains('\n'), "a redraw must not end the line");
213 }
214
215 #[test]
216 fn redirected_progress_writes_lines() {
217 let line = progress_line(
218 false,
219 "model.safetensors",
220 42,
221 42 * 1024 * 1024,
222 100 * 1024 * 1024,
223 );
224 assert_eq!(line, "model.safetensors: 42% (42.0/100.0 MiB)\n");
225 assert!(!line.contains('\r'), "a log must not get carriage returns");
226 }
227
228 #[test]
229 fn verify_human_units() {
230 assert_eq!(human_unit(612).0, "B");
231 assert_eq!(human_unit(4 * 1024).0, "KiB");
232 assert_eq!(human_unit(4 * 1024 * 1024).0, "MiB");
233 assert_eq!(human_unit(4 * 1024 * 1024 * 1024).0, "GiB");
234 }
235}