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, T: Into>(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, R: AsRef, C: AsRef>( &self, owner: O, repo: R, commit: C, ) -> Result> { let mut files = Vec::new(); let mut page: Option = 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::()?; 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, Box> { 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, #[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, } #[derive(Debug)] pub(crate) struct TreeFileReference { pub(crate) path: String, pub(crate) url: String, } impl Tree { pub(crate) fn new(files: Vec) -> Tree { Tree { files } } } impl TreeFileReference { pub(crate) fn new, U: Into>(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, }