diff --git a/src/main.rs b/src/main.rs index eb809ad..bd6718a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ mod init; mod server; use clap::Parser; -use kxio::filesystem; +use kxio::{filesystem, network::Network}; #[derive(Parser, Debug)] #[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())] @@ -23,6 +23,7 @@ enum Server { } fn main() { let fs = filesystem::FileSystem::new_real(None); + let net = Network::new_real(); let commands = Commands::parse(); match commands.command { @@ -34,7 +35,7 @@ fn main() { server::init(fs); } Server::Start => { - server::start(fs); + server::start(fs, net); } }, } diff --git a/src/server/actors/repo.rs b/src/server/actors/repo.rs deleted file mode 100644 index 2f2f946..0000000 --- a/src/server/actors/repo.rs +++ /dev/null @@ -1,25 +0,0 @@ -use actix::prelude::*; -use tracing::info; - -use crate::server::config::RepoDetails; - -pub struct RepoActor { - details: RepoDetails, -} -impl RepoActor { - pub(crate) const fn new(details: RepoDetails) -> Self { - Self { details } - } -} -impl Actor for RepoActor { - type Context = Context; -} -#[derive(Message)] -#[rtype(result = "()")] -pub struct StartRepo; -impl Handler for RepoActor { - type Result = (); - fn handle(&mut self, _msg: StartRepo, _ctx: &mut Self::Context) -> Self::Result { - info!(%self.details, "Starting Repo"); - } -} diff --git a/src/server/actors/repo/config.rs b/src/server/actors/repo/config.rs new file mode 100644 index 0000000..dbf08be --- /dev/null +++ b/src/server/actors/repo/config.rs @@ -0,0 +1,22 @@ +use actix::prelude::*; +use kxio::network::Network; +use tracing::error; + +use crate::server::{ + config::{ForgeType, RepoDetails}, + forge, +}; + +use super::{LoadedConfig, RepoActor}; + +pub async fn load(details: RepoDetails, addr: Addr, net: Network) { + let config = match details.forge.forge_type { + ForgeType::ForgeJo => forge::forgejo::config::load(details, &net).await, + }; + match config { + Ok(config) => addr.do_send(LoadedConfig(config)), + Err(err) => { + error!(?err, "Failed to load config"); + } + } +} diff --git a/src/server/actors/repo/mod.rs b/src/server/actors/repo/mod.rs new file mode 100644 index 0000000..84b4341 --- /dev/null +++ b/src/server/actors/repo/mod.rs @@ -0,0 +1,51 @@ +mod config; + +use actix::prelude::*; +use kxio::network::Network; +use tracing::info; + +use crate::server::config::{RepoConfig, RepoDetails}; + +pub struct RepoActor { + details: RepoDetails, + config: Option, // INFO: if [None] then send [StartRepo] to populate it + net: Network, +} +impl RepoActor { + pub(crate) const fn new(details: RepoDetails, net: Network) -> Self { + Self { + details, + config: None, + net, + } + } +} +impl Actor for RepoActor { + type Context = Context; +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct StartRepo; +impl Handler for RepoActor { + type Result = (); + fn handle(&mut self, _msg: StartRepo, ctx: &mut Self::Context) -> Self::Result { + info!(%self.details, "Starting Repo"); + let details = self.details.clone(); + let addr = ctx.address(); + let net = self.net.clone(); + config::load(details, addr, net).into_actor(self).wait(ctx); + } +} + +#[derive(Message)] +#[rtype(result = "()")] +struct LoadedConfig(pub RepoConfig); +impl Handler for RepoActor { + type Result = (); + fn handle(&mut self, msg: LoadedConfig, _ctx: &mut Self::Context) -> Self::Result { + let config = msg.0; + info!(%self.details, %config, "Config loaded"); + self.config.replace(config); + } +} diff --git a/src/server/config.rs b/src/server/config.rs index 6afc258..420aa73 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -35,12 +35,22 @@ impl RepoConfig { toml::from_str(toml).map_err(OneOf::new) } } +impl Display for RepoConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.branches) + } +} #[derive(Debug, PartialEq, Eq, Deserialize)] pub struct RepoBranches { main: String, next: String, dev: String, } +impl Display for RepoBranches { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct Forge { diff --git a/src/server/forge/forgejo/config.rs b/src/server/forge/forgejo/config.rs new file mode 100644 index 0000000..3149fb5 --- /dev/null +++ b/src/server/forge/forgejo/config.rs @@ -0,0 +1,112 @@ +use base64::Engine; +use kxio::network::{self, Network}; +use terrors::OneOf; +use tracing::{error, info}; + +use crate::server::config::{RepoConfig, RepoDetails}; + +pub async fn load( + details: RepoDetails, + net: &Network, +) -> Result< + RepoConfig, + OneOf<( + RepoConfigFileNotFound, + RepoConfigIsNotFile, + RepoConfigDecodeError, + RepoConfigParseError, + RepoConfigUnknownError, + )>, +> { + let hostname = details.forge.hostname; + let path = details.repo; + let filepath = ".git-next.toml"; + let branch = details.branch; + let token = details.forge.token; + 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::Both, + ); + 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(); + 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))) + } + } +} + +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(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); + +#[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, +} diff --git a/src/server/forge/forgejo/mod.rs b/src/server/forge/forgejo/mod.rs new file mode 100644 index 0000000..ef68c36 --- /dev/null +++ b/src/server/forge/forgejo/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/src/server/forge/mod.rs b/src/server/forge/mod.rs new file mode 100644 index 0000000..607ec12 --- /dev/null +++ b/src/server/forge/mod.rs @@ -0,0 +1 @@ +pub mod forgejo; diff --git a/src/server/mod.rs b/src/server/mod.rs index 47ff3ca..605b0dc 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,7 +1,9 @@ mod actors; mod config; +pub mod forge; use actix::prelude::*; +use kxio::network::Network; use std::path::PathBuf; @@ -32,7 +34,7 @@ pub fn init(fs: FileSystem) { } } -pub fn start(fs: FileSystem) { +pub fn start(fs: FileSystem, net: Network) { let Ok(_) = init_logging() else { eprintln!("Failed to initialize logging."); return; @@ -49,7 +51,7 @@ pub fn start(fs: FileSystem) { info!("Config loaded"); let actors = config .forges() - .flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name)) + .flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, &net)) .collect::>(); actix::System::new().block_on(async { @@ -63,6 +65,7 @@ pub fn start(fs: FileSystem) { fn create_forge_repos( forge: &Forge, forge_name: ForgeName, + net: &Network, ) -> Vec<(ForgeName, RepoName, RepoActor)> { let forge = forge.clone(); let span = tracing::info_span!("Forge", %forge_name, %forge); @@ -70,24 +73,24 @@ fn create_forge_repos( info!("Creating Forge"); forge .repos() - .map(create_actor(forge_name, forge.clone())) + .map(create_actor(forge_name, forge.clone(), net)) .collect::>() } fn create_actor( forge_name: ForgeName, forge: config::Forge, + net: &Network, ) -> impl Fn((RepoName, &Repo)) -> (ForgeName, RepoName, RepoActor) { + let net = net.clone(); move |(repo_name, repo)| { let span = tracing::info_span!("Repo", %repo_name, %repo); let _guard = span.enter(); info!("Creating Repo"); - let actor = actors::repo::RepoActor::new(config::RepoDetails::new( - &repo_name, - repo, - &forge_name, - &forge, - )); + let actor = actors::repo::RepoActor::new( + config::RepoDetails::new(&repo_name, repo, &forge_name, &forge), + net.clone(), + ); info!("Created Repo"); (forge_name.clone(), repo_name, actor) }