Skip to main content

hf_hub/repository/
listing.rs

1//! Repository content listing and metadata lookups.
2//!
3//! Builders on [`HFRepository`] for inspecting what's in a repo without downloading file
4//! contents:
5//!
6//! - [`HFRepository::list_tree`] — paginated stream of [`RepoTreeEntry`] (files and directories), optionally recursive
7//!   and prefix-filtered.
8//! - [`HFRepository::get_paths_info`] — batched lookup of metadata for a known set of paths.
9//! - [`HFRepository::get_file_metadata`] — HEAD-based metadata for one file (size, ETag, commit hash, xet hash).
10
11use bon::bon;
12use futures::stream::Stream;
13use reqwest::Url;
14
15use super::files::{extract_commit_hash, extract_etag, extract_file_size, extract_xet_hash};
16use super::{FileMetadataInfo, HFRepository, RepoTreeEntry, RepoType};
17use crate::client::encode_ref;
18use crate::error::{HFError, HFResult};
19use crate::{constants, retry};
20
21#[bon]
22impl<T: RepoType> HFRepository<T> {
23    /// Stream file and directory entries in the repository tree.
24    ///
25    /// Returns `HFResult<impl Stream<Item = HFResult<RepoTreeEntry>>>`.
26    ///
27    /// Use [`HFRepository::get_paths_info`] when you already know the exact paths
28    /// you want to inspect.
29    ///
30    /// # Parameters
31    ///
32    /// - `revision`: Git revision to list. Defaults to the main branch.
33    /// - `path_in_repo`: repository-relative subdirectory to list. Defaults to the repo root. Leading, trailing, and
34    ///   consecutive `/` separators are ignored; path segments are URL-encoded automatically.
35    /// - `recursive` (default `false`): traverse subdirectories.
36    /// - `expand` (default `false`): include per-file metadata such as size, LFS info, and last-commit summaries.
37    /// - `limit`: cap the total number of entries yielded.
38    #[builder(finish_fn = send, derive(Debug, Clone))]
39    pub fn list_tree(
40        &self,
41        /// Git revision to list. Defaults to the main branch.
42        #[builder(into)]
43        revision: Option<String>,
44        /// Repository-relative subdirectory to list. Defaults to the repo root.
45        /// Leading, trailing, and consecutive `/` separators are ignored;
46        /// path segments are URL-encoded automatically.
47        #[builder(into)]
48        path_in_repo: Option<String>,
49        /// Traverse subdirectories.
50        #[builder(default)]
51        recursive: bool,
52        /// Include per-file metadata such as size, LFS info, and last-commit summaries.
53        #[builder(default)]
54        expand: bool,
55        /// Cap the total number of entries yielded.
56        limit: Option<usize>,
57    ) -> HFResult<impl Stream<Item = HFResult<RepoTreeEntry>> + '_> {
58        let revision = revision.as_deref().unwrap_or(constants::DEFAULT_REVISION);
59        let url_str = format!(
60            "{}/tree/{}",
61            self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
62            encode_ref(revision)
63        );
64        let mut url = Url::parse(&url_str)?;
65        if let Some(path) = path_in_repo.as_deref() {
66            crate::client::append_path_segments(&mut url, path)?;
67        }
68
69        let mut query: Vec<(String, String)> = Vec::new();
70        if recursive {
71            query.push(("recursive".into(), "true".into()));
72        }
73        if expand {
74            query.push(("expand".into(), "true".into()));
75        }
76
77        Ok(self.hf_client.paginate(url, query, limit))
78    }
79
80    /// Get info about specific paths in a repository.
81    ///
82    /// Prefer this over [`HFRepository::list_tree`] when you already know the
83    /// small set of paths you want to inspect.
84    ///
85    /// Endpoint: `POST /api/{repo_type}s/{repo_id}/paths-info/{revision}`.
86    ///
87    /// # Parameters
88    ///
89    /// - `paths` (required): paths in the repository to fetch info for.
90    /// - `revision`: Git revision. Defaults to the main branch.
91    #[builder(finish_fn = send, derive(Debug, Clone))]
92    pub async fn get_paths_info(
93        &self,
94        /// Paths in the repository to fetch info for.
95        paths: Vec<String>,
96        /// Git revision. Defaults to the main branch.
97        #[builder(into)]
98        revision: Option<String>,
99    ) -> HFResult<Vec<RepoTreeEntry>> {
100        let revision = revision.as_deref().unwrap_or(constants::DEFAULT_REVISION);
101        let url = format!(
102            "{}/paths-info/{}",
103            self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
104            encode_ref(revision)
105        );
106
107        let body = serde_json::json!({ "paths": paths });
108
109        let headers = self.hf_client.auth_headers();
110        let response = retry::retry(self.hf_client.retry_config(), || {
111            self.hf_client
112                .http_client()
113                .post(&url)
114                .headers(headers.clone())
115                .json(&body)
116                .send()
117        })
118        .await?;
119
120        let repo_path = self.repo_path();
121        let response = self
122            .hf_client
123            .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Entry { path: paths.join(", ") })
124            .await?;
125        Ok(response.json().await?)
126    }
127
128    /// Fetch metadata for a single file via a HEAD request on its resolve URL.
129    ///
130    /// Returns the resolved commit hash, ETag, file size, and (if the file is Xet-backed)
131    /// the Xet content hash — without downloading the file contents.
132    ///
133    /// Endpoint: `HEAD {endpoint}/{prefix}{repo_id}/resolve/{revision}/{filepath}`.
134    ///
135    /// # Parameters
136    ///
137    /// - `filepath` (required): path of the file to inspect within the repository.
138    /// - `revision`: Git revision. Defaults to the main branch.
139    #[builder(finish_fn = send, derive(Debug, Clone))]
140    pub async fn get_file_metadata(
141        &self,
142        /// Path of the file to inspect within the repository.
143        #[builder(into)]
144        filepath: String,
145        /// Git revision. Defaults to the main branch.
146        revision: Option<&str>,
147    ) -> HFResult<FileMetadataInfo> {
148        let filename = filepath;
149        let revision = revision.unwrap_or(constants::DEFAULT_REVISION);
150        let repo_path = self.repo_path();
151        let url = self
152            .hf_client
153            .download_url(self.repo_type.url_prefix(), &repo_path, revision, &filename)?;
154
155        let headers = self.hf_client.auth_headers();
156        let response = retry::retry(self.hf_client.retry_config(), || {
157            self.hf_client.http_client().head(&url).headers(headers.clone()).send()
158        })
159        .await?;
160        let response = self
161            .hf_client
162            .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Entry { path: filename.clone() })
163            .await?;
164
165        let etag = extract_etag(&response).ok_or_else(|| {
166            HFError::malformed_response_at(format!("missing ETag header for {filename}"), url.to_string())
167        })?;
168        let commit_hash = extract_commit_hash(&response).ok_or_else(|| {
169            HFError::malformed_response_at(format!("missing X-Repo-Commit header for {filename}"), url.to_string())
170        })?;
171        let xet_hash = extract_xet_hash(&response);
172        let file_size = extract_file_size(&response).unwrap_or_else(|| {
173            tracing::warn!(
174                file = %filename,
175                "missing or invalid Content-Length/X-Linked-Size header, defaulting file size to 0"
176            );
177            0
178        });
179        let location = Some(response.url().to_string());
180
181        Ok(FileMetadataInfo {
182            filename,
183            etag,
184            commit_hash,
185            xet_hash,
186            file_size,
187            location,
188        })
189    }
190}
191
192#[cfg(feature = "blocking")]
193use futures::stream::StreamExt as _;
194
195#[cfg(feature = "blocking")]
196#[bon]
197impl<T: RepoType> crate::blocking::HFRepositorySync<T> {
198    /// Blocking counterpart of [`HFRepository::list_tree`]. Returns the collected stream as a
199    /// `Vec<RepoTreeEntry>`. See the async method for parameters and behavior.
200    #[builder(finish_fn = send, derive(Debug, Clone))]
201    pub fn list_tree(
202        &self,
203        #[builder(into)] revision: Option<String>,
204        #[builder(into)] path_in_repo: Option<String>,
205        #[builder(default)] recursive: bool,
206        #[builder(default)] expand: bool,
207        limit: Option<usize>,
208    ) -> HFResult<Vec<RepoTreeEntry>> {
209        self.runtime.block_on(async move {
210            let stream = self
211                .inner
212                .list_tree()
213                .maybe_revision(revision)
214                .maybe_path_in_repo(path_in_repo)
215                .recursive(recursive)
216                .expand(expand)
217                .maybe_limit(limit)
218                .send()?;
219            futures::pin_mut!(stream);
220            let mut items = Vec::new();
221            while let Some(item) = stream.next().await {
222                items.push(item?);
223            }
224            Ok(items)
225        })
226    }
227
228    /// Blocking counterpart of [`HFRepository::get_paths_info`]. See the async method for
229    /// parameters and behavior.
230    #[builder(finish_fn = send, derive(Debug, Clone))]
231    pub fn get_paths_info(
232        &self,
233        paths: Vec<String>,
234        #[builder(into)] revision: Option<String>,
235    ) -> HFResult<Vec<RepoTreeEntry>> {
236        self.runtime
237            .block_on(self.inner.get_paths_info().paths(paths).maybe_revision(revision).send())
238    }
239
240    /// Blocking counterpart of [`HFRepository::get_file_metadata`]. See the async method for
241    /// parameters and behavior.
242    #[builder(finish_fn = send, derive(Debug, Clone))]
243    pub fn get_file_metadata(
244        &self,
245        #[builder(into)] filepath: String,
246        revision: Option<&str>,
247    ) -> HFResult<FileMetadataInfo> {
248        self.runtime.block_on(
249            self.inner
250                .get_file_metadata()
251                .filepath(filepath)
252                .maybe_revision(revision)
253                .send(),
254        )
255    }
256}