Files
webhook_bridge/src/gitea_client/mod.rs
2024-09-29 15:03:07 -04:00

179 lines
4.7 KiB
Rust

use base64::engine::general_purpose;
use base64::Engine as _;
use serde::Deserialize;
use tracing::debug;
use self::error::GiteaClientError;
pub(crate) mod error;
#[derive(Debug, Clone)]
pub(crate) struct GiteaClient {
api_root: String,
token: String,
http_client: reqwest::Client,
}
impl GiteaClient {
pub(crate) fn new<R: Into<String>, T: Into<String>>(api_root: R, token: T) -> GiteaClient {
GiteaClient {
api_root: api_root.into(),
token: token.into(),
http_client: reqwest::Client::new(),
}
}
pub(crate) async fn get_tree<O: AsRef<str>, R: AsRef<str>, C: AsRef<str>>(
&self,
owner: O,
repo: R,
commit: C,
) -> Result<Tree, Box<dyn std::error::Error>> {
let mut files = Vec::new();
let mut page: Option<u64> = None;
let mut num_responses: u64 = 0;
loop {
let url = format!(
"{api_root}/v1/repos/{owner}/{repo}/git/trees/{commit}?recursive=true&per_page=100{page}",
api_root = self.api_root,
owner = owner.as_ref(),
repo = repo.as_ref(),
commit = commit.as_ref(),
page = page.map(|num| format!("&page={}", num)).unwrap_or_default()
);
let response = self
.http_client
.get(url)
.header("Authorization", format!("token {}", self.token))
.send()
.await?;
let response = response.error_for_status()?;
let total_count = response
.headers()
.get("x-total-count")
.ok_or(GiteaClientError::NoTotalCountHeaderInResponse)?
.to_str()?
.parse::<u64>()?;
if num_responses == 0 {
files.reserve(total_count as usize);
}
let body = response.text().await?;
let parsed_body: ResponseGetTree = serde_json::from_str(body.as_str())?;
num_responses += parsed_body.tree.len() as u64;
files.extend(parsed_body.tree.into_iter().map(|response_object| {
TreeFileReference::new(response_object.path, response_object.url)
}));
if num_responses >= total_count {
break;
}
page = Some(parsed_body.page + 1);
}
Ok(Tree::new(files))
}
pub(crate) async fn read_file(
&self,
file_reference: &TreeFileReference,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let response = self
.http_client
.get(&file_reference.url)
.header("Authorization", format!("token {}", self.token))
.send()
.await?;
let response = response.error_for_status()?;
let body = response.text().await?;
debug!("read_file response: {}", body);
let parsed_body: ResponseReadFile = serde_json::from_str(body.as_str())?;
assert!(
parsed_body.encoding == "base64",
"We currently only support base64 file encoding from gitea."
);
Ok(general_purpose::STANDARD.decode(parsed_body.content)?)
}
}
/// A single API response for GetTree containing only one page.
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ResponseGetTree {
#[allow(dead_code)]
sha: String,
#[allow(dead_code)]
url: String,
tree: Vec<ResponseObjectReference>,
#[allow(dead_code)]
truncated: bool,
page: u64,
#[allow(dead_code)]
total_count: u64,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ResponseObjectReference {
path: String,
#[allow(dead_code)]
mode: String,
#[allow(dead_code)]
#[serde(rename = "type")]
object_type: String,
#[allow(dead_code)]
size: u64,
#[allow(dead_code)]
sha: String,
url: String,
}
#[derive(Debug)]
pub(crate) struct Tree {
pub(crate) files: Vec<TreeFileReference>,
}
#[derive(Debug)]
pub(crate) struct TreeFileReference {
pub(crate) path: String,
pub(crate) url: String,
}
impl Tree {
pub(crate) fn new(files: Vec<TreeFileReference>) -> Tree {
Tree { files }
}
}
impl TreeFileReference {
pub(crate) fn new<P: Into<String>, U: Into<String>>(path: P, url: U) -> TreeFileReference {
TreeFileReference {
path: path.into(),
url: url.into(),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ResponseReadFile {
content: String,
encoding: String,
#[allow(dead_code)]
url: String,
#[allow(dead_code)]
sha: String,
#[allow(dead_code)]
size: u64,
}