mod atomizer; mod error; mod fs; mod ifc; mod markup; mod settings; mod templated; mod timeparse; mod util; 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; use confindent::Confindent; pub use error::RuntimeError; use fs::Filesystem; use settings::Settings; use tokio_util::io::ReaderStream; use tracing_subscriber::{prelude::*, EnvFilter}; use util::{Referer, RemoteIp, SessionId}; use crate::{ fs::{PathResolution, Webpath}, templated::Templated, }; #[tokio::main] async fn main() { match std::env::args().nth(1).as_deref() { Some("atomizer") => atomizer::main(), /* fallthrough*/ Some("serve") => (), _ => (), } tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) .with( EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new("info")) .unwrap(), ) .init(); let conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap(); let webroot: Utf8PathBuf = conf.child_parse("Webroot").unwrap(); let templates = conf.child_value("Templates").unwrap(); let fs = Filesystem::new(&webroot); let settings = Settings { template_dir: Utf8PathBuf::from(webroot.join(templates)) .canonicalize_utf8() .unwrap(), }; 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, sid: SessionId, rfr: Option, ) -> Response { handler(fse, se, sid, rfr, Path(String::from("/"))).await } async fn handler( Extension(fs): Extension, Extension(settings): Extension, sid: SessionId, rfr: Option, Path(path): Path, ) -> Response { match falible_handler(fs, settings, sid, rfr, path).await { Ok(resp) => resp, Err(re) => Response::builder() .body(Body::from(re.to_string())) .unwrap(), } } async fn falible_handler( fs: Filesystem, settings: Settings, sid: SessionId, rfr: Option, path: String, ) -> Result { tracing::debug!("webpath = {path}"); let webpath: Webpath = path.parse()?; let resolve = fs.resolve(&webpath)?; if !webpath.is_dir() && resolve.is_dirfile { return Ok(redirect(webpath.as_dir())); } match rfr { None => { tracing::info!("[{sid}] serving {webpath}"); } Some(referer) => { tracing::info!("[{sid}] (refer {referer}) serving {webpath}"); } } let ext = resolve.filepath.extension().unwrap_or_default(); if ext != "html" { send_file(resolve.filepath).await } else { let content = Filesystem::read_to_string(&resolve.filepath).await?; match Templated::from_str(&content) { Ok(templated) => send_template(templated, resolve, webpath, 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(); tracing::info!("redirect 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 stem = filepath.file_stem().unwrap_or_default(); let mime = match ext { // Text "css" => "text/css", "html" => "text/html", "js" => "text/javascript", "txt" => "text/plain", "xml" if stem.ends_with("atom") => "application/atom+xml", "xml" => "application/xml", // 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 { tracing::debug!("large file, streaming to client"); 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, resolve: PathResolution, webpath: Webpath, 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 = resolve .filepath .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), ); // styles the templated stuff wants 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); } // path to the file for navigation let mut path: Vec<&str> = webpath.webcanon.iter().collect(); // we don't want the directory/filename itself path.pop(); if let Some(path_pattern) = template.get_pattern("path") { let offset = match templated .frontmatter .get("path-offset") .map(|raw| raw.parse::()) { Some(Ok(offset)) => offset, None => 0, Some(Err(_)) => { tracing::error!( "path-offset in template {} is not an integer", resolve.filepath ); 0 } }; for _ in 0..offset { path.pop(); } let mut link = Utf8PathBuf::from("/"); let mut pat = path_pattern.clone(); pat.set("path_link", "/"); pat.set("path_name", "home"); template.set_pattern("path", pat); for part in path { link.push(part); let mut pat = path_pattern.clone(); pat.set("path_link", &link); pat.set("path_name", part); template.set_pattern("path", pat); } } // insert the page content itself let markedup = markup::process(&templated.content); template.set("main", markedup); Ok(Response::builder() .header(header::CONTENT_TYPE, "text/html") .body(Body::from(template.compile())) .unwrap()) }