Skip to main content

opendal_service_github/
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::sync::Arc;
20
21use bytes::Buf;
22use http::StatusCode;
23use log::debug;
24
25use super::GITHUB_SCHEME;
26use super::config::GithubConfig;
27use super::core::Entry;
28use super::core::parse_error;
29use super::core::{ErrorContext, GithubCore};
30use super::deleter::GithubDeleter;
31use super::lister::GithubLister;
32use super::reader::*;
33use super::writer::GithubWriter;
34use super::writer::GithubWriters;
35use opendal_core::raw::*;
36use opendal_core::*;
37
38/// [github contents](https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#create-or-update-file-contents) services support.
39#[doc = include_str!("docs.md")]
40#[derive(Default)]
41pub struct GithubBuilder {
42    pub(super) config: GithubConfig,
43}
44
45impl Debug for GithubBuilder {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("GithubBuilder")
48            .field("config", &self.config)
49            .finish_non_exhaustive()
50    }
51}
52
53impl GithubBuilder {
54    /// Set root of this backend.
55    ///
56    /// All operations will happen under this root.
57    pub fn root(mut self, root: &str) -> Self {
58        self.config.root = if root.is_empty() {
59            None
60        } else {
61            Some(root.to_string())
62        };
63
64        self
65    }
66
67    /// Github access_token.
68    ///
69    /// required.
70    pub fn token(mut self, token: &str) -> Self {
71        if !token.is_empty() {
72            self.config.token = Some(token.to_string());
73        }
74        self
75    }
76
77    /// Set Github repo owner.
78    pub fn owner(mut self, owner: &str) -> Self {
79        self.config.owner = owner.to_string();
80
81        self
82    }
83
84    /// Set Github repo name.
85    pub fn repo(mut self, repo: &str) -> Self {
86        self.config.repo = repo.to_string();
87
88        self
89    }
90}
91
92impl Builder for GithubBuilder {
93    type Config = GithubConfig;
94
95    /// Builds the backend and returns the result of GithubBackend.
96    fn build(self) -> Result<impl Service> {
97        debug!("backend build started: {:?}", self);
98
99        let root = normalize_root(&self.config.root.clone().unwrap_or_default());
100        debug!("backend use root {}", root);
101
102        // Handle owner.
103        if self.config.owner.is_empty() {
104            return Err(Error::new(ErrorKind::ConfigInvalid, "owner is empty")
105                .with_operation("Builder::build")
106                .with_context("service", GITHUB_SCHEME));
107        }
108
109        debug!("backend use owner {}", self.config.owner);
110
111        // Handle repo.
112        if self.config.repo.is_empty() {
113            return Err(Error::new(ErrorKind::ConfigInvalid, "repo is empty")
114                .with_operation("Builder::build")
115                .with_context("service", GITHUB_SCHEME));
116        }
117
118        debug!("backend use repo {}", self.config.repo);
119
120        Ok(GithubBackend {
121            core: Arc::new(GithubCore {
122                info: ServiceInfo::new(GITHUB_SCHEME, &root, ""),
123                capability: Capability {
124                    stat: true,
125
126                    read: true,
127                    read_with_suffix: true,
128
129                    create_dir: true,
130
131                    write: true,
132                    write_can_empty: true,
133
134                    delete: true,
135
136                    list: true,
137                    list_with_recursive: true,
138
139                    shared: true,
140
141                    ..Default::default()
142                },
143                root,
144                token: self.config.token.clone(),
145                owner: self.config.owner.clone(),
146                repo: self.config.repo.clone(),
147            }),
148        })
149    }
150}
151
152/// Backend for Github services.
153#[derive(Debug, Clone)]
154pub struct GithubBackend {
155    pub(crate) core: Arc<GithubCore>,
156}
157
158impl Service for GithubBackend {
159    type Reader = oio::StreamReader<GithubReader>;
160    type Writer = GithubWriters;
161    type Lister = oio::PageLister<GithubLister>;
162    type Deleter = oio::OneShotDeleter<GithubDeleter>;
163    type Copier = ();
164    type Composer = ();
165
166    fn info(&self) -> ServiceInfo {
167        self.core.info.clone()
168    }
169
170    fn capability(&self) -> Capability {
171        self.core.capability
172    }
173
174    async fn create_dir(
175        &self,
176        ctx: &OperationContext,
177        path: &str,
178        _: OpCreateDir,
179    ) -> Result<RpCreateDir> {
180        let empty_bytes = Buffer::new();
181
182        let resp = self
183            .core
184            .upload(ctx, &format!("{path}.gitkeep"), empty_bytes)
185            .await?;
186
187        let status = resp.status();
188
189        match status {
190            StatusCode::OK | StatusCode::CREATED => Ok(RpCreateDir::default()),
191            _ => Err(parse_error(
192                ErrorContext::new(ServiceOperation("CreateOrUpdateFileContents")),
193                resp,
194            )),
195        }
196    }
197
198    async fn stat(&self, ctx: &OperationContext, path: &str, _args: OpStat) -> Result<RpStat> {
199        let resp = self.core.stat(ctx, path).await?;
200
201        let status = resp.status();
202
203        match status {
204            StatusCode::OK => {
205                let body = resp.into_body();
206                let resp: Entry =
207                    serde_json::from_reader(body.reader()).map_err(new_json_deserialize_error)?;
208
209                let m = if resp.type_field == "dir" {
210                    MetadataBuilder::dir().build()
211                } else {
212                    {
213                        let mut metadata = MetadataBuilder::file(resp.size);
214                        metadata.etag(resp.sha);
215                        metadata.build()
216                    }
217                };
218
219                Ok(RpStat::new(m))
220            }
221            _ => Err(parse_error(
222                ErrorContext::new(ServiceOperation("GetRepositoryContent")),
223                resp,
224            )),
225        }
226    }
227    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
228        let output: oio::StreamReader<GithubReader> = {
229            Ok(oio::StreamReader::new(GithubReader::new(
230                self.clone(),
231                ctx.clone(),
232                path,
233                args,
234            )))
235        }?;
236
237        Ok(output)
238    }
239
240    fn write(&self, ctx: &OperationContext, path: &str, _args: OpWrite) -> Result<Self::Writer> {
241        let output: GithubWriters = {
242            let writer = GithubWriter::new(self.core.clone(), ctx.clone(), path.to_string());
243
244            let w = oio::OneShotWriter::new(writer);
245
246            Ok(w)
247        }?;
248
249        Ok(output)
250    }
251
252    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
253        let output: oio::OneShotDeleter<GithubDeleter> = {
254            Ok(oio::OneShotDeleter::new(GithubDeleter::new(
255                self.core.clone(),
256                ctx.clone(),
257            )))
258        }?;
259
260        Ok(output)
261    }
262
263    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
264        let output: oio::PageLister<GithubLister> = {
265            let l = GithubLister::new(self.core.clone(), ctx.clone(), path, args.recursive());
266            Ok(oio::PageLister::new(l))
267        }?;
268
269        Ok(output)
270    }
271
272    fn copy(
273        &self,
274        _ctx: &OperationContext,
275        _from: &str,
276        _to: &str,
277        _args: OpCopy,
278    ) -> Result<Self::Copier> {
279        Err(Error::new(
280            ErrorKind::Unsupported,
281            "operation is not supported",
282        ))
283    }
284
285    async fn rename(
286        &self,
287        _ctx: &OperationContext,
288        _from: &str,
289        _to: &str,
290        _args: OpRename,
291    ) -> Result<RpRename> {
292        Err(Error::new(
293            ErrorKind::Unsupported,
294            "operation is not supported",
295        ))
296    }
297
298    async fn presign(
299        &self,
300        _ctx: &OperationContext,
301        _path: &str,
302        _args: OpPresign,
303    ) -> Result<RpPresign> {
304        Err(Error::new(
305            ErrorKind::Unsupported,
306            "operation is not supported",
307        ))
308    }
309}