diff --git a/src/orangutan/Cargo.lock b/src/orangutan/Cargo.lock index 8dc58a9..a001033 100644 --- a/src/orangutan/Cargo.lock +++ b/src/orangutan/Cargo.lock @@ -1170,9 +1170,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" dependencies = [ "proc-macro2", ] diff --git a/src/orangutan/src/config.rs b/src/orangutan/src/config.rs index 3731faa..f88010d 100644 --- a/src/orangutan/src/config.rs +++ b/src/orangutan/src/config.rs @@ -19,9 +19,11 @@ pub const NOT_FOUND_FILE: &'static str = "/404.html"; const WEBSITE_DIR_NAME: &'static str = "website"; lazy_static! { - pub static ref WEBSITE_ROOT_PATH: String = env::var("APP_HOME").unwrap_or(".".to_string()); + static ref WORK_DIR: PathBuf = env::current_dir().unwrap(); + pub static ref WEBSITE_REPOSITORY: String = env::var("WEBSITE_REPOSITORY").expect("Environment variable `WEBSITE_REPOSITORY` is required."); + pub static ref WEBSITE_ROOT_PATH: String = env::var("WEBSITE_ROOT").unwrap_or("website".to_string()); pub static ref WEBSITE_ROOT: &'static Path = Path::new(WEBSITE_ROOT_PATH.as_str()); - pub static ref BASE_DIR: PathBuf = WEBSITE_ROOT.join(".orangutan"); + pub static ref BASE_DIR: PathBuf = WORK_DIR.join(".orangutan"); pub static ref KEYS_DIR: PathBuf = BASE_DIR.join("keys"); pub static ref HUGO_CONFIG_DIR: PathBuf = BASE_DIR.join("hugo-config"); pub static ref DEST_DIR: PathBuf = BASE_DIR.join("out"); diff --git a/src/orangutan/src/generate.rs b/src/orangutan/src/generate.rs index e4fbe59..6d89fe1 100644 --- a/src/orangutan/src/generate.rs +++ b/src/orangutan/src/generate.rs @@ -1,6 +1,7 @@ use crate::config::*; use crate::helpers::copy_directory; use core::fmt; +use std::os::fd::FromRawFd; use std::sync::{Mutex, MutexGuard, Arc}; use std::sync::atomic::{AtomicBool, Ordering}; use std::io::Cursor; @@ -24,6 +25,104 @@ lazy_static! { static ref GENERATED_WEBSITES: Arc>> = Arc::new(Mutex::new(HashSet::new())); } +pub fn generate_default_website() -> Result<(), Error> { + // Generate the website + generate_website_if_needed(&WebsiteId::default())?; + + // Generate Orangutan data files + generate_data_files_if_needed()?; + + Ok(()) +} + +pub fn clone_repository() -> Result<(), Error> { + if WEBSITE_ROOT.is_dir() { + return pull_repository() + } + + _clone_repository()?; + _init_submodules()?; + Ok(()) +} + +fn _clone_repository() -> Result<(), Error> { + let mut command = Command::new("git"); + command + .args(vec!["clone", &WEBSITE_REPOSITORY, &WEBSITE_ROOT_PATH]) + .args(vec!["--depth", "1"]); + + trace!("Running `{:?}`…", command); + let output = command + .output() + .map_err(|e| Error::CannotExecuteCommand(format!("{:?}", command), e))?; + + if output.status.success() { + Ok(()) + } else { + Err(Error::CommandExecutionFailed { command: format!("{:?}", command), output }) + } +} + +fn _init_submodules() -> Result<(), Error> { + let mut command = Command::new("git"); + command + .args(vec!["-C", &WEBSITE_ROOT_PATH]) + .args(vec!["submodule", "update", "--init"]); + + trace!("Running `{:?}`…", command); + let output = command + .output() + .map_err(|e| Error::CannotExecuteCommand(format!("{:?}", command), e))?; + + if output.status.success() { + Ok(()) + } else { + Err(Error::CommandExecutionFailed { command: format!("{:?}", command), output }) + } +} + +pub fn pull_repository() -> Result<(), Error> { + _pull_repository()?; + _update_submodules()?; + Ok(()) +} + +fn _pull_repository() -> Result<(), Error> { + let mut command = Command::new("git"); + command + .args(vec!["-C", &WEBSITE_ROOT_PATH]) + .arg("pull"); + + trace!("Running `{:?}`…", command); + let output = command + .output() + .map_err(|e| Error::CannotExecuteCommand(format!("{:?}", command), e))?; + + if output.status.success() { + Ok(()) + } else { + Err(Error::CommandExecutionFailed { command: format!("{:?}", command), output }) + } +} + +fn _update_submodules() -> Result<(), Error> { + let mut command = Command::new("git"); + command + .args(vec!["-C", &WEBSITE_ROOT_PATH]) + .args(vec!["submodule", "update", "--recursive"]); + + trace!("Running `{:?}`…", command); + let output = command + .output() + .map_err(|e| Error::CannotExecuteCommand(format!("{:?}", command), e))?; + + if output.status.success() { + Ok(()) + } else { + Err(Error::CommandExecutionFailed { command: format!("{:?}", command), output }) + } +} + fn _copy_hugo_config() -> Result<(), Error> { debug!("Copying hugo config…"); @@ -155,7 +254,7 @@ pub fn generate_data_files_if_needed() -> Result<(), Error> { } pub fn hugo_gen(params: Vec<&str>, destination: String) -> Result<(), Error> { - let mut _command = Command::new("hugo"); + let mut command = Command::new("hugo"); let website_root = WEBSITE_ROOT.display().to_string(); let base_params: Vec<&str> = vec![ @@ -163,7 +262,7 @@ pub fn hugo_gen(params: Vec<&str>, destination: String) -> Result<(), Error> { "--destination", destination.as_str(), ]; let params = base_params.iter().chain(params.iter()); - let command = _command.args(params); + command.args(params); trace!("Running `{:?}`…", command); let output = command @@ -173,7 +272,7 @@ pub fn hugo_gen(params: Vec<&str>, destination: String) -> Result<(), Error> { if output.status.success() { Ok(()) } else { - Err(Error::CommandExecutionFailed { command: format!("{:?}", command), code: output.status.code(), stderr: output.stderr }) + Err(Error::CommandExecutionFailed { command: format!("{:?}", command), output }) } } @@ -194,7 +293,7 @@ pub fn hugo(params: Vec<&str>) -> Result, Error> { .map_err(|e| Error::CannotExecuteCommand(format!("{:?}", command), e))?; if !output.status.success() { - return Err(Error::CommandExecutionFailed { command: format!("{:?}", command), code: output.status.code(), stderr: output.stderr }) + return Err(Error::CommandExecutionFailed { command: format!("{:?}", command), output }) } Ok(output.stdout.clone()) @@ -211,7 +310,7 @@ fn empty_index_json(website_dir: &PathBuf) -> Result<(), io::Error> { #[derive(Debug)] pub enum Error { CannotExecuteCommand(String, io::Error), - CommandExecutionFailed { command: String, code: Option, stderr: Vec }, + CommandExecutionFailed { command: String, output: std::process::Output }, CannotGenerateWebsite(Box), CannotEmptyIndexJson(io::Error), CannotCreateHugoConfigFile(io::Error), @@ -221,7 +320,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::CannotExecuteCommand(command, err) => write!(f, "Could not execute command `{command}`: {err}"), - Error::CommandExecutionFailed { command, code, stderr } => write!(f, "Command `{command}` failed with exit code {:?}: {}", code, String::from_utf8_lossy(stderr)), + Error::CommandExecutionFailed { command, output } => write!(f, "Command `{command}` failed with exit code {:?}\nstdout: {}\nstderr: {}", output.status.code(), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)), Error::CannotGenerateWebsite(err) => write!(f, "Could not generate website: {err}"), Error::CannotEmptyIndexJson(err) => write!(f, "Could not empty file: {err}"), Error::CannotCreateHugoConfigFile(err) => write!(f, "Could create hugo config file: {err}"), diff --git a/src/orangutan/src/server.rs b/src/orangutan/src/server.rs index 7a2e2d7..95d2e85 100644 --- a/src/orangutan/src/server.rs +++ b/src/orangutan/src/server.rs @@ -6,19 +6,20 @@ mod object_reader; use biscuit::builder::{Fact, Term}; use object_reader::{ObjectReader, ReadObjectResponse}; -use rocket::Either; +use rocket::fairing::AdHoc; +use rocket::{Either, post, Responder}; use rocket::form::Errors; use rocket::http::CookieJar; use rocket::http::{Status, Cookie, SameSite}; use rocket::http::uri::Origin; -use rocket::response::status::NotFound; +use rocket::response::status::{BadRequest, NotFound}; use rocket::{Request, request, get, routes, catch, catchers, State}; use rocket::response::Redirect; use rocket::request::FromRequest; use rocket::outcome::Outcome; use tracing::Level; use tracing_subscriber::FmtSubscriber; -use std::fmt; +use std::{fmt, fs, io}; use std::time::SystemTime; use std::path::{PathBuf, Path}; use std::process::exit; @@ -51,7 +52,7 @@ lazy_static! { #[rocket::main] async fn main() { let subscriber = FmtSubscriber::builder() - .with_max_level(Level::DEBUG) + .with_max_level(Level::TRACE) .finish(); tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber."); @@ -68,32 +69,35 @@ async fn main() { } async fn throwing_main() -> Result<(), Box> { - // Generate the website - generate_website_if_needed(&WebsiteId::default()) - .map_err(Error::WebsiteGenerationError) - .map_err(Box::new)?; - - // Generate Orangutan data files - generate_data_files_if_needed() - .map_err(Error::CannotGenerateDataFiles) - .map_err(Box::new)?; - - rocket::build() + let rocket = rocket::build() .mount("/", routes![ clear_cookies, handle_refresh_token, handle_request_authenticated, handle_request, get_user_info, + update_content_github, ]) .register("/", catchers![not_found]) .manage(ObjectReader::new()) + .attach(AdHoc::on_liftoff("Liftoff website generation", |rocket| Box::pin(async move { + if let Err(err) = liftoff() { + error!("Error: {}", err); + rocket.shutdown().await; + } + }))) .launch() .await?; Ok(()) } +fn liftoff() -> Result<(), Error> { + clone_repository().map_err(Error::WebsiteGenerationError)?; + generate_default_website().map_err(Error::WebsiteGenerationError)?; + Ok(()) +} + #[get("/_info")] fn get_user_info(token: Option) -> String { match token { @@ -154,6 +158,31 @@ async fn handle_request( _handle_request(origin, None, object_reader).await } +/// TODO: [Validate webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries#validating-webhook-deliveries) +#[post("/update-content/github")] +fn update_content_github() -> Result<(), Error> { + // Update repository + pull_repository() + .map_err(Error::CannotPullOutdatedRepository)?; + + // Remove outdated websites + fs::remove_dir_all(DEST_DIR.as_path()) + .map_err(Error::CannotDeleteOutdatedWebsites)?; + + // Pre-generate default website as we will access it at some point anyway + generate_default_website() + .map_err(Error::WebsiteGenerationError)?; + + Ok(()) +} + +#[post("/update-content/")] +fn update_content_other( + source: &str, +) -> BadRequest { + BadRequest(format!("Source '{source}' is not supported.")) +} + #[catch(404)] fn not_found() -> &'static str { "This page doesn't exist or you are not allowed to see it." @@ -496,17 +525,20 @@ fn add_padding(base64_string: &str) -> String { } } -#[derive(Debug)] +#[derive(Debug, Responder)] +#[response(status = 500)] enum Error { WebsiteGenerationError(generate::Error), - CannotGenerateDataFiles(generate::Error), + CannotPullOutdatedRepository(generate::Error), + CannotDeleteOutdatedWebsites(io::Error), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::WebsiteGenerationError(err) => write!(f, "Website generation error: {err}"), - Error::CannotGenerateDataFiles(err) => write!(f, "Could not generate data files: {err}"), + Error::CannotPullOutdatedRepository(err) => write!(f, "Cannot pull outdated repository: {err}"), + Error::CannotDeleteOutdatedWebsites(err) => write!(f, "Cannot delete outdated websites: {err}"), } } }