1use crate::{Error, Result, bundle};
4use hf_hub::progress::{DownloadEvent, ProgressEvent, ProgressHandler};
5use hf_hub::{HFClientSync, HFError};
6use std::{
7 io::{IsTerminal, Write},
8 path::PathBuf,
9 sync::{Mutex, OnceLock},
10};
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Repository {
15 pub owner: String,
17 pub name: String,
19 pub revision: Option<String>,
21}
22
23impl Repository {
24 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
26 Self {
27 owner: owner.into(),
28 name: name.into(),
29 revision: None,
30 }
31 }
32
33 #[must_use]
35 pub fn at(mut self, revision: impl Into<String>) -> Self {
36 self.revision = Some(revision.into());
37 self
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct HubFile {
44 pub name: String,
46 pub sha256: Option<String>,
48}
49
50impl HubFile {
51 pub fn new(name: impl Into<String>) -> Self {
53 Self {
54 name: name.into(),
55 sha256: None,
56 }
57 }
58
59 #[must_use]
61 pub fn sha256(mut self, digest: impl Into<String>) -> Self {
62 self.sha256 = Some(digest.into());
63 self
64 }
65}
66
67#[derive(Clone, Debug)]
69pub struct Resolver {
70 repository: Repository,
71 offline: bool,
72 progress: Option<bool>,
73}
74
75impl Resolver {
76 pub fn new(repository: Repository) -> Self {
78 Self {
79 repository,
80 offline: false,
81 progress: None,
82 }
83 }
84
85 #[must_use]
87 pub fn offline(mut self, offline: bool) -> Self {
88 self.offline = offline;
89 self
90 }
91
92 #[must_use]
95 pub fn progress(mut self, progress: bool) -> Self {
96 self.progress = Some(progress);
97 self
98 }
99
100 pub fn resolve(&self, file: &HubFile) -> Result<PathBuf> {
102 if let Some(path) = self.local(file)? {
103 return Ok(path);
104 }
105 if self.offline || environment_true("HF_HUB_OFFLINE") {
106 return Err(Error::Message(format!(
107 "{}/{}/{} is not in the Hugging Face cache, and downloading is off",
108 self.repository.owner, self.repository.name, file.name
109 )));
110 }
111 let repository = client()?.model(&self.repository.owner, &self.repository.name);
112 let path = repository
113 .download_file()
114 .filename(&file.name)
115 .maybe_revision(self.repository.revision.clone())
116 .maybe_progress(self.show_progress().then(DownloadBar::default))
117 .send()
118 .map_err(|error| self.fetch_error(file, error))?;
119 self.verify(file, path)
120 }
121
122 pub fn local(&self, file: &HubFile) -> Result<Option<PathBuf>> {
124 let result = client()?
125 .model(&self.repository.owner, &self.repository.name)
126 .download_file()
127 .filename(&file.name)
128 .maybe_revision(self.repository.revision.clone())
129 .local_files_only(true)
130 .send();
131 match result {
132 Ok(path) => self.verify(file, path).map(Some),
133 Err(HFError::LocalEntryNotFound { .. }) => Ok(None),
134 Err(error) => Err(self.fetch_error(file, error)),
135 }
136 }
137
138 pub fn resolve_all(&self, files: &[HubFile]) -> Result<Vec<PathBuf>> {
140 files.iter().map(|file| self.resolve(file)).collect()
141 }
142
143 fn verify(&self, file: &HubFile, path: PathBuf) -> Result<PathBuf> {
144 if let Some(expected) = &file.sha256 {
145 let actual = bundle::file_digest(&path)
146 .map_err(|error| error.context(format!("hashing {}", path.display())))?;
147 if &actual != expected {
148 return Err(Error::Message(format!(
149 "{} has SHA-256 {actual}, expected {expected}",
150 path.display()
151 )));
152 }
153 }
154 Ok(path)
155 }
156
157 fn show_progress(&self) -> bool {
158 self.progress.unwrap_or_else(|| {
159 std::io::stderr().is_terminal() && !environment_true("HF_HUB_DISABLE_PROGRESS_BARS")
160 })
161 }
162
163 fn fetch_error(&self, file: &HubFile, error: HFError) -> Error {
164 Error::Message(format!(
165 "cannot fetch {}/{}/{}{}: {error}",
166 self.repository.owner,
167 self.repository.name,
168 file.name,
169 self.repository
170 .revision
171 .as_deref()
172 .map_or(String::new(), |revision| format!(" at {revision}"))
173 ))
174 }
175}
176
177fn client() -> Result<&'static HFClientSync> {
178 static CLIENT: OnceLock<std::result::Result<HFClientSync, String>> = OnceLock::new();
179 CLIENT
180 .get_or_init(|| HFClientSync::new().map_err(|error| error.to_string()))
181 .as_ref()
182 .map_err(|error| Error::Message(format!("cannot initialize Hugging Face client: {error}")))
183}
184
185fn environment_true(name: &str) -> bool {
186 std::env::var(name).is_ok_and(|value| {
187 ["1", "ON", "YES", "TRUE"]
188 .iter()
189 .any(|truth| value.eq_ignore_ascii_case(truth))
190 })
191}
192
193#[derive(Default)]
194struct DownloadBar {
195 total: Mutex<u64>,
196}
197
198impl ProgressHandler for DownloadBar {
199 fn on_progress(&self, event: &ProgressEvent) {
200 let ProgressEvent::Download(event) = event else {
201 return;
202 };
203 let mut stderr = std::io::stderr().lock();
204 match event {
205 DownloadEvent::Start { total_bytes, .. } => {
206 *self.total.lock().unwrap_or_else(|error| error.into_inner()) = *total_bytes;
207 }
208 DownloadEvent::AggregateProgress {
209 bytes_completed,
210 total_bytes,
211 ..
212 } => {
213 let total = if *total_bytes == 0 {
214 *self.total.lock().unwrap_or_else(|error| error.into_inner())
215 } else {
216 *total_bytes
217 };
218 if total > 0 {
219 let _ = write!(
220 stderr,
221 "\rfetching {:5.1}% of {:.2} GB",
222 100.0 * *bytes_completed as f64 / total as f64,
223 total as f64 / 1e9
224 );
225 let _ = stderr.flush();
226 }
227 }
228 DownloadEvent::Complete => {
229 let _ = writeln!(stderr, "\rfetched ");
230 }
231 DownloadEvent::Progress { .. } => {}
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 #[test]
239 fn documented_boolean_values_are_recognized() {
240 for value in ["1", "ON", "on", "YES", "yes", "TRUE", "true"] {
241 assert!(
242 ["1", "ON", "YES", "TRUE"]
243 .iter()
244 .any(|truth| value.eq_ignore_ascii_case(truth))
245 );
246 }
247 }
248}