use std::path::PathBuf; use actix::prelude::*; use git_next_config::{ForgeConfig, ForgeName, GitDir, RepoAlias, ServerRepoConfig}; use git_next_git::{Generation, RepoDetails}; use kxio::{fs::FileSystem, network::Network}; use tracing::{error, info, warn}; use crate::{ actors::{ file_watcher::FileUpdated, repo::{CloneRepo, RepoActor}, webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter}, }, config::{ServerConfig, ServerStorage, Webhook}, }; #[derive(Debug, derive_more::Display, derive_more::From)] pub enum Error { #[display("Failed to create data directories")] FailedToCreateDataDirectory(kxio::fs::Error), #[display("The forge data path is not a directory: {path:?}")] ForgeDirIsNotDirectory { path: PathBuf, }, Config(crate::config::Error), Io(std::io::Error), } type Result = core::result::Result; pub struct Server { generation: Generation, webhook: Option>, fs: FileSystem, net: Network, } impl Actor for Server { type Context = Context; } impl Handler for Server { type Result = (); fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result { let server_config = match ServerConfig::load(&self.fs) { Ok(server_config) => server_config, Err(err) => { error!("Failed to load config file. Error: {}", err); return; } }; ctx.notify(server_config); } } impl Handler for Server { type Result = (); #[allow(clippy::cognitive_complexity)] // TODO: (#75) reduce complexity fn handle(&mut self, msg: ServerConfig, _ctx: &mut Self::Context) -> Self::Result { let Ok(socket_addr) = msg.http() else { warn!("Unable to parse http.addr"); return; }; if let Some(webhook) = self.webhook.take() { webhook.do_send(ShutdownWebhook); } self.generation.inc(); let server_config = msg; // Server Storage let dir = server_config.storage().path(); if !dir.exists() { info!(?dir, "server storage doesn't exist - creating it"); if let Err(err) = self.fs.dir_create(dir) { error!(?err, ?dir, "Failed to create server storage"); return; } } let Ok(canon) = dir.canonicalize() else { error!(?dir, "Failed to confirm server storage"); return; }; info!(dir = ?canon, "server storage"); // Forge directories in Server Storage let server_storage = server_config.storage(); if let Err(err) = self.create_forge_data_directories(&server_config, dir) { error!(?err, "Failure creating forge storage"); return; } // Webhook Server info!("Starting Webhook Server..."); let webhook_router = WebhookRouter::new().start(); let webhook = server_config.webhook(); // Forge Actors for (forge_name, forge_config) in server_config.forges() { self.create_forge_repos(forge_config, forge_name.clone(), server_storage, webhook) .into_iter() .map(|a| self.start_actor(a)) .map(|(alias, addr)| AddWebhookRecipient(alias, addr.recipient())) .for_each(|msg| webhook_router.do_send(msg)); } let webhook = WebhookActor::new(socket_addr, webhook_router.recipient()).start(); self.webhook.replace(webhook); } } impl Server { pub fn new(fs: FileSystem, net: Network) -> Self { let generation = Generation::new(); Self { generation, webhook: None, fs, net, } } fn create_forge_data_directories( &self, server_config: &ServerConfig, server_dir: &std::path::Path, ) -> Result<()> { for (forge_name, _forge_config) in server_config.forges() { let forge_dir: PathBuf = (&forge_name).into(); let path = server_dir.join(&forge_dir); if self.fs.path_exists(&path)? { if !self.fs.path_is_dir(&path)? { return Err(Error::ForgeDirIsNotDirectory { path }); } } else { info!(%forge_name, ?path, "creating storage"); self.fs.dir_create_all(&path)?; } } Ok(()) } fn create_forge_repos( &self, forge_config: &ForgeConfig, forge_name: ForgeName, server_storage: &ServerStorage, webhook: &Webhook, ) -> Vec<(ForgeName, RepoAlias, RepoActor)> { let span = tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config); let _guard = span.enter(); info!("Creating Forge"); let mut repos = vec![]; let creator = self.create_actor(forge_name, forge_config.clone(), server_storage, webhook); for (repo_alias, server_repo_config) in forge_config.repos() { let forge_repo = creator((repo_alias, server_repo_config)); info!( alias = %forge_repo.1, "Created Repo" ); repos.push(forge_repo); } repos } fn create_actor( &self, forge_name: ForgeName, forge_config: ForgeConfig, server_storage: &ServerStorage, webhook: &Webhook, ) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> (ForgeName, RepoAlias, RepoActor) { let server_storage = server_storage.clone(); let webhook = webhook.clone(); let net = self.net.clone(); let generation = self.generation; move |(repo_alias, server_repo_config)| { let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config); let _guard = span.enter(); info!("Creating Repo"); let gitdir = server_repo_config.gitdir().map_or_else( || { GitDir::from( server_storage .path() .join(forge_name.to_string()) .join(repo_alias.to_string()), ) }, |gitdir| gitdir, ); // INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not // have cloned the repo yet let repo_details = RepoDetails::new( generation, &repo_alias, server_repo_config, &forge_name, &forge_config, gitdir, ); info!("Starting Repo Actor"); let actor = RepoActor::new(repo_details, webhook.clone(), generation, net.clone()); (forge_name.clone(), repo_alias, actor) } } fn start_actor( &self, actor: (ForgeName, RepoAlias, RepoActor), ) -> (RepoAlias, Addr) { let (forge_name, repo_alias, actor) = actor; let span = tracing::info_span!("start_actor", forge = %forge_name, repo = %repo_alias); let _guard = span.enter(); let addr = actor.start(); addr.do_send(CloneRepo); info!("Started"); (repo_alias, addr) } }