Skip to content

Commit

Permalink
✨ Listen to repository changes to have an always-running server
Browse files Browse the repository at this point in the history
  • Loading branch information
RemiBardon committed Jan 2, 2024
1 parent 4f660fb commit 9292304
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 28 deletions.
4 changes: 2 additions & 2 deletions src/orangutan/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions src/orangutan/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
111 changes: 105 additions & 6 deletions src/orangutan/src/generate.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,6 +25,104 @@ lazy_static! {
static ref GENERATED_WEBSITES: Arc<Mutex<HashSet<PathBuf>>> = 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…");

Expand Down Expand Up @@ -155,15 +254,15 @@ 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![
"--source", website_root.as_str(),
"--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
Expand All @@ -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 })
}
}

Expand All @@ -194,7 +293,7 @@ pub fn hugo(params: Vec<&str>) -> Result<Vec<u8>, 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())
Expand All @@ -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<i32>, stderr: Vec<u8> },
CommandExecutionFailed { command: String, output: std::process::Output },
CannotGenerateWebsite(Box<Error>),
CannotEmptyIndexJson(io::Error),
CannotCreateHugoConfigFile(io::Error),
Expand All @@ -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 <index.json> file: {err}"),
Error::CannotCreateHugoConfigFile(err) => write!(f, "Could create hugo config file: {err}"),
Expand Down
68 changes: 50 additions & 18 deletions src/orangutan/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
Expand All @@ -68,32 +69,35 @@ async fn main() {
}

async fn throwing_main() -> Result<(), Box<dyn std::error::Error>> {
// 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<Token>) -> String {
match token {
Expand Down Expand Up @@ -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/<source>")]
fn update_content_other(
source: &str,
) -> BadRequest<String> {
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."
Expand Down Expand Up @@ -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}"),
}
}
}
Expand Down

0 comments on commit 9292304

Please sign in to comment.