website/src/main.rs
Joey Hines 521db9fef9 Final round of refactoring for now
+ Moved away from blocking FS ops
+ Clippy + fmt
2022-12-26 21:30:03 -06:00

262 lines
7.5 KiB
Rust

mod config;
mod error;
use axum::error_handling::HandleErrorLayer;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse};
use axum::routing::get;
use axum::{BoxError, Router};
use axum_extra::routing::SpaRouter;
use error::{JSiteError, PageResult};
use pulldown_cmark::html::push_html;
use pulldown_cmark::{Options, Parser};
use regex::Regex;
use serde::Serialize;
use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use structopt::StructOpt;
use tera::{Context, Tera};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use crate::config::SiteArgs;
#[derive(Serialize)]
struct SiteFile {
rank: u32,
file_name: String,
link_name: String,
path: PathBuf,
}
#[derive(Serialize)]
struct PageData {
site_file: SiteFile,
links: Vec<SiteFile>,
}
/// Returns the rendered template of the index page of the website. This includes links and md
/// pages included in `static/raw_md`
async fn index(State(state): State<Arc<Tera>>) -> PageResult<impl IntoResponse> {
let mut ctx = Context::new();
let mut links: Vec<SiteFile> = Vec::new();
// Get the links to display on the main page
get_pages("static/raw_md", &mut links).await?;
ctx.insert("links", &links);
Ok(Html(state.render("index.html.tera", &ctx)?))
}
/// Gets all the raw md pages contained in a directory
///
/// The order of the vector is determined by OS. Ordering can be set by prepending the file name
/// with a number. Files that start with lower numbers are placed earlier in the list.
///
/// # Arguments
/// * `path` - the path to look for pages in
/// * `pages` - A vector where found pages will be inserted
async fn get_pages(path: &str, pages: &mut Vec<SiteFile>) -> PageResult<()> {
let re = Regex::new(r"(?P<rank>^\d*)(?P<link_name>.+)").unwrap();
// Find all files in the directory
let mut dir = tokio::fs::read_dir(path).await?;
while let Some(entry) = dir.next_entry().await? {
let path = entry.path();
let file_name = match path.file_stem() {
Some(name) => name,
None => continue,
};
let file_name = match file_name.to_str() {
Some(name) => name,
None => continue,
};
if let Some(caps) = re.captures(file_name) {
let link_name = &caps["link_name"];
let rank = &caps["rank"];
let rank = match rank.parse() {
Ok(r) => r,
Err(_) => u32::MAX,
};
let site_file = SiteFile {
rank,
file_name: file_name.to_string(),
link_name: link_name.to_string(),
path: entry.path(),
};
pages.push(site_file);
}
}
pages.sort_by(|a, b| a.rank.cmp(&b.rank));
Ok(())
}
/// Gets a page matching `page_name` in directory `path`
///
/// # Arguments
/// * `path` - path to search in
/// * `page_name` - file to look for
fn get_page(path: &std::path::Path) -> PageResult<SiteFile> {
let file_name = path
.file_name()
.ok_or(JSiteError::PageNotFound(path.to_path_buf()))?;
let file_name = file_name
.to_str()
.ok_or(JSiteError::PageNotFound(path.to_path_buf()))?
.to_string();
if path.exists() {
return Ok(SiteFile {
rank: 0,
file_name: file_name.clone(),
link_name: file_name,
path: path.to_path_buf(),
});
} else {
let mut dir_path = path.to_path_buf();
dir_path.pop();
for entry in dir_path.read_dir()? {
let entry = entry?;
let entry_name = entry.file_name().into_string().unwrap();
if entry_name.contains(&file_name) {
return Ok(SiteFile {
rank: 0,
file_name: entry_name,
link_name: file_name,
path: entry.path(),
});
}
}
}
Err(JSiteError::PageNotFound(path.to_path_buf()))
}
/// Returns a rendered template of a raw md page if it exists
///
/// # Arguments
/// * `page` - path to page
async fn md_page(tera: State<Arc<Tera>>, Path(page): Path<PathBuf>) -> PageResult<Html<String>> {
let mut path = PathBuf::from("static/raw_md");
path.push(page);
// Try and get the page
let site_page = match get_page(path.as_path()) {
Ok(site_page) => site_page,
Err(_) => {
return error_page(&tera, path.to_str().unwrap()).await;
}
};
let page = if site_page.path.is_dir() {
// If the file is a directory, list its contents instead
let mut map = Context::new();
let mut sub_files: Vec<SiteFile> = Vec::new();
match get_pages(site_page.path.to_str().unwrap(), &mut sub_files).await {
Ok(_) => (),
Err(_) => return error_page(&tera, &site_page.link_name).await,
}
let page_data = PageData {
links: sub_files,
site_file: site_page,
};
map.insert("page_data", &page_data);
tera.render("listing.html.tera", &map)?
} else {
// Else, render the MD page
let mut map = Context::new();
let contents = match tokio::fs::read_to_string(site_page.path.clone()).await {
Ok(contents) => contents,
Err(_) => return error_page(&tera, site_page.path.to_str().unwrap()).await,
};
let options = Options::all();
let parser = Parser::new_ext(&contents, options);
let mut html_output = String::new();
push_html(&mut html_output, parser);
map.insert("page", &site_page.link_name);
map.insert("content", &html_output);
tera.render("md_page.html.tera", &map)?
};
Ok(Html(page))
}
/// Build error page
async fn error_page(tera: &Tera, page: &str) -> PageResult<Html<String>> {
let mut map = Context::new();
map.insert("error_page", page);
Ok(Html(tera.render("404.html.tera", &map)?))
}
/// Handle server errors
async fn handle_error(error: BoxError) -> impl IntoResponse {
if error.is::<tower::timeout::error::Elapsed>() {
return (StatusCode::REQUEST_TIMEOUT, Cow::from("request timed out"));
}
if error.is::<tower::load_shed::error::Overloaded>() {
return (
StatusCode::SERVICE_UNAVAILABLE,
Cow::from("service is overloaded, try again later"),
);
}
(
StatusCode::INTERNAL_SERVER_ERROR,
Cow::from(format!("Unhandled internal error: {}", error)),
)
}
/// Launches website
#[tokio::main]
async fn main() {
let args = SiteArgs::from_args();
// Use globbing
let tera = match Tera::new("templates/*.tera") {
Ok(t) => Arc::new(t),
Err(e) => {
println!("Parsing error(s): {}", e);
return;
}
};
let app = Router::new()
.route("/", get(index))
.route("/about/*path", get(md_page))
.merge(SpaRouter::new("/static", "static"))
.layer(
ServiceBuilder::new()
// Handle errors from middleware
.layer(HandleErrorLayer::new(handle_error))
.load_shed()
.concurrency_limit(1024)
.timeout(Duration::from_secs(10))
.layer(TraceLayer::new_for_http()),
)
.with_state(tera);
println!("listening on {}", args.serve_at);
axum::Server::bind(&args.serve_at)
.serve(app.into_make_service())
.await
.unwrap();
}