mod error; mod fs; mod markup; mod settings; mod templated; use std::{os::unix::fs::MetadataExt, str::FromStr}; use axum::{ body::Body, extract::Path, http::{header, StatusCode}, response::Response, routing::get, Extension, Router, }; use bempline::{Document, Options}; use camino::Utf8PathBuf; pub use error::RuntimeError; use fs::Filesystem; use settings::Settings; use tokio_util::io::ReaderStream; use crate::{ fs::{PathResolution, Webpath}, templated::Templated, }; #[tokio::main] async fn main() { let fs = Filesystem::new("../inf/served"); let settings = Settings { template_dir: Utf8PathBuf::from("../inf/templates"), }; let app = Router::new() .route("/", get(index_handler)) .route("/*path", get(handler)) .layer(Extension(fs)) .layer(Extension(settings)); let listener = tokio::net::TcpListener::bind("0.0.0.0:2560").await.unwrap(); axum::serve(listener, app).await.unwrap() } async fn index_handler(fse: Extension, se: Extension) -> Response { handler(fse, se, Path(String::from("/"))).await } async fn handler( Extension(fs): Extension, Extension(settings): Extension, Path(path): Path, ) -> Response { match falible_handler(fs, settings, path).await { Ok(resp) => resp, Err(re) => Response::builder() .body(Body::from(re.to_string())) .unwrap(), } } async fn falible_handler( fs: Filesystem, settings: Settings, path: String, ) -> Result { println!("raw = {path}"); let webpath: Webpath = path.parse()?; println!("path = {path}"); let PathResolution { filepath, is_dirfile, } = fs.resolve(&webpath)?; if !webpath.is_dir() && is_dirfile { println!("as_dir = {}", webpath.as_dir()); return Ok(redirect(webpath.as_dir())); } let ext = filepath.extension().unwrap_or_default(); if ext != "html" { send_file(filepath).await } else { let content = Filesystem::read_to_string(&filepath).await?; match Templated::from_str(&content) { Ok(templated) => send_template(templated, filepath, settings).await, Err(_) => Ok(Response::builder() .header(header::CONTENT_TYPE, "text/html") .body(Body::from(content)) .unwrap()), } } } fn redirect>(redirection: S) -> Response { let location = redirection.into(); println!("redirecting to {location}"); Response::builder() .status(StatusCode::TEMPORARY_REDIRECT) .header(header::LOCATION, &location) .body(Body::new(format!("redirecting to {location}"))) .unwrap() } // 20 megabytes const STREAM_AFTER: u64 = 20 * 1024 * 1024; async fn send_file(filepath: Utf8PathBuf) -> Result { let ext = filepath.extension().unwrap_or_default(); let mime = match ext { // Text "css" => "text/css", "html" => "text/html", "js" => "text/javascript", "txt" => "txt/plain", // Multimedia "gif" => "image/gif", "jpg" | "jpeg" => "image/jpeg", "mp4" => "video/mp4", "png" => "image/png", _ => "", }; let mut response = Response::builder(); if !mime.is_empty() { response = response.header(header::CONTENT_TYPE, mime); } let metadata = Filesystem::metadata(&filepath)?; if metadata.size() > STREAM_AFTER { let file = Filesystem::open(filepath).await?; let stream = ReaderStream::new(file); Ok(response.body(Body::from_stream(stream)).unwrap()) } else { let content = Filesystem::read(filepath).await?; Ok(response.body(Body::from(content)).unwrap()) } } async fn send_template( templated: Templated, path: Utf8PathBuf, settings: Settings, ) -> Result { let template_stem = templated.frontmatter.get("template").expect("no template"); let template_name = Utf8PathBuf::from(format!("{template_stem}.html")); let template_path = settings.template_dir.join(template_name); let filename = path.file_name().expect("template has no filename"); let mut template = Document::from_file( template_path, Options::default().include_path(bempline::options::IncludeMethod::Path( settings.template_dir.as_std_path().to_owned(), )), ) .unwrap(); template.set( "title", templated.frontmatter.get("title").unwrap_or(filename), ); let style_pattern = template.get_pattern("styles").unwrap(); for style in templated.frontmatter.get_many("style") { let mut pat = style_pattern.clone(); pat.set("style", style); template.set_pattern("styles", pat); } template.set("main", templated.content); Ok(Response::builder() .header(header::CONTENT_TYPE, "text/html") .body(Body::from(template.compile())) .unwrap()) }