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