1use base64::Engine;
19use bytes::Buf;
20use bytes::Bytes;
21use http::header;
22use http::request;
23use http::Request;
24use http::Response;
25use http::StatusCode;
26use serde::Deserialize;
27use serde::Serialize;
28use std::fmt::Debug;
29use std::fmt::Formatter;
30use std::sync::Arc;
31
32use super::error::parse_error;
33use crate::raw::*;
34use crate::*;
35
36#[derive(Clone)]
38pub struct GithubCore {
39 pub info: Arc<AccessorInfo>,
40 pub root: String,
42 pub token: Option<String>,
44 pub owner: String,
46 pub repo: String,
48}
49
50impl Debug for GithubCore {
51 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("Backend")
53 .field("root", &self.root)
54 .field("owner", &self.owner)
55 .field("repo", &self.repo)
56 .finish_non_exhaustive()
57 }
58}
59
60impl GithubCore {
61 #[inline]
62 pub async fn send(&self, req: Request<Buffer>) -> Result<Response<Buffer>> {
63 self.info.http_client().send(req).await
64 }
65
66 pub fn sign(&self, req: request::Builder) -> Result<request::Builder> {
67 let mut req = req
68 .header(header::USER_AGENT, format!("opendal-{}", VERSION))
69 .header("X-GitHub-Api-Version", "2022-11-28");
70
71 if let Some(token) = &self.token {
73 req = req.header(
74 header::AUTHORIZATION,
75 format_authorization_by_bearer(token)?,
76 )
77 }
78
79 Ok(req)
80 }
81}
82
83impl GithubCore {
84 pub async fn get_file_sha(&self, path: &str) -> Result<Option<String>> {
85 if self.token.is_none() {
87 return Err(Error::new(
88 ErrorKind::PermissionDenied,
89 "Github access_token is not set",
90 ));
91 }
92
93 let resp = self.stat(path).await?;
94
95 match resp.status() {
96 StatusCode::OK => {
97 let body = resp.into_body();
98 let resp: Entry =
99 serde_json::from_reader(body.reader()).map_err(new_json_deserialize_error)?;
100
101 Ok(Some(resp.sha))
102 }
103 StatusCode::NOT_FOUND => Ok(None),
104 _ => Err(parse_error(resp)),
105 }
106 }
107
108 pub async fn stat(&self, path: &str) -> Result<Response<Buffer>> {
109 let path = build_abs_path(&self.root, path);
110
111 let url = format!(
112 "https://api.github.com/repos/{}/{}/contents/{}",
113 self.owner,
114 self.repo,
115 percent_encode_path(&path)
116 );
117
118 let req = Request::get(url);
119
120 let req = req.extension(Operation::Stat);
121
122 let req = self.sign(req)?;
123
124 let req = req
125 .header("Accept", "application/vnd.github.object+json")
126 .body(Buffer::new())
127 .map_err(new_request_build_error)?;
128
129 self.send(req).await
130 }
131
132 pub async fn get(&self, path: &str, range: BytesRange) -> Result<Response<HttpBody>> {
133 let path = build_abs_path(&self.root, path);
134
135 let url = format!(
136 "https://api.github.com/repos/{}/{}/contents/{}",
137 self.owner,
138 self.repo,
139 percent_encode_path(&path)
140 );
141
142 let req = Request::get(url);
143
144 let req = req.extension(Operation::Read);
145
146 let req = self.sign(req)?;
147
148 let req = req
149 .header(header::ACCEPT, "application/vnd.github.raw+json")
150 .header(header::RANGE, range.to_header())
151 .body(Buffer::new())
152 .map_err(new_request_build_error)?;
153
154 self.info.http_client().fetch(req).await
155 }
156
157 pub async fn upload(&self, path: &str, bs: Buffer) -> Result<Response<Buffer>> {
158 let sha = self.get_file_sha(path).await?;
159
160 let path = build_abs_path(&self.root, path);
161
162 let url = format!(
163 "https://api.github.com/repos/{}/{}/contents/{}",
164 self.owner,
165 self.repo,
166 percent_encode_path(&path)
167 );
168
169 let req = Request::put(url);
170
171 let req = req.extension(Operation::Write);
172
173 let req = self.sign(req)?;
174
175 let mut req_body = CreateOrUpdateContentsRequest {
176 message: format!("Write {} at {} via opendal", path, chrono::Local::now()),
177 content: base64::engine::general_purpose::STANDARD.encode(bs.to_bytes()),
178 sha: None,
179 };
180
181 if let Some(sha) = sha {
182 req_body.sha = Some(sha);
183 }
184
185 let req_body = serde_json::to_vec(&req_body).map_err(new_json_serialize_error)?;
186
187 let req = req
188 .header("Accept", "application/vnd.github+json")
189 .body(Buffer::from(req_body))
190 .map_err(new_request_build_error)?;
191
192 self.send(req).await
193 }
194
195 pub async fn delete(&self, path: &str) -> Result<()> {
196 let formatted_path = format!("{}.gitkeep", path);
198 let p = if path.ends_with('/') {
199 formatted_path.as_str()
200 } else {
201 path
202 };
203
204 let Some(sha) = self.get_file_sha(p).await? else {
205 return Ok(());
206 };
207
208 let path = build_abs_path(&self.root, p);
209
210 let url = format!(
211 "https://api.github.com/repos/{}/{}/contents/{}",
212 self.owner,
213 self.repo,
214 percent_encode_path(&path)
215 );
216
217 let req = Request::delete(url);
218
219 let req = req.extension(Operation::Delete);
220
221 let req = self.sign(req)?;
222
223 let req_body = DeleteContentsRequest {
224 message: format!("Delete {} at {} via opendal", path, chrono::Local::now()),
225 sha,
226 };
227
228 let req_body = serde_json::to_vec(&req_body).map_err(new_json_serialize_error)?;
229
230 let req = req
231 .header("Accept", "application/vnd.github.object+json")
232 .body(Buffer::from(Bytes::from(req_body)))
233 .map_err(new_request_build_error)?;
234
235 let resp = self.send(req).await?;
236
237 match resp.status() {
238 StatusCode::OK => Ok(()),
239 StatusCode::NOT_FOUND => Ok(()),
240 _ => Err(parse_error(resp)),
241 }
242 }
243
244 pub async fn list(&self, path: &str) -> Result<ListResponse> {
245 let path = build_abs_path(&self.root, path);
246
247 let url = format!(
248 "https://api.github.com/repos/{}/{}/contents/{}",
249 self.owner,
250 self.repo,
251 percent_encode_path(&path)
252 );
253
254 let req = Request::get(url);
255
256 let req = req.extension(Operation::List);
257
258 let req = self.sign(req)?;
259
260 let req = req
261 .header("Accept", "application/vnd.github.object+json")
262 .body(Buffer::new())
263 .map_err(new_request_build_error)?;
264
265 let resp = self.send(req).await?;
266
267 match resp.status() {
268 StatusCode::OK => {
269 let body = resp.into_body();
270 let resp: ListResponse =
271 serde_json::from_reader(body.reader()).map_err(new_json_deserialize_error)?;
272
273 Ok(resp)
274 }
275 StatusCode::NOT_FOUND => Ok(ListResponse::default()),
276 _ => Err(parse_error(resp)),
277 }
278 }
279
280 pub async fn list_with_recursive(&self, git_url: &str) -> Result<Vec<Tree>> {
282 let url = format!("{}?recursive=true", git_url);
283
284 let req = Request::get(url);
285
286 let req = req.extension(Operation::List);
287
288 let req = self.sign(req)?;
289
290 let req = req
291 .header("Accept", "application/vnd.github.object+json")
292 .body(Buffer::new())
293 .map_err(new_request_build_error)?;
294
295 let resp = self.send(req).await?;
296
297 match resp.status() {
298 StatusCode::OK => {
299 let body = resp.into_body();
300 let resp: ListTreeResponse =
301 serde_json::from_reader(body.reader()).map_err(new_json_deserialize_error)?;
302
303 Ok(resp.tree)
304 }
305 _ => Err(parse_error(resp)),
306 }
307 }
308}
309
310#[derive(Default, Debug, Clone, Serialize)]
311pub struct CreateOrUpdateContentsRequest {
312 pub message: String,
313 pub content: String,
314 pub sha: Option<String>,
315}
316
317#[derive(Default, Debug, Clone, Serialize)]
318pub struct DeleteContentsRequest {
319 pub message: String,
320 pub sha: String,
321}
322
323#[derive(Default, Debug, Clone, Deserialize)]
324pub struct ListTreeResponse {
325 pub tree: Vec<Tree>,
326}
327
328#[derive(Default, Debug, Clone, Deserialize)]
329pub struct Tree {
330 pub path: String,
331 #[serde(rename = "type")]
332 pub type_field: String,
333 pub size: Option<u64>,
334 pub sha: String,
335}
336
337#[derive(Default, Debug, Clone, Deserialize)]
338pub struct ListResponse {
339 pub git_url: String,
340 pub entries: Vec<Entry>,
341}
342
343#[derive(Default, Debug, Clone, Deserialize)]
344pub struct Entry {
345 pub path: String,
346 pub sha: String,
347 pub size: u64,
348 #[serde(rename = "type")]
349 pub type_field: String,
350}