1use super::{RepoInfo, HF_ENDPOINT};
2use crate::api::sync::ApiError::InvalidHeader;
3use crate::api::Progress;
4use crate::{Cache, Repo, RepoType};
5use http::{StatusCode, Uri};
6use indicatif::ProgressBar;
7use rand::Rng;
8use std::collections::HashMap;
9use std::io::Read;
10use std::io::Seek;
11use std::num::ParseIntError;
12use std::path::{Component, Path, PathBuf};
13use std::str::FromStr;
14use thiserror::Error;
15use ureq::{Agent, AgentBuilder, Request};
16
17const VERSION: &str = env!("CARGO_PKG_VERSION");
19const NAME: &str = env!("CARGO_PKG_NAME");
21
22const RANGE: &str = "Range";
23const CONTENT_RANGE: &str = "Content-Range";
24const LOCATION: &str = "Location";
25const USER_AGENT: &str = "User-Agent";
26const AUTHORIZATION: &str = "Authorization";
27
28type HeaderMap = HashMap<&'static str, String>;
29type HeaderName = &'static str;
30
31const EXTENSION: &str = "part";
33
34struct Wrapper<'a, P: Progress, R: Read> {
35 progress: &'a mut P,
36 inner: R,
37}
38
39fn wrap_read<P: Progress, R: Read>(inner: R, progress: &mut P) -> Wrapper<P, R> {
40 Wrapper { inner, progress }
41}
42
43impl<P: Progress, R: Read> Read for Wrapper<'_, P, R> {
44 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
45 let read = self.inner.read(buf)?;
46 self.progress.update(read);
47 Ok(read)
48 }
49}
50
51#[derive(Clone, Debug)]
53pub struct HeaderAgent {
54 agent: Agent,
55 headers: HeaderMap,
56}
57
58impl HeaderAgent {
59 fn new(agent: Agent, headers: HeaderMap) -> Self {
60 Self { agent, headers }
61 }
62
63 fn get(&self, url: &str) -> ureq::Request {
64 let mut request = self.agent.get(url);
65 for (header, value) in &self.headers {
66 request = request.set(header, value);
67 }
68 request
69 }
70}
71
72struct Handle {
73 file: std::fs::File,
74}
75
76impl Drop for Handle {
77 fn drop(&mut self) {
78 unlock(&self.file);
79 }
80}
81
82fn lock_file(mut path: PathBuf) -> Result<Handle, ApiError> {
83 path.set_extension("lock");
84
85 let file = std::fs::File::create(path.clone())?;
86 let mut res = lock(&file);
87 for _ in 0..5 {
88 if res == 0 {
89 break;
90 }
91 std::thread::sleep(std::time::Duration::from_secs(1));
92 res = lock(&file);
93 }
94 if res != 0 {
95 Err(ApiError::LockAcquisition(path))
96 } else {
97 Ok(Handle { file })
98 }
99}
100
101#[cfg(target_family = "unix")]
102mod unix {
103 use std::os::fd::AsRawFd;
104
105 pub(crate) fn lock(file: &std::fs::File) -> i32 {
106 unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }
107 }
108 pub(crate) fn unlock(file: &std::fs::File) -> i32 {
109 unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) }
110 }
111}
112#[cfg(target_family = "unix")]
113use unix::{lock, unlock};
114
115#[cfg(target_family = "windows")]
116mod windows {
117 use std::os::windows::io::AsRawHandle;
118 use windows_sys::Win32::Foundation::HANDLE;
119 use windows_sys::Win32::Storage::FileSystem::{
120 LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
121 };
122
123 pub(crate) fn lock(file: &std::fs::File) -> i32 {
124 unsafe {
125 let mut overlapped = std::mem::zeroed();
126 let flags = LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY;
127 let res = LockFileEx(
128 file.as_raw_handle() as HANDLE,
129 flags,
130 0,
131 !0,
132 !0,
133 &mut overlapped,
134 );
135 1 - res
136 }
137 }
138 pub(crate) fn unlock(file: &std::fs::File) -> i32 {
139 unsafe { UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0) }
140 }
141}
142#[cfg(target_family = "windows")]
143use windows::{lock, unlock};
144
145#[cfg(not(any(target_family = "unix", target_family = "windows")))]
146mod other {
147 pub(crate) fn lock(file: &std::fs::File) -> i32 {
148 0
149 }
150 pub(crate) fn unlock(file: &std::fs::File) -> i32 {
151 0
152 }
153}
154#[cfg(not(any(target_family = "unix", target_family = "windows")))]
155use other::{lock, unlock};
156
157#[derive(Debug, Error)]
158pub enum ApiError {
160 #[error("Header {0} is missing")]
162 MissingHeader(HeaderName),
163
164 #[error("Header {0} is invalid")]
166 InvalidHeader(HeaderName),
167
168 #[error("request error: {0}")]
177 RequestError(#[from] Box<ureq::Error>),
178
179 #[error("Cannot parse int")]
181 ParseIntError(#[from] ParseIntError),
182
183 #[error("I/O error {0}")]
185 IoError(#[from] std::io::Error),
186
187 #[error("Too many retries: {0}")]
189 TooManyRetries(Box<ApiError>),
190
191 #[error("Native tls: {0}")]
193 #[cfg(feature = "native-tls")]
194 Native(#[from] native_tls::Error),
195
196 #[error("Invalid part file - corrupted file")]
198 InvalidResume,
199
200 #[error("Lock acquisition failed: {0}")]
203 LockAcquisition(PathBuf),
204}
205
206#[derive(Debug)]
208pub struct ApiBuilder {
209 endpoint: String,
210 cache: Cache,
211 token: Option<String>,
212 max_retries: usize,
213 progress: bool,
214 user_agent: Vec<(String, String)>,
215}
216
217impl Default for ApiBuilder {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl ApiBuilder {
224 pub fn new() -> Self {
230 let cache = Cache::default();
231 Self::from_cache(cache)
232 }
233
234 pub fn from_env() -> Self {
243 let cache = Cache::from_env();
244 let mut builder = Self::from_cache(cache);
245 if let Ok(endpoint) = std::env::var(HF_ENDPOINT) {
246 builder = builder.with_endpoint(endpoint);
247 }
248 builder
249 }
250
251 pub fn from_cache(cache: Cache) -> Self {
259 let token = cache.token();
260
261 let max_retries = 0;
262 let progress = true;
263
264 let endpoint = "https://huggingface.co".to_string();
265
266 let user_agent = vec![
267 ("unknown".to_string(), "None".to_string()),
268 (NAME.to_string(), VERSION.to_string()),
269 ("rust".to_string(), "unknown".to_string()),
270 ];
271
272 Self {
273 endpoint,
274 cache,
275 token,
276 max_retries,
277 progress,
278 user_agent,
279 }
280 }
281
282 pub fn with_progress(mut self, progress: bool) -> Self {
284 self.progress = progress;
285 self
286 }
287
288 pub fn with_endpoint(mut self, endpoint: String) -> Self {
290 self.endpoint = endpoint;
291 self
292 }
293
294 pub fn with_cache_dir(mut self, cache_dir: PathBuf) -> Self {
296 self.cache = Cache::new(cache_dir);
297 self
298 }
299
300 pub fn with_token(mut self, token: Option<String>) -> Self {
302 self.token = token;
303 self
304 }
305
306 pub fn with_retries(mut self, max_retries: usize) -> Self {
308 self.max_retries = max_retries;
309 self
310 }
311
312 pub fn with_user_agent(mut self, key: &str, value: &str) -> Self {
314 self.user_agent.push((key.to_string(), value.to_string()));
315 self
316 }
317
318 fn build_headers(&self) -> HeaderMap {
319 let mut headers = HeaderMap::new();
320 let user_agent = self
321 .user_agent
322 .iter()
323 .map(|(key, value)| format!("{key}/{value}"))
324 .collect::<Vec<_>>()
325 .join("; ");
326 headers.insert(USER_AGENT, user_agent.to_string());
327 if let Some(token) = &self.token {
328 headers.insert(AUTHORIZATION, format!("Bearer {token}"));
329 }
330 headers
331 }
332
333 pub fn build(self) -> Result<Api, ApiError> {
335 let headers = self.build_headers();
336
337 let builder = builder()?;
338 let agent = builder.build();
339 let client = HeaderAgent::new(agent, headers.clone());
340
341 let no_redirect_agent = ureq::builder()
342 .try_proxy_from_env(true)
343 .redirects(0)
344 .build();
345 let no_redirect_client = HeaderAgent::new(no_redirect_agent, headers);
346
347 Ok(Api {
348 endpoint: self.endpoint,
349 cache: self.cache,
350 client,
351 no_redirect_client,
352 max_retries: self.max_retries,
353 progress: self.progress,
354 })
355 }
356}
357
358#[derive(Debug)]
359struct Metadata {
360 commit_hash: String,
361 etag: String,
362 size: usize,
363}
364
365#[derive(Clone, Debug)]
368pub struct Api {
369 endpoint: String,
370 cache: Cache,
371 client: HeaderAgent,
372 no_redirect_client: HeaderAgent,
373 max_retries: usize,
374 progress: bool,
375}
376
377fn make_relative(src: &Path, dst: &Path) -> PathBuf {
378 let path = src;
379 let base = dst;
380
381 assert_eq!(
382 path.is_absolute(),
383 base.is_absolute(),
384 "This function is made to look at absolute paths only"
385 );
386 let mut ita = path.components();
387 let mut itb = base.components();
388
389 loop {
390 match (ita.next(), itb.next()) {
391 (Some(a), Some(b)) if a == b => (),
392 (some_a, _) => {
393 let mut new_path = PathBuf::new();
397 for _ in itb {
398 new_path.push(Component::ParentDir);
399 }
400 if let Some(a) = some_a {
401 new_path.push(a);
402 for comp in ita {
403 new_path.push(comp);
404 }
405 }
406 return new_path;
407 }
408 }
409 }
410}
411
412fn symlink_or_rename(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
413 if dst.exists() {
414 return Ok(());
415 }
416
417 let rel_src = make_relative(src, dst);
418 #[cfg(target_os = "windows")]
419 {
420 if std::os::windows::fs::symlink_file(rel_src, dst).is_err() {
421 std::fs::rename(src, dst)?;
422 }
423 }
424
425 #[cfg(target_family = "unix")]
426 std::os::unix::fs::symlink(rel_src, dst)?;
427
428 Ok(())
429}
430
431fn jitter() -> usize {
432 rand::thread_rng().gen_range(0..=500)
433}
434
435fn exponential_backoff(base_wait_time: usize, n: usize, max: usize) -> usize {
436 (base_wait_time + n.pow(2) + jitter()).min(max)
437}
438
439impl Api {
440 pub fn new() -> Result<Self, ApiError> {
442 ApiBuilder::new().build()
443 }
444
445 pub fn client(&self) -> &HeaderAgent {
448 &self.client
449 }
450
451 fn metadata(&self, url: &str) -> Result<Metadata, ApiError> {
452 let mut response = self
453 .no_redirect_client
454 .get(url)
455 .set(RANGE, "bytes=0-0")
456 .call()
457 .map_err(Box::new)?;
458
459 let should_redirect = |status_code: u16| {
461 matches!(
462 StatusCode::from_u16(status_code).unwrap(),
463 StatusCode::MOVED_PERMANENTLY
464 | StatusCode::FOUND
465 | StatusCode::SEE_OTHER
466 | StatusCode::TEMPORARY_REDIRECT
467 | StatusCode::PERMANENT_REDIRECT
468 )
469 };
470
471 let response = loop {
474 if should_redirect(response.status()) {
476 if let Some(location) = response.header("Location") {
478 let uri = Uri::from_str(location).map_err(|_| InvalidHeader("location"))?;
480
481 if uri.host().is_none() {
483 let mut parts = Uri::from_str(url).unwrap().into_parts();
485 parts.path_and_query = uri.into_parts().path_and_query;
486 let redirect_uri = Uri::from_parts(parts).unwrap();
488
489 response = self
491 .no_redirect_client
492 .get(&redirect_uri.to_string())
493 .set(RANGE, "bytes=0-0")
494 .call()
495 .map_err(Box::new)?;
496 continue;
497 }
498 };
499 }
500 break response;
501 };
502
503 let header_commit = "x-repo-commit";
505 let header_linked_etag = "x-linked-etag";
506 let header_etag = "etag";
507
508 let etag = match response.header(header_linked_etag) {
509 Some(etag) => etag,
510 None => response
511 .header(header_etag)
512 .ok_or(ApiError::MissingHeader(header_etag))?,
513 };
514 let etag = etag.to_string().replace('"', "");
516 let commit_hash = response
517 .header(header_commit)
518 .ok_or(ApiError::MissingHeader(header_commit))?
519 .to_string();
520
521 let status = response.status();
524 let is_redirection = (300..400).contains(&status);
525 let response = if is_redirection {
526 self.client
527 .get(response.header(LOCATION).unwrap())
528 .set(RANGE, "bytes=0-0")
529 .call()
530 .map_err(Box::new)?
531 } else {
532 response
533 };
534 let content_range = response
535 .header(CONTENT_RANGE)
536 .ok_or(ApiError::MissingHeader(CONTENT_RANGE))?;
537
538 let size = content_range
539 .split('/')
540 .last()
541 .ok_or(ApiError::InvalidHeader(CONTENT_RANGE))?
542 .parse()?;
543 Ok(Metadata {
544 commit_hash,
545 etag,
546 size,
547 })
548 }
549
550 fn download_tempfile<P: Progress>(
551 &self,
552 url: &str,
553 size: usize,
554 mut progress: P,
555 tmp_path: PathBuf,
556 filename: &str,
557 ) -> Result<PathBuf, ApiError> {
558 progress.init(size, filename);
559 let filepath = tmp_path;
560
561 let mut file = match std::fs::OpenOptions::new().append(true).open(&filepath) {
564 Ok(f) => f,
565 Err(_) => std::fs::File::create(&filepath)?,
566 };
567
568 let start = file.metadata()?.len();
570 if start > size as u64 {
571 return Err(ApiError::InvalidResume);
572 }
573
574 let mut res = self.download_from(url, start, size, &mut file, filename, &mut progress);
575 if self.max_retries > 0 {
576 let mut i = 0;
577 while let Err(dlerr) = res {
578 let wait_time = exponential_backoff(300, i, 10_000);
579 std::thread::sleep(std::time::Duration::from_millis(wait_time as u64));
580
581 let current = file.stream_position()?;
582 res = self.download_from(url, current, size, &mut file, filename, &mut progress);
583 i += 1;
584 if i > self.max_retries {
585 return Err(ApiError::TooManyRetries(dlerr.into()));
586 }
587 }
588 }
589 res?;
590 Ok(filepath)
591 }
592
593 fn download_from<P>(
594 &self,
595 url: &str,
596 current: u64,
597 size: usize,
598 file: &mut std::fs::File,
599 filename: &str,
600 progress: &mut P,
601 ) -> Result<(), ApiError>
602 where
603 P: Progress,
604 {
605 let range = format!("bytes={current}-");
606 let response = self
607 .client
608 .get(url)
609 .set(RANGE, &range)
610 .call()
611 .map_err(Box::new)?;
612 let reader = response.into_reader();
613 progress.init(size, filename);
614 progress.update(current as usize);
615 let mut reader = Box::new(wrap_read(reader, progress));
616 std::io::copy(&mut reader, file)?;
617 progress.finish();
618 Ok(())
619 }
620
621 pub fn repo(&self, repo: Repo) -> ApiRepo {
624 ApiRepo::new(self.clone(), repo)
625 }
626
627 pub fn model(&self, model_id: String) -> ApiRepo {
635 self.repo(Repo::new(model_id, RepoType::Model))
636 }
637
638 pub fn dataset(&self, model_id: String) -> ApiRepo {
646 self.repo(Repo::new(model_id, RepoType::Dataset))
647 }
648
649 pub fn space(&self, model_id: String) -> ApiRepo {
657 self.repo(Repo::new(model_id, RepoType::Space))
658 }
659}
660
661#[derive(Debug)]
665pub struct ApiRepo {
666 api: Api,
667 repo: Repo,
668}
669
670impl ApiRepo {
671 fn new(api: Api, repo: Repo) -> Self {
672 Self { api, repo }
673 }
674}
675
676#[cfg(feature = "native-tls")]
677fn builder() -> Result<AgentBuilder, ApiError> {
678 Ok(ureq::builder()
679 .try_proxy_from_env(true)
680 .tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new()?)))
681}
682
683#[cfg(not(feature = "native-tls"))]
684fn builder() -> Result<AgentBuilder, ApiError> {
685 Ok(ureq::builder().try_proxy_from_env(true))
686}
687
688impl ApiRepo {
689 pub fn url(&self, filename: &str) -> String {
697 let endpoint = &self.api.endpoint;
698 let revision = &self.repo.url_revision();
699 let repo_id = self.repo.url();
700 format!("{endpoint}/{repo_id}/resolve/{revision}/{filename}")
701 }
702
703 pub fn get(&self, filename: &str) -> Result<PathBuf, ApiError> {
710 if let Some(path) = self.api.cache.repo(self.repo.clone()).get(filename) {
711 Ok(path)
712 } else {
713 self.download(filename)
714 }
715 }
716
717 pub fn download_with_progress<P: Progress>(
747 &self,
748 filename: &str,
749 progress: P,
750 ) -> Result<PathBuf, ApiError> {
751 let url = self.url(filename);
752 let metadata = self.api.metadata(&url)?;
753
754 let blob_path = self
755 .api
756 .cache
757 .repo(self.repo.clone())
758 .blob_path(&metadata.etag);
759 std::fs::create_dir_all(blob_path.parent().unwrap())?;
760
761 let lock = lock_file(blob_path.clone()).unwrap();
762 let mut tmp_path = blob_path.clone();
763 tmp_path.set_extension(EXTENSION);
764 let tmp_filename =
765 self.api
766 .download_tempfile(&url, metadata.size, progress, tmp_path, filename)?;
767
768 std::fs::rename(tmp_filename, &blob_path)?;
769 drop(lock);
770
771 let mut pointer_path = self
772 .api
773 .cache
774 .repo(self.repo.clone())
775 .pointer_path(&metadata.commit_hash);
776 pointer_path.push(filename);
777 std::fs::create_dir_all(pointer_path.parent().unwrap()).ok();
778
779 symlink_or_rename(&blob_path, &pointer_path)?;
780 self.api
781 .cache
782 .repo(self.repo.clone())
783 .create_ref(&metadata.commit_hash)?;
784
785 assert!(pointer_path.exists());
786
787 Ok(pointer_path)
788 }
789
790 pub fn download(&self, filename: &str) -> Result<PathBuf, ApiError> {
800 if self.api.progress {
801 self.download_with_progress(filename, ProgressBar::new(0))
802 } else {
803 self.download_with_progress(filename, ())
804 }
805 }
806
807 pub fn info(&self) -> Result<RepoInfo, ApiError> {
814 Ok(self.info_request().call().map_err(Box::new)?.into_json()?)
815 }
816
817 pub fn info_request(&self) -> Request {
827 let url = format!("{}/api/{}", self.api.endpoint, self.repo.api_url());
828 self.api.client.get(&url)
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835 use crate::api::Siblings;
836 use crate::assert_no_diff;
837 use hex_literal::hex;
838 use rand::{distributions::Alphanumeric, Rng};
839 use serde_json::{json, Value};
840 use sha2::{Digest, Sha256};
841 use std::io::{Seek, SeekFrom, Write};
842 use std::time::Duration;
843
844 struct TempDir {
845 path: PathBuf,
846 }
847
848 impl TempDir {
849 pub fn new() -> Self {
850 let s: String = rand::thread_rng()
851 .sample_iter(&Alphanumeric)
852 .take(7)
853 .map(char::from)
854 .collect();
855 let mut path = std::env::temp_dir();
856 path.push(s);
857 std::fs::create_dir(&path).unwrap();
858 Self { path }
859 }
860 }
861
862 impl Drop for TempDir {
863 fn drop(&mut self) {
864 std::fs::remove_dir_all(&self.path).unwrap()
865 }
866 }
867
868 #[test]
869 fn simple() {
870 let tmp = TempDir::new();
871 let api = ApiBuilder::new()
872 .with_progress(false)
873 .with_cache_dir(tmp.path.clone())
874 .build()
875 .unwrap();
876
877 let model_id = "julien-c/dummy-unknown".to_string();
878 let downloaded_path = api.model(model_id.clone()).download("config.json").unwrap();
879 assert!(downloaded_path.exists());
880 let val = Sha256::digest(std::fs::read(&*downloaded_path).unwrap());
881 assert_eq!(
882 val[..],
883 hex!("b908f2b7227d4d31a2105dfa31095e28d304f9bc938bfaaa57ee2cacf1f62d32")
884 );
885
886 let cache_path = api
888 .cache
889 .repo(Repo::new(model_id, RepoType::Model))
890 .get("config.json")
891 .unwrap();
892 assert_eq!(cache_path, downloaded_path);
893 }
894
895 #[test]
896 fn resume() {
897 let tmp = TempDir::new();
898 let api = ApiBuilder::new()
899 .with_progress(false)
900 .with_cache_dir(tmp.path.clone())
901 .build()
902 .unwrap();
903
904 let model_id = "julien-c/dummy-unknown".to_string();
905 let downloaded_path = api.model(model_id.clone()).download("config.json").unwrap();
906 assert!(downloaded_path.exists());
907 let val = Sha256::digest(std::fs::read(&*downloaded_path).unwrap());
908 assert_eq!(
909 val[..],
910 hex!("b908f2b7227d4d31a2105dfa31095e28d304f9bc938bfaaa57ee2cacf1f62d32")
911 );
912
913 let blob = std::fs::canonicalize(&downloaded_path).unwrap();
914 let file = std::fs::OpenOptions::new().write(true).open(&blob).unwrap();
915 let size = file.metadata().unwrap().len();
916 let truncate: f32 = rand::random();
917 let new_size = (size as f32 * truncate) as u64;
918 file.set_len(new_size).unwrap();
919 let mut blob_part = blob.clone();
920 blob_part.set_extension("part");
921 std::fs::rename(blob, &blob_part).unwrap();
922 std::fs::remove_file(&downloaded_path).unwrap();
923 let content = std::fs::read(&*blob_part).unwrap();
924 assert_eq!(content.len() as u64, new_size);
925 let val = Sha256::digest(content);
926 assert!(
928 val[..] != hex!("b908f2b7227d4d31a2105dfa31095e28d304f9bc938bfaaa57ee2cacf1f62d32")
929 );
930 let new_downloaded_path = api.model(model_id.clone()).download("config.json").unwrap();
931 let val = Sha256::digest(std::fs::read(&*new_downloaded_path).unwrap());
932 assert_eq!(downloaded_path, new_downloaded_path);
933 assert_eq!(
934 val[..],
935 hex!("b908f2b7227d4d31a2105dfa31095e28d304f9bc938bfaaa57ee2cacf1f62d32")
936 );
937
938 let blob = std::fs::canonicalize(&downloaded_path).unwrap();
941 let mut file = std::fs::OpenOptions::new().write(true).open(&blob).unwrap();
942 let size = file.metadata().unwrap().len();
943 let truncate: f32 = 0.5;
945 let new_size = (size as f32 * truncate) as u64;
946 file.set_len(new_size).unwrap();
948 file.seek(SeekFrom::Start(new_size - 1)).unwrap();
950 file.write_all(&[0]).unwrap();
951
952 let mut blob_part = blob.clone();
953 blob_part.set_extension("part");
954 std::fs::rename(blob, &blob_part).unwrap();
955 std::fs::remove_file(&downloaded_path).unwrap();
956 let content = std::fs::read(&*blob_part).unwrap();
957 assert_eq!(content.len() as u64, new_size);
958 let val = Sha256::digest(content);
959 assert!(
961 val[..] != hex!("b908f2b7227d4d31a2105dfa31095e28d304f9bc938bfaaa57ee2cacf1f62d32")
962 );
963 let new_downloaded_path = api.model(model_id.clone()).download("config.json").unwrap();
964 let val = Sha256::digest(std::fs::read(&*new_downloaded_path).unwrap());
965 assert_eq!(downloaded_path, new_downloaded_path);
966 println!("{new_downloaded_path:?}");
967 println!("Corrupted {val:#x}");
968 assert_eq!(
969 val[..],
970 hex!("32b83c94ee55a8d43d68b03a859975f6789d647342ddeb2326fcd5e0127035b5")
972 );
973 }
974
975 #[test]
976 fn locking() {
977 use std::sync::{Arc, Mutex};
978 let tmp = Arc::new(Mutex::new(TempDir::new()));
979
980 let mut handles = vec![];
981 for _ in 0..5 {
982 let tmp2 = tmp.clone();
983 let f = std::thread::spawn(move || {
984 std::thread::sleep(Duration::from_millis(rand::random::<u8>().into()));
986 let api = ApiBuilder::new()
987 .with_progress(false)
988 .with_cache_dir(tmp2.lock().unwrap().path.clone())
989 .build()
990 .unwrap();
991
992 let model_id = "julien-c/dummy-unknown".to_string();
993 api.model(model_id.clone()).download("config.json").unwrap()
994 });
995 handles.push(f);
996 }
997 while let Some(handle) = handles.pop() {
998 let downloaded_path = handle.join().unwrap();
999 assert!(downloaded_path.exists());
1000 let val = Sha256::digest(std::fs::read(&*downloaded_path).unwrap());
1001 assert_eq!(
1002 val[..],
1003 hex!("b908f2b7227d4d31a2105dfa31095e28d304f9bc938bfaaa57ee2cacf1f62d32")
1004 );
1005 }
1006 }
1007
1008 #[test]
1009 fn simple_with_retries() {
1010 let tmp = TempDir::new();
1011 let api = ApiBuilder::new()
1012 .with_progress(false)
1013 .with_cache_dir(tmp.path.clone())
1014 .with_retries(3)
1015 .build()
1016 .unwrap();
1017
1018 let model_id = "julien-c/dummy-unknown".to_string();
1019 let downloaded_path = api.model(model_id.clone()).download("config.json").unwrap();
1020 assert!(downloaded_path.exists());
1021 let val = Sha256::digest(std::fs::read(&*downloaded_path).unwrap());
1022 assert_eq!(
1023 val[..],
1024 hex!("b908f2b7227d4d31a2105dfa31095e28d304f9bc938bfaaa57ee2cacf1f62d32")
1025 );
1026
1027 let cache_path = api
1029 .cache
1030 .repo(Repo::new(model_id, RepoType::Model))
1031 .get("config.json")
1032 .unwrap();
1033 assert_eq!(cache_path, downloaded_path);
1034 }
1035
1036 #[test]
1037 fn dataset() {
1038 let tmp = TempDir::new();
1039 let api = ApiBuilder::new()
1040 .with_progress(false)
1041 .with_cache_dir(tmp.path.clone())
1042 .build()
1043 .unwrap();
1044 let repo = Repo::with_revision(
1045 "wikitext".to_string(),
1046 RepoType::Dataset,
1047 "refs/convert/parquet".to_string(),
1048 );
1049 let downloaded_path = api
1050 .repo(repo)
1051 .download("wikitext-103-v1/test/0000.parquet")
1052 .unwrap();
1053 assert!(downloaded_path.exists());
1054 let val = Sha256::digest(std::fs::read(&*downloaded_path).unwrap());
1055 assert_eq!(
1056 val[..],
1057 hex!("ABDFC9F83B1103B502924072460D4C92F277C9B49C313CEF3E48CFCF7428E125")
1058 );
1059 }
1060
1061 #[test]
1062 fn models() {
1063 let tmp = TempDir::new();
1064 let api = ApiBuilder::new()
1065 .with_progress(false)
1066 .with_cache_dir(tmp.path.clone())
1067 .build()
1068 .unwrap();
1069 let repo = Repo::with_revision(
1070 "BAAI/bGe-reRanker-Base".to_string(),
1071 RepoType::Model,
1072 "refs/pr/5".to_string(),
1073 );
1074 let downloaded_path = api.repo(repo).download("tokenizer.json").unwrap();
1075 assert!(downloaded_path.exists());
1076 let val = Sha256::digest(std::fs::read(&*downloaded_path).unwrap());
1077 assert_eq!(
1078 val[..],
1079 hex!("9EB652AC4E40CC093272BBBE0F55D521CF67570060227109B5CDC20945A4489E")
1080 );
1081 }
1082
1083 #[test]
1084 fn info() {
1085 let tmp = TempDir::new();
1086 let api = ApiBuilder::new()
1087 .with_progress(false)
1088 .with_cache_dir(tmp.path.clone())
1089 .build()
1090 .unwrap();
1091 let repo = Repo::with_revision(
1092 "wikitext".to_string(),
1093 RepoType::Dataset,
1094 "refs/convert/parquet".to_string(),
1095 );
1096 let model_info = api.repo(repo).info().unwrap();
1097 assert_eq!(
1098 model_info,
1099 RepoInfo {
1100 siblings: vec![
1101 Siblings {
1102 rfilename: ".gitattributes".to_string()
1103 },
1104 Siblings {
1105 rfilename: "wikitext-103-raw-v1/test/0000.parquet".to_string()
1106 },
1107 Siblings {
1108 rfilename: "wikitext-103-raw-v1/train/0000.parquet".to_string()
1109 },
1110 Siblings {
1111 rfilename: "wikitext-103-raw-v1/train/0001.parquet".to_string()
1112 },
1113 Siblings {
1114 rfilename: "wikitext-103-raw-v1/validation/0000.parquet".to_string()
1115 },
1116 Siblings {
1117 rfilename: "wikitext-103-v1/test/0000.parquet".to_string()
1118 },
1119 Siblings {
1120 rfilename: "wikitext-103-v1/train/0000.parquet".to_string()
1121 },
1122 Siblings {
1123 rfilename: "wikitext-103-v1/train/0001.parquet".to_string()
1124 },
1125 Siblings {
1126 rfilename: "wikitext-103-v1/validation/0000.parquet".to_string()
1127 },
1128 Siblings {
1129 rfilename: "wikitext-2-raw-v1/test/0000.parquet".to_string()
1130 },
1131 Siblings {
1132 rfilename: "wikitext-2-raw-v1/train/0000.parquet".to_string()
1133 },
1134 Siblings {
1135 rfilename: "wikitext-2-raw-v1/validation/0000.parquet".to_string()
1136 },
1137 Siblings {
1138 rfilename: "wikitext-2-v1/test/0000.parquet".to_string()
1139 },
1140 Siblings {
1141 rfilename: "wikitext-2-v1/train/0000.parquet".to_string()
1142 },
1143 Siblings {
1144 rfilename: "wikitext-2-v1/validation/0000.parquet".to_string()
1145 }
1146 ],
1147 sha: "3f68cd45302c7b4b532d933e71d9e6e54b1c7d5e".to_string()
1148 }
1149 );
1150 }
1151
1152 #[test]
1153 fn detailed_info() {
1154 let tmp = TempDir::new();
1155 let api = ApiBuilder::new()
1156 .with_progress(false)
1157 .with_token(None)
1158 .with_cache_dir(tmp.path.clone())
1159 .build()
1160 .unwrap();
1161 let repo = Repo::with_revision(
1162 "mcpotato/42-eicar-street".to_string(),
1163 RepoType::Model,
1164 "8b3861f6931c4026b0cd22b38dbc09e7668983ac".to_string(),
1165 );
1166 let blobs_info: Value = api
1167 .repo(repo)
1168 .info_request()
1169 .query("blobs", "true")
1170 .call()
1171 .unwrap()
1172 .into_json()
1173 .unwrap();
1174 assert_no_diff!(
1175 blobs_info,
1176 json!({
1177 "_id": "621ffdc136468d709f17ddb4",
1178 "author": "mcpotato",
1179 "createdAt": "2022-03-02T23:29:05.000Z",
1180 "disabled": false,
1181 "downloads": 0,
1182 "gated": false,
1183 "id": "mcpotato/42-eicar-street",
1184 "lastModified": "2022-11-30T19:54:16.000Z",
1185 "likes": 2,
1186 "modelId": "mcpotato/42-eicar-street",
1187 "private": false,
1188 "sha": "8b3861f6931c4026b0cd22b38dbc09e7668983ac",
1189 "siblings": [
1190 {
1191 "blobId": "6d34772f5ca361021038b404fb913ec8dc0b1a5a",
1192 "rfilename": ".gitattributes",
1193 "size": 1175
1194 },
1195 {
1196 "blobId": "be98037f7c542112c15a1d2fc7e2a2427e42cb50",
1197 "rfilename": "build_pickles.py",
1198 "size": 304
1199 },
1200 {
1201 "blobId": "8acd02161fff53f9df9597e377e22b04bc34feff",
1202 "rfilename": "danger.dat",
1203 "size": 66
1204 },
1205 {
1206 "blobId": "86b812515e075a1ae216e1239e615a1d9e0b316e",
1207 "rfilename": "eicar_test_file",
1208 "size": 70
1209 },
1210 {
1211 "blobId": "86b812515e075a1ae216e1239e615a1d9e0b316e",
1212 "rfilename": "eicar_test_file_bis",
1213 "size":70
1214 },
1215 {
1216 "blobId": "cd1c6d8bde5006076655711a49feae66f07d707e",
1217 "lfs": {
1218 "pointerSize": 127,
1219 "sha256": "f9343d7d7ec5c3d8bcced056c438fc9f1d3819e9ca3d42418a40857050e10e20",
1220 "size": 22
1221 },
1222 "rfilename": "pytorch_model.bin",
1223 "size": 22
1224 },
1225 {
1226 "blobId": "8ab39654695136173fee29cba0193f679dfbd652",
1227 "rfilename": "supposedly_safe.pkl",
1228 "size": 31
1229 }
1230 ],
1231 "spaces": [],
1232 "tags": ["pytorch", "region:us"],
1233 "usedStorage": 22
1234 })
1235 );
1236 }
1237
1238 #[test]
1239 fn endpoint() {
1240 let api = ApiBuilder::new().build().unwrap();
1241 assert_eq!(api.endpoint, "https://huggingface.co".to_string());
1242 let fake_endpoint = "https://fake_endpoint.com".to_string();
1243 let api = ApiBuilder::new()
1244 .with_endpoint(fake_endpoint.clone())
1245 .build()
1246 .unwrap();
1247 assert_eq!(api.endpoint, fake_endpoint);
1248 }
1249
1250 #[test]
1251 fn headers_with_token() {
1252 let api = ApiBuilder::new()
1253 .with_token(Some("token".to_string()))
1254 .build()
1255 .unwrap();
1256 let headers = api.client.headers;
1257 assert_eq!(
1258 headers.get("Authorization"),
1259 Some(&"Bearer token".to_string())
1260 );
1261 }
1262
1263 #[test]
1264 fn headers_default() {
1265 let api = ApiBuilder::new().build().unwrap();
1266 let headers = api.client.headers;
1267 assert_eq!(
1268 headers.get(USER_AGENT),
1269 Some(&"unknown/None; hf-hub/0.4.1; rust/unknown".to_string())
1270 );
1271 }
1272
1273 #[test]
1274 fn headers_custom() {
1275 let api = ApiBuilder::new()
1276 .with_user_agent("origin", "custom")
1277 .build()
1278 .unwrap();
1279 let headers = api.client.headers;
1280 assert_eq!(
1281 headers.get(USER_AGENT),
1282 Some(&"unknown/None; hf-hub/0.4.1; rust/unknown; origin/custom".to_string())
1283 );
1284 }
1285
1286 }