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::GithubCore;
29use super::core::parse_error;
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
165    fn info(&self) -> ServiceInfo {
166        self.core.info.clone()
167    }
168
169    fn capability(&self) -> Capability {
170        self.core.capability
171    }
172
173    async fn create_dir(
174        &self,
175        ctx: &OperationContext,
176        path: &str,
177        _: OpCreateDir,
178    ) -> Result<RpCreateDir> {
179        let empty_bytes = Buffer::new();
180
181        let resp = self
182            .core
183            .upload(ctx, &format!("{path}.gitkeep"), empty_bytes)
184            .await?;
185
186        let status = resp.status();
187
188        match status {
189            StatusCode::OK | StatusCode::CREATED => Ok(RpCreateDir::default()),
190            _ => Err(parse_error(resp)),
191        }
192    }
193
194    async fn stat(&self, ctx: &OperationContext, path: &str, _args: OpStat) -> Result<RpStat> {
195        let resp = self.core.stat(ctx, path).await?;
196
197        let status = resp.status();
198
199        match status {
200            StatusCode::OK => {
201                let body = resp.into_body();
202                let resp: Entry =
203                    serde_json::from_reader(body.reader()).map_err(new_json_deserialize_error)?;
204
205                let m = if resp.type_field == "dir" {
206                    Metadata::new(EntryMode::DIR)
207                } else {
208                    Metadata::new(EntryMode::FILE)
209                        .with_content_length(resp.size)
210                        .with_etag(resp.sha)
211                };
212
213                Ok(RpStat::new(m))
214            }
215            _ => Err(parse_error(resp)),
216        }
217    }
218    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
219        let output: oio::StreamReader<GithubReader> = {
220            Ok(oio::StreamReader::new(GithubReader::new(
221                self.clone(),
222                ctx.clone(),
223                path,
224                args,
225            )))
226        }?;
227
228        Ok(output)
229    }
230
231    fn write(&self, ctx: &OperationContext, path: &str, _args: OpWrite) -> Result<Self::Writer> {
232        let output: GithubWriters = {
233            let writer = GithubWriter::new(self.core.clone(), ctx.clone(), path.to_string());
234
235            let w = oio::OneShotWriter::new(writer);
236
237            Ok(w)
238        }?;
239
240        Ok(output)
241    }
242
243    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
244        let output: oio::OneShotDeleter<GithubDeleter> = {
245            Ok(oio::OneShotDeleter::new(GithubDeleter::new(
246                self.core.clone(),
247                ctx.clone(),
248            )))
249        }?;
250
251        Ok(output)
252    }
253
254    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
255        let output: oio::PageLister<GithubLister> = {
256            let l = GithubLister::new(self.core.clone(), ctx.clone(), path, args.recursive());
257            Ok(oio::PageLister::new(l))
258        }?;
259
260        Ok(output)
261    }
262
263    fn copy(
264        &self,
265        _ctx: &OperationContext,
266        _from: &str,
267        _to: &str,
268        _args: OpCopy,
269        _opts: OpCopier,
270    ) -> Result<Self::Copier> {
271        Err(Error::new(
272            ErrorKind::Unsupported,
273            "operation is not supported",
274        ))
275    }
276
277    async fn rename(
278        &self,
279        _ctx: &OperationContext,
280        _from: &str,
281        _to: &str,
282        _args: OpRename,
283    ) -> Result<RpRename> {
284        Err(Error::new(
285            ErrorKind::Unsupported,
286            "operation is not supported",
287        ))
288    }
289
290    async fn presign(
291        &self,
292        _ctx: &OperationContext,
293        _path: &str,
294        _args: OpPresign,
295    ) -> Result<RpPresign> {
296        Err(Error::new(
297            ErrorKind::Unsupported,
298            "operation is not supported",
299        ))
300    }
301}