use base64::Engine; use kxio::network::{self, Network}; use secrecy::ExposeSecret; use terrors::OneOf; use tracing::{error, info}; use crate::server::config::{BranchName, RepoConfig, RepoDetails}; #[derive(Debug)] pub struct RepoConfigFileNotFound; #[derive(Debug)] pub struct RepoConfigIsNotFile; #[derive(Debug)] pub struct RepoConfigDecodeError; #[derive(Debug)] pub struct RepoConfigParseError; #[derive(Debug)] pub struct RepoConfigUnknownError(pub network::StatusCode); type RepoConfigLoadErrors = ( RepoConfigFileNotFound, RepoConfigIsNotFile, RepoConfigDecodeError, RepoConfigParseError, RepoConfigUnknownError, RepoConfigBranchNotFound, ); pub async fn load( details: &RepoDetails, net: &Network, ) -> Result> { let hostname = &details.forge.hostname; let path = &details.repo; let filepath = ".git-next.toml"; let branch = &details.branch; let token = details.forge.token.expose_secret(); let url = network::NetUrl::new(format!( "https://{hostname}/api/v1/repos/{path}/contents/{filepath}?ref={branch}&token={token}" )); info!(%url, "Loading config"); let request = network::NetRequest::new( network::RequestMethod::Get, url, network::NetRequestHeaders::new(), network::RequestBody::None, network::ResponseType::Json, None, network::NetRequestLogging::None, ); let result = net.get::(request).await; let response = result.map_err(|e| match e { network::NetworkError::RequestFailed(_, status_code, _) | network::NetworkError::RequestError(_, status_code, _) => match status_code { network::StatusCode::NOT_FOUND => OneOf::new(RepoConfigFileNotFound), _ => OneOf::new(RepoConfigUnknownError(status_code)), }, _ => OneOf::new(RepoConfigUnknownError( network::StatusCode::INTERNAL_SERVER_ERROR, )), })?; let status = response.status_code(); let config = match response.response_body() { Some(body) => { // we need to decode (see encoding field) the value of 'content' from the response match body.content_type { ForgeContentsType::File => decode_config(body).map_err(OneOf::broaden), _ => Err(OneOf::new(RepoConfigIsNotFile)), } } None => { error!(%status, "Failed to fetch repo config file"); Err(OneOf::new(RepoConfigUnknownError(status))) } }?; let config = validate(details, config, net) .await .map_err(OneOf::broaden)?; Ok(config) } fn decode_config( body: ForgeContentsResponse, ) -> Result> { match body.encoding.as_str() { "base64" => { let decoded = base64::engine::general_purpose::STANDARD .decode(body.content) .map_err(|_| OneOf::new(RepoConfigDecodeError))?; let decoded = String::from_utf8(decoded).map_err(|_| OneOf::new(RepoConfigDecodeError))?; let config = toml::from_str(&decoded).map_err(|_| OneOf::new(RepoConfigParseError))?; Ok(config) } _ => Err(OneOf::new(RepoConfigDecodeError)), } } #[derive(Clone, Debug, serde::Deserialize)] struct ForgeContentsResponse { #[serde(rename = "type")] pub content_type: ForgeContentsType, pub content: String, pub encoding: String, } #[derive(Clone, Debug, serde::Deserialize)] enum ForgeContentsType { #[serde(rename = "file")] File, #[serde(rename = "dir")] Dir, #[serde(rename = "symlink")] Symlink, #[serde(rename = "submodule")] Submodule, } #[derive(Debug)] pub struct RepoConfigBranchNotFound(pub BranchName); type RepoConfigValidateErrors = (RepoConfigBranchNotFound, RepoConfigUnknownError); pub async fn validate( details: &RepoDetails, config: RepoConfig, net: &Network, ) -> Result> { let hostname = &details.forge.hostname; let path = &details.repo; let token = details.forge.token.expose_secret(); let url = network::NetUrl::new(format!( "https://{hostname}/api/v1/repos/{path}/branches?token={token}" )); info!(%url, "Listing branches"); let request = network::NetRequest::new( network::RequestMethod::Get, url, network::NetRequestHeaders::new(), network::RequestBody::None, network::ResponseType::Json, None, network::NetRequestLogging::None, ); let result = net.get::(request).await; let response = result.map_err(|e| { error!(?e, "Failed to list branches"); OneOf::new(RepoConfigUnknownError( network::StatusCode::INTERNAL_SERVER_ERROR, )) })?; let branches = response.response_body().unwrap_or_default(); if !branches .iter() .any(|branch| branch.name() == config.branches().main()) { return Err(OneOf::new(RepoConfigBranchNotFound( config.branches().main(), ))); } if !branches .iter() .any(|branch| branch.name() == config.branches().next()) { return Err(OneOf::new(RepoConfigBranchNotFound( config.branches().next(), ))); } if !branches .iter() .any(|branch| branch.name() == config.branches().dev()) { return Err(OneOf::new(RepoConfigBranchNotFound( config.branches().dev(), ))); } Ok(config) } type BranchList = Vec; #[derive(Debug, serde::Deserialize)] struct Branch { name: String, } impl Branch { fn name(&self) -> BranchName { BranchName(self.name.clone()) } }