hf_hub/api/
sync.rs

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
17/// Current version (used in user-agent)
18const VERSION: &str = env!("CARGO_PKG_VERSION");
19/// Current name (used in user-agent)
20const 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
31/// Specific name for the sync part of the resumable file
32const 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/// Simple wrapper over [`ureq::Agent`] to include default headers
52#[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)]
158/// All errors the API can throw
159pub enum ApiError {
160    /// Api expects certain header to be present in the results to derive some information
161    #[error("Header {0} is missing")]
162    MissingHeader(HeaderName),
163
164    /// The header exists, but the value is not conform to what the Api expects.
165    #[error("Header {0} is invalid")]
166    InvalidHeader(HeaderName),
167
168    // /// The value cannot be used as a header during request header construction
169    // #[error("Invalid header value {0}")]
170    // InvalidHeaderValue(#[from] InvalidHeaderValue),
171
172    // /// The header value is not valid utf-8
173    // #[error("header value is not a string")]
174    // ToStr(#[from] ToStrError),
175    /// Error in the request
176    #[error("request error: {0}")]
177    RequestError(#[from] Box<ureq::Error>),
178
179    /// Error parsing some range value
180    #[error("Cannot parse int")]
181    ParseIntError(#[from] ParseIntError),
182
183    /// I/O Error
184    #[error("I/O error {0}")]
185    IoError(#[from] std::io::Error),
186
187    /// We tried to download chunk too many times
188    #[error("Too many retries: {0}")]
189    TooManyRetries(Box<ApiError>),
190
191    /// Native tls error
192    #[error("Native tls: {0}")]
193    #[cfg(feature = "native-tls")]
194    Native(#[from] native_tls::Error),
195
196    /// The part file is corrupted
197    #[error("Invalid part file - corrupted file")]
198    InvalidResume,
199
200    /// We failed to acquire lock for file `f`. Meaning
201    /// Someone else is writing/downloading said file
202    #[error("Lock acquisition failed: {0}")]
203    LockAcquisition(PathBuf),
204}
205
206/// Helper to create [`Api`] with all the options.
207#[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    /// Default api builder
225    /// ```
226    /// use hf_hub::api::sync::ApiBuilder;
227    /// let api = ApiBuilder::new().build().unwrap();
228    /// ```
229    pub fn new() -> Self {
230        let cache = Cache::default();
231        Self::from_cache(cache)
232    }
233
234    /// Creates API with values potentially from environment variables.
235    /// HF_HOME decides the location of the cache folder
236    /// HF_ENDPOINT modifies the URL for the huggingface location
237    /// to download files from.
238    /// ```
239    /// use hf_hub::api::sync::ApiBuilder;
240    /// let api = ApiBuilder::from_env().build().unwrap();
241    /// ```
242    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    /// From a given cache
252    /// ```
253    /// use hf_hub::{api::sync::ApiBuilder, Cache};
254    /// let path = std::path::PathBuf::from("/tmp");
255    /// let cache = Cache::new(path);
256    /// let api = ApiBuilder::from_cache(cache).build().unwrap();
257    /// ```
258    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    /// Wether to show a progressbar
283    pub fn with_progress(mut self, progress: bool) -> Self {
284        self.progress = progress;
285        self
286    }
287
288    /// Changes the endpoint of the API. Default is `https://huggingface.co`.
289    pub fn with_endpoint(mut self, endpoint: String) -> Self {
290        self.endpoint = endpoint;
291        self
292    }
293
294    /// Changes the location of the cache directory. Defaults is `~/.cache/huggingface/`.
295    pub fn with_cache_dir(mut self, cache_dir: PathBuf) -> Self {
296        self.cache = Cache::new(cache_dir);
297        self
298    }
299
300    /// Sets the token to be used in the API
301    pub fn with_token(mut self, token: Option<String>) -> Self {
302        self.token = token;
303        self
304    }
305
306    /// Sets the number of times the API will retry to download a file
307    pub fn with_retries(mut self, max_retries: usize) -> Self {
308        self.max_retries = max_retries;
309        self
310    }
311
312    /// Adds custom fields to headers user-agent
313    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    /// Consumes the builder and buids the final [`Api`]
334    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/// The actual Api used to interacto with the hub.
366/// Use any repo with [`Api::repo`]
367#[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                // Ignoring b, because 1 component is the filename
394                // for which we don't need to go back up for relative
395                // filename to work.
396                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    /// Creates a default Api, for Api options See [`ApiBuilder`]
441    pub fn new() -> Result<Self, ApiError> {
442        ApiBuilder::new().build()
443    }
444
445    /// Get the underlying api client
446    /// Allows for lower level access
447    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        // Closure to check if status code is a redirection
460        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        // Follow redirects until `host.is_some()` i.e. only follow relative redirects
472        // See: https://github.com/huggingface/huggingface_hub/blob/9c6af39cdce45b570f0b7f8fad2b311c96019804/src/huggingface_hub/file_download.py#L411
473        let response = loop {
474            // Check if redirect
475            if should_redirect(response.status()) {
476                // Get redirect location
477                if let Some(location) = response.header("Location") {
478                    // Parse location
479                    let uri = Uri::from_str(location).map_err(|_| InvalidHeader("location"))?;
480
481                    // Check if relative i.e. host is none
482                    if uri.host().is_none() {
483                        // Merge relative path with url
484                        let mut parts = Uri::from_str(url).unwrap().into_parts();
485                        parts.path_and_query = uri.into_parts().path_and_query;
486                        // Final uri
487                        let redirect_uri = Uri::from_parts(parts).unwrap();
488
489                        // Follow redirect
490                        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 headers = response.headers();
504        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        // Cleaning extra quotes
515        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        // The response was redirected o S3 most likely which will
522        // know about the size of the file
523        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        // Create the file and set everything properly
562
563        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        // In case of resume.
569        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    /// Creates a new handle [`ApiRepo`] which contains operations
622    /// on a particular [`Repo`]
623    pub fn repo(&self, repo: Repo) -> ApiRepo {
624        ApiRepo::new(self.clone(), repo)
625    }
626
627    /// Simple wrapper over
628    /// ```
629    /// # use hf_hub::{api::sync::Api, Repo, RepoType};
630    /// # let model_id = "gpt2".to_string();
631    /// let api = Api::new().unwrap();
632    /// let api = api.repo(Repo::new(model_id, RepoType::Model));
633    /// ```
634    pub fn model(&self, model_id: String) -> ApiRepo {
635        self.repo(Repo::new(model_id, RepoType::Model))
636    }
637
638    /// Simple wrapper over
639    /// ```
640    /// # use hf_hub::{api::sync::Api, Repo, RepoType};
641    /// # let model_id = "gpt2".to_string();
642    /// let api = Api::new().unwrap();
643    /// let api = api.repo(Repo::new(model_id, RepoType::Dataset));
644    /// ```
645    pub fn dataset(&self, model_id: String) -> ApiRepo {
646        self.repo(Repo::new(model_id, RepoType::Dataset))
647    }
648
649    /// Simple wrapper over
650    /// ```
651    /// # use hf_hub::{api::sync::Api, Repo, RepoType};
652    /// # let model_id = "gpt2".to_string();
653    /// let api = Api::new().unwrap();
654    /// let api = api.repo(Repo::new(model_id, RepoType::Space));
655    /// ```
656    pub fn space(&self, model_id: String) -> ApiRepo {
657        self.repo(Repo::new(model_id, RepoType::Space))
658    }
659}
660
661/// Shorthand for accessing things within a particular repo
662/// You can inspect repos with [`ApiRepo::info`]
663/// or download files with [`ApiRepo::download`]
664#[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    /// Get the fully qualified URL of the remote filename
690    /// ```
691    /// # use hf_hub::api::sync::Api;
692    /// let api = Api::new().unwrap();
693    /// let url = api.model("gpt2".to_string()).url("model.safetensors");
694    /// assert_eq!(url, "https://huggingface.co/gpt2/resolve/main/model.safetensors");
695    /// ```
696    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    /// This will attempt the fetch the file locally first, then [`Api.download`]
704    /// if the file is not present.
705    /// ```no_run
706    /// use hf_hub::{api::sync::Api};
707    /// let api = Api::new().unwrap();
708    /// let local_filename = api.model("gpt2".to_string()).get("model.safetensors").unwrap();
709    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    /// This function is used to download a file with a custom progress function.
718    /// It uses the [`Progress`] trait and can be used in more complex use
719    /// cases like downloading a showing progress in a UI.
720    /// ```no_run
721    /// # use hf_hub::api::{sync::Api, Progress};
722    /// struct MyProgress{
723    ///     current: usize,
724    ///     total: usize
725    /// }
726    ///
727    /// impl Progress for MyProgress{
728    ///     fn init(&mut self, size: usize, _filename: &str){
729    ///         self.total = size;
730    ///         self.current = 0;
731    ///     }
732    ///
733    ///     fn update(&mut self, size: usize){
734    ///         self.current += size;
735    ///         println!("{}/{}", self.current, self.total)
736    ///     }
737    ///
738    ///     fn finish(&mut self){
739    ///         println!("Done !");
740    ///     }
741    /// }
742    /// let api = Api::new().unwrap();
743    /// let progress = MyProgress{current: 0, total: 0};
744    /// let local_filename = api.model("gpt2".to_string()).download_with_progress("model.safetensors", progress).unwrap();
745    /// ```
746    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    /// Downloads a remote file (if not already present) into the cache directory
791    /// to be used locally.
792    /// This functions require internet access to verify if new versions of the file
793    /// exist, even if a file is already on disk at location.
794    /// ```no_run
795    /// # use hf_hub::api::sync::Api;
796    /// let api = Api::new().unwrap();
797    /// let local_filename = api.model("gpt2".to_string()).download("model.safetensors").unwrap();
798    /// ```
799    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    /// Get information about the Repo
808    /// ```
809    /// use hf_hub::{api::sync::Api};
810    /// let api = Api::new().unwrap();
811    /// api.model("gpt2".to_string()).info();
812    /// ```
813    pub fn info(&self) -> Result<RepoInfo, ApiError> {
814        Ok(self.info_request().call().map_err(Box::new)?.into_json()?)
815    }
816
817    /// Get the raw [`ureq::Request`] with the url and method already set
818    /// ```
819    /// # use hf_hub::api::sync::Api;
820    /// let api = Api::new().unwrap();
821    /// api.model("gpt2".to_owned())
822    ///     .info_request()
823    ///     .query("blobs", "true")
824    ///     .call();
825    /// ```
826    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        // Make sure the file is now seeable without connection
887        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        // We modified the sha.
927        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        // Here we prove the previous part was correctly resuming by purposefully corrupting the
939        // file.
940        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        // Not random for consistent sha corruption
944        let truncate: f32 = 0.5;
945        let new_size = (size as f32 * truncate) as u64;
946        // Truncating
947        file.set_len(new_size).unwrap();
948        // Corrupting by changing a single byte.
949        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        // We modified the sha.
960        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            // Corrupted sha
971            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                // 0..256ms sleep to randomize potential clashes
985                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        // Make sure the file is now seeable without connection
1028        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    // #[test]
1287    // fn real() {
1288    //     let api = Api::new().unwrap();
1289    //     let repo = api.model("bert-base-uncased".to_string());
1290    //     let weights = repo.get("model.safetensors").unwrap();
1291    //     let val = Sha256::digest(std::fs::read(&*weights).unwrap());
1292    //     assert_eq!(
1293    //         val[..],
1294    //         hex!("68d45e234eb4a928074dfd868cead0219ab85354cc53d20e772753c6bb9169d3")
1295    //     );
1296    // }
1297}