opendal/services/github/
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::GITHUB_SCHEME;
28use super::core::Entry;
29use super::core::GithubCore;
30use super::delete::GithubDeleter;
31use super::error::parse_error;
32use super::lister::GithubLister;
33use super::writer::GithubWriter;
34use super::writer::GithubWriters;
35use crate::raw::*;
36use crate::services::GithubConfig;
37use crate::*;
38
39#[doc = include_str!("docs.md")]
41#[derive(Default)]
42pub struct GithubBuilder {
43    pub(super) config: GithubConfig,
44
45    #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")]
46    pub(super) http_client: Option<HttpClient>,
47}
48
49impl Debug for GithubBuilder {
50    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
51        let mut d = f.debug_struct("GithubBuilder");
52
53        d.field("config", &self.config);
54        d.finish_non_exhaustive()
55    }
56}
57
58impl GithubBuilder {
59    pub fn root(mut self, root: &str) -> Self {
63        self.config.root = if root.is_empty() {
64            None
65        } else {
66            Some(root.to_string())
67        };
68
69        self
70    }
71
72    pub fn token(mut self, token: &str) -> Self {
76        if !token.is_empty() {
77            self.config.token = Some(token.to_string());
78        }
79        self
80    }
81
82    pub fn owner(mut self, owner: &str) -> Self {
84        self.config.owner = owner.to_string();
85
86        self
87    }
88
89    pub fn repo(mut self, repo: &str) -> Self {
91        self.config.repo = repo.to_string();
92
93        self
94    }
95
96    #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")]
103    #[allow(deprecated)]
104    pub fn http_client(mut self, client: HttpClient) -> Self {
105        self.http_client = Some(client);
106        self
107    }
108}
109
110impl Builder for GithubBuilder {
111    type Config = GithubConfig;
112
113    fn build(self) -> Result<impl Access> {
115        debug!("backend build started: {:?}", &self);
116
117        let root = normalize_root(&self.config.root.clone().unwrap_or_default());
118        debug!("backend use root {}", &root);
119
120        if self.config.owner.is_empty() {
122            return Err(Error::new(ErrorKind::ConfigInvalid, "owner is empty")
123                .with_operation("Builder::build")
124                .with_context("service", Scheme::Github));
125        }
126
127        debug!("backend use owner {}", &self.config.owner);
128
129        if self.config.repo.is_empty() {
131            return Err(Error::new(ErrorKind::ConfigInvalid, "repo is empty")
132                .with_operation("Builder::build")
133                .with_context("service", Scheme::Github));
134        }
135
136        debug!("backend use repo {}", &self.config.repo);
137
138        Ok(GithubBackend {
139            core: Arc::new(GithubCore {
140                info: {
141                    let am = AccessorInfo::default();
142                    am.set_scheme(GITHUB_SCHEME)
143                        .set_root(&root)
144                        .set_native_capability(Capability {
145                            stat: true,
146
147                            read: true,
148
149                            create_dir: true,
150
151                            write: true,
152                            write_can_empty: true,
153
154                            delete: true,
155
156                            list: true,
157                            list_with_recursive: true,
158
159                            shared: true,
160
161                            ..Default::default()
162                        });
163
164                    #[allow(deprecated)]
166                    if let Some(client) = self.http_client {
167                        am.update_http_client(|_| client);
168                    }
169
170                    am.into()
171                },
172                root,
173                token: self.config.token.clone(),
174                owner: self.config.owner.clone(),
175                repo: self.config.repo.clone(),
176            }),
177        })
178    }
179}
180
181#[derive(Debug, Clone)]
183pub struct GithubBackend {
184    core: Arc<GithubCore>,
185}
186
187impl Access for GithubBackend {
188    type Reader = HttpBody;
189    type Writer = GithubWriters;
190    type Lister = oio::PageLister<GithubLister>;
191    type Deleter = oio::OneShotDeleter<GithubDeleter>;
192
193    fn info(&self) -> Arc<AccessorInfo> {
194        self.core.info.clone()
195    }
196
197    async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result<RpCreateDir> {
198        let empty_bytes = Buffer::new();
199
200        let resp = self
201            .core
202            .upload(&format!("{path}.gitkeep"), empty_bytes)
203            .await?;
204
205        let status = resp.status();
206
207        match status {
208            StatusCode::OK | StatusCode::CREATED => Ok(RpCreateDir::default()),
209            _ => Err(parse_error(resp)),
210        }
211    }
212
213    async fn stat(&self, path: &str, _args: OpStat) -> Result<RpStat> {
214        let resp = self.core.stat(path).await?;
215
216        let status = resp.status();
217
218        match status {
219            StatusCode::OK => {
220                let body = resp.into_body();
221                let resp: Entry =
222                    serde_json::from_reader(body.reader()).map_err(new_json_deserialize_error)?;
223
224                let m = if resp.type_field == "dir" {
225                    Metadata::new(EntryMode::DIR)
226                } else {
227                    Metadata::new(EntryMode::FILE)
228                        .with_content_length(resp.size)
229                        .with_etag(resp.sha)
230                };
231
232                Ok(RpStat::new(m))
233            }
234            _ => Err(parse_error(resp)),
235        }
236    }
237
238    async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
239        let resp = self.core.get(path, args.range()).await?;
240
241        let status = resp.status();
242
243        match status {
244            StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
245                Ok((RpRead::default(), resp.into_body()))
246            }
247            _ => {
248                let (part, mut body) = resp.into_parts();
249                let buf = body.to_buffer().await?;
250                Err(parse_error(Response::from_parts(part, buf)))
251            }
252        }
253    }
254
255    async fn write(&self, path: &str, _args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
256        let writer = GithubWriter::new(self.core.clone(), path.to_string());
257
258        let w = oio::OneShotWriter::new(writer);
259
260        Ok((RpWrite::default(), w))
261    }
262
263    async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> {
264        Ok((
265            RpDelete::default(),
266            oio::OneShotDeleter::new(GithubDeleter::new(self.core.clone())),
267        ))
268    }
269
270    async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
271        let l = GithubLister::new(self.core.clone(), path, args.recursive());
272        Ok((RpList::default(), oio::PageLister::new(l)))
273    }
274}