opendal/services/huggingface/
backend.rs1use std::fmt::Debug;
19use std::fmt::Formatter;
20use std::sync::Arc;
21
22use bytes::Buf;
23use http::Response;
24use http::StatusCode;
25use log::debug;
26
27use super::core::HuggingfaceCore;
28use super::core::HuggingfaceStatus;
29use super::error::parse_error;
30use super::lister::HuggingfaceLister;
31use crate::raw::*;
32use crate::services::HuggingfaceConfig;
33use crate::*;
34
35impl Configurator for HuggingfaceConfig {
36 type Builder = HuggingfaceBuilder;
37 fn into_builder(self) -> Self::Builder {
38 HuggingfaceBuilder { config: self }
39 }
40}
41
42#[doc = include_str!("docs.md")]
44#[derive(Default, Clone)]
45pub struct HuggingfaceBuilder {
46 config: HuggingfaceConfig,
47}
48
49impl Debug for HuggingfaceBuilder {
50 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
51 let mut ds = f.debug_struct("Builder");
52
53 ds.field("config", &self.config);
54 ds.finish()
55 }
56}
57
58impl HuggingfaceBuilder {
59 pub fn repo_type(mut self, repo_type: &str) -> Self {
68 if !repo_type.is_empty() {
69 self.config.repo_type = Some(repo_type.to_string());
70 }
71 self
72 }
73
74 pub fn repo_id(mut self, repo_id: &str) -> Self {
84 if !repo_id.is_empty() {
85 self.config.repo_id = Some(repo_id.to_string());
86 }
87 self
88 }
89
90 pub fn revision(mut self, revision: &str) -> Self {
98 if !revision.is_empty() {
99 self.config.revision = Some(revision.to_string());
100 }
101 self
102 }
103
104 pub fn root(mut self, root: &str) -> Self {
108 self.config.root = if root.is_empty() {
109 None
110 } else {
111 Some(root.to_string())
112 };
113
114 self
115 }
116
117 pub fn token(mut self, token: &str) -> Self {
121 if !token.is_empty() {
122 self.config.token = Some(token.to_string());
123 }
124 self
125 }
126}
127
128impl Builder for HuggingfaceBuilder {
129 const SCHEME: Scheme = Scheme::Huggingface;
130 type Config = HuggingfaceConfig;
131
132 fn build(self) -> Result<impl Access> {
134 debug!("backend build started: {:?}", &self);
135
136 let repo_type = match self.config.repo_type.as_deref() {
137 Some("model") => Ok(RepoType::Model),
138 Some("dataset") => Ok(RepoType::Dataset),
139 Some("space") => Err(Error::new(
140 ErrorKind::ConfigInvalid,
141 "repo type \"space\" is unsupported",
142 )),
143 Some(repo_type) => Err(Error::new(
144 ErrorKind::ConfigInvalid,
145 format!("unknown repo_type: {repo_type}").as_str(),
146 )
147 .with_operation("Builder::build")
148 .with_context("service", Scheme::Huggingface)),
149 None => Ok(RepoType::Model),
150 }?;
151 debug!("backend use repo_type: {:?}", &repo_type);
152
153 let repo_id = match &self.config.repo_id {
154 Some(repo_id) => Ok(repo_id.clone()),
155 None => Err(Error::new(ErrorKind::ConfigInvalid, "repo_id is empty")
156 .with_operation("Builder::build")
157 .with_context("service", Scheme::Huggingface)),
158 }?;
159 debug!("backend use repo_id: {}", &repo_id);
160
161 let revision = match &self.config.revision {
162 Some(revision) => revision.clone(),
163 None => "main".to_string(),
164 };
165 debug!("backend use revision: {}", &revision);
166
167 let root = normalize_root(&self.config.root.unwrap_or_default());
168 debug!("backend use root: {}", &root);
169
170 let token = self.config.token.as_ref().cloned();
171
172 Ok(HuggingfaceBackend {
173 core: Arc::new(HuggingfaceCore {
174 info: {
175 let am = AccessorInfo::default();
176 am.set_scheme(Scheme::Huggingface)
177 .set_native_capability(Capability {
178 stat: true,
179
180 read: true,
181
182 list: true,
183 list_with_recursive: true,
184
185 shared: true,
186
187 ..Default::default()
188 });
189 am.into()
190 },
191 repo_type,
192 repo_id,
193 revision,
194 root,
195 token,
196 }),
197 })
198 }
199}
200
201#[derive(Debug, Clone)]
203pub struct HuggingfaceBackend {
204 core: Arc<HuggingfaceCore>,
205}
206
207impl Access for HuggingfaceBackend {
208 type Reader = HttpBody;
209 type Writer = ();
210 type Lister = oio::PageLister<HuggingfaceLister>;
211 type Deleter = ();
212
213 fn info(&self) -> Arc<AccessorInfo> {
214 self.core.info.clone()
215 }
216
217 async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> {
218 if path == "/" {
220 return Ok(RpStat::new(Metadata::new(EntryMode::DIR)));
221 }
222
223 let resp = self.core.hf_path_info(path).await?;
224
225 let status = resp.status();
226
227 match status {
228 StatusCode::OK => {
229 let mut meta = parse_into_metadata(path, resp.headers())?;
230 let bs = resp.into_body();
231
232 let decoded_response: Vec<HuggingfaceStatus> =
233 serde_json::from_reader(bs.reader()).map_err(new_json_deserialize_error)?;
234
235 if let Some(status) = decoded_response.first() {
237 if let Some(commit_info) = status.last_commit.as_ref() {
238 meta.set_last_modified(parse_datetime_from_rfc3339(
239 commit_info.date.as_str(),
240 )?);
241 }
242
243 meta.set_content_length(status.size);
244
245 match status.type_.as_str() {
246 "directory" => meta.set_mode(EntryMode::DIR),
247 "file" => meta.set_mode(EntryMode::FILE),
248 _ => return Err(Error::new(ErrorKind::Unexpected, "unknown status type")),
249 };
250 } else {
251 return Err(Error::new(ErrorKind::NotFound, "path not found"));
252 }
253
254 Ok(RpStat::new(meta))
255 }
256 _ => Err(parse_error(resp)),
257 }
258 }
259
260 async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
261 let resp = self.core.hf_resolve(path, args.range(), &args).await?;
262
263 let status = resp.status();
264
265 match status {
266 StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
267 Ok((RpRead::default(), resp.into_body()))
268 }
269 _ => {
270 let (part, mut body) = resp.into_parts();
271 let buf = body.to_buffer().await?;
272 Err(parse_error(Response::from_parts(part, buf)))
273 }
274 }
275 }
276
277 async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
278 let l = HuggingfaceLister::new(self.core.clone(), path.to_string(), args.recursive());
279
280 Ok((RpList::default(), oio::PageLister::new(l)))
281 }
282}
283
284#[derive(Debug, Clone, Copy)]
287pub enum RepoType {
288 Model,
289 Dataset,
290}