opendal/services/huggingface/
backend.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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 super::DEFAULT_SCHEME;
32use crate::raw::*;
33use crate::services::HuggingfaceConfig;
34use crate::*;
35impl Configurator for HuggingfaceConfig {
36    type Builder = HuggingfaceBuilder;
37    fn into_builder(self) -> Self::Builder {
38        HuggingfaceBuilder { config: self }
39    }
40}
41
42/// [Huggingface](https://huggingface.co/docs/huggingface_hub/package_reference/hf_api)'s API support.
43#[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    /// Set repo type of this backend. Default is model.
60    ///
61    /// Available values:
62    /// - model
63    /// - dataset
64    ///
65    /// Currently, only models and datasets are supported.
66    /// [Reference](https://huggingface.co/docs/hub/repositories)
67    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    /// Set repo id of this backend. This is required.
75    ///
76    /// Repo id consists of the account name and the repository name.
77    ///
78    /// For example, model's repo id looks like:
79    /// - meta-llama/Llama-2-7b
80    ///
81    /// Dataset's repo id looks like:
82    /// - databricks/databricks-dolly-15k
83    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    /// Set revision of this backend. Default is main.
91    ///
92    /// Revision can be a branch name or a commit hash.
93    ///
94    /// For example, revision can be:
95    /// - main
96    /// - 1d0c4eb
97    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    /// Set root of this backend.
105    ///
106    /// All operations will happen under this root.
107    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    /// Set the token of this backend.
118    ///
119    /// This is optional.
120    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    type Config = HuggingfaceConfig;
130
131    /// Build a HuggingfaceBackend.
132    fn build(self) -> Result<impl Access> {
133        debug!("backend build started: {:?}", &self);
134
135        let repo_type = match self.config.repo_type.as_deref() {
136            Some("model") => Ok(RepoType::Model),
137            Some("dataset") => Ok(RepoType::Dataset),
138            Some("space") => Err(Error::new(
139                ErrorKind::ConfigInvalid,
140                "repo type \"space\" is unsupported",
141            )),
142            Some(repo_type) => Err(Error::new(
143                ErrorKind::ConfigInvalid,
144                format!("unknown repo_type: {repo_type}").as_str(),
145            )
146            .with_operation("Builder::build")
147            .with_context("service", Scheme::Huggingface)),
148            None => Ok(RepoType::Model),
149        }?;
150        debug!("backend use repo_type: {:?}", &repo_type);
151
152        let repo_id = match &self.config.repo_id {
153            Some(repo_id) => Ok(repo_id.clone()),
154            None => Err(Error::new(ErrorKind::ConfigInvalid, "repo_id is empty")
155                .with_operation("Builder::build")
156                .with_context("service", Scheme::Huggingface)),
157        }?;
158        debug!("backend use repo_id: {}", &repo_id);
159
160        let revision = match &self.config.revision {
161            Some(revision) => revision.clone(),
162            None => "main".to_string(),
163        };
164        debug!("backend use revision: {}", &revision);
165
166        let root = normalize_root(&self.config.root.unwrap_or_default());
167        debug!("backend use root: {}", &root);
168
169        let token = self.config.token.as_ref().cloned();
170
171        Ok(HuggingfaceBackend {
172            core: Arc::new(HuggingfaceCore {
173                info: {
174                    let am = AccessorInfo::default();
175                    am.set_scheme(DEFAULT_SCHEME)
176                        .set_native_capability(Capability {
177                            stat: true,
178
179                            read: true,
180
181                            list: true,
182                            list_with_recursive: true,
183
184                            shared: true,
185
186                            ..Default::default()
187                        });
188                    am.into()
189                },
190                repo_type,
191                repo_id,
192                revision,
193                root,
194                token,
195            }),
196        })
197    }
198}
199
200/// Backend for Huggingface service
201#[derive(Debug, Clone)]
202pub struct HuggingfaceBackend {
203    core: Arc<HuggingfaceCore>,
204}
205
206impl Access for HuggingfaceBackend {
207    type Reader = HttpBody;
208    type Writer = ();
209    type Lister = oio::PageLister<HuggingfaceLister>;
210    type Deleter = ();
211
212    fn info(&self) -> Arc<AccessorInfo> {
213        self.core.info.clone()
214    }
215
216    async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> {
217        // Stat root always returns a DIR.
218        if path == "/" {
219            return Ok(RpStat::new(Metadata::new(EntryMode::DIR)));
220        }
221
222        let resp = self.core.hf_path_info(path).await?;
223
224        let status = resp.status();
225
226        match status {
227            StatusCode::OK => {
228                let mut meta = parse_into_metadata(path, resp.headers())?;
229                let bs = resp.into_body();
230
231                let decoded_response: Vec<HuggingfaceStatus> =
232                    serde_json::from_reader(bs.reader()).map_err(new_json_deserialize_error)?;
233
234                // NOTE: if the file is not found, the server will return 200 with an empty array
235                if let Some(status) = decoded_response.first() {
236                    if let Some(commit_info) = status.last_commit.as_ref() {
237                        meta.set_last_modified(parse_datetime_from_rfc3339(
238                            commit_info.date.as_str(),
239                        )?);
240                    }
241
242                    meta.set_content_length(status.size);
243
244                    match status.type_.as_str() {
245                        "directory" => meta.set_mode(EntryMode::DIR),
246                        "file" => meta.set_mode(EntryMode::FILE),
247                        _ => return Err(Error::new(ErrorKind::Unexpected, "unknown status type")),
248                    };
249                } else {
250                    return Err(Error::new(ErrorKind::NotFound, "path not found"));
251                }
252
253                Ok(RpStat::new(meta))
254            }
255            _ => Err(parse_error(resp)),
256        }
257    }
258
259    async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
260        let resp = self.core.hf_resolve(path, args.range(), &args).await?;
261
262        let status = resp.status();
263
264        match status {
265            StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
266                Ok((RpRead::default(), resp.into_body()))
267            }
268            _ => {
269                let (part, mut body) = resp.into_parts();
270                let buf = body.to_buffer().await?;
271                Err(parse_error(Response::from_parts(part, buf)))
272            }
273        }
274    }
275
276    async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
277        let l = HuggingfaceLister::new(self.core.clone(), path.to_string(), args.recursive());
278
279        Ok((RpList::default(), oio::PageLister::new(l)))
280    }
281}
282
283/// Repository type of Huggingface. Currently, we only support `model` and `dataset`.
284/// [Reference](https://huggingface.co/docs/hub/repositories)
285#[derive(Debug, Clone, Copy)]
286pub enum RepoType {
287    Model,
288    Dataset,
289}