diff options
| author | gennyble <gen@nyble.dev> | 2026-09-21 14:23:28 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-09-21 14:23:28 -0500 |
| commit | de0e60603d3e470befad49f2ec59778b5d1b5d59 (patch) | |
| tree | 861edd0aaa1745e97cca926262e0eb73448da83c /splorm/src/handler | |
| parent | be872aa0cd61ea16b95eddcd68c6a72f0fe3af96 (diff) | |
splorm, i wake and breathe
Diffstat (limited to 'splorm/src/handler')
| -rw-r--r-- | splorm/src/handler/mod.rs | 25 | ||||
| -rw-r--r-- | splorm/src/handler/staticfile.rs | 52 | ||||
| -rw-r--r-- | splorm/src/handler/templated.rs | 131 |
3 files changed, 208 insertions, 0 deletions
diff --git a/splorm/src/handler/mod.rs b/splorm/src/handler/mod.rs new file mode 100644 index 0000000..0089c48 --- /dev/null +++ b/splorm/src/handler/mod.rs @@ -0,0 +1,25 @@ +use axum::{Extension, extract::Path, response::Response}; + +mod staticfile; +mod templated; + +use crate::{RuntimeError, config::Settings, fs::RequestPath}; + +pub async fn index(Extension(settings): Extension<Settings>) -> Result<Response, RuntimeError> { + println!("index!"); + let rqp = RequestPath::verify_safe(settings.root(), "home.html")?; + templated::send_file(rqp, &settings).await +} + +pub async fn static_path( + Extension(settings): Extension<Settings>, + Path(path): Path<String>, +) -> Result<Response, RuntimeError> { + println!("{path}"); + let rqp = RequestPath::verify_safe(settings.root(), &path)?; + if rqp.guess_mime().as_ref() == "text/html" { + templated::send_file(rqp, &settings).await + } else { + staticfile::send_or_stream_file(rqp).await + } +} diff --git a/splorm/src/handler/staticfile.rs b/splorm/src/handler/staticfile.rs new file mode 100644 index 0000000..8f26251 --- /dev/null +++ b/splorm/src/handler/staticfile.rs @@ -0,0 +1,52 @@ +use axum::{body::Body, response::Response}; +use tokio_util::io::ReaderStream; + +use crate::{ + RuntimeError, + fs::{self, Mimetype, RequestPath}, +}; + +const STREAM_AFTER: u64 = 1024 * 1024 * 5; +pub async fn send_or_stream_file(path: RequestPath) -> Result<Response, RuntimeError> { + let meta = path.system_path().metadata()?; + + if meta.len() > STREAM_AFTER { + stream_file(path).await + } else { + send_file(path).await + } +} + +pub async fn send_file(path: RequestPath) -> Result<Response, RuntimeError> { + let mime = path.guess_mime(); + + let body = match mime.mimetype() { + Mimetype::Text => { + let text = fs::read_file_to_string(path.system_path()).await?; + Body::from(text) + } + Mimetype::Media => { + let data = fs::read_file(path.system_path()).await?; + Body::from(data) + } + }; + + Response::builder() + .status(200) + .header("Content-Type", mime.as_ref()) + .body(body) + .map_err(|_| RuntimeError::unknown()) +} + +pub async fn stream_file(path: RequestPath) -> Result<Response, RuntimeError> { + let mime = path.guess_mime(); + + let file = fs::open_file(path.system_path()).await?; + let stream = ReaderStream::new(file); + + Response::builder() + .status(200) + .header("Content-Type", mime.as_ref()) + .body(Body::from_stream(stream)) + .map_err(|_| RuntimeError::unknown()) +} diff --git a/splorm/src/handler/templated.rs b/splorm/src/handler/templated.rs new file mode 100644 index 0000000..596210d --- /dev/null +++ b/splorm/src/handler/templated.rs @@ -0,0 +1,131 @@ +use std::str::FromStr; + +use axum::{body::Body, response::Response}; +use bempline::{Document, Options, options::IncludeMethod, variables}; +use paige::{Frontmatter, Templated}; +use timeparse::OffsetDateTime; + +use crate::{ + RuntimeError, + config::Settings, + fs::{self, RequestPath, SafePath, SafePathBuf}, +}; + +pub async fn send_file(path: RequestPath, settings: &Settings) -> Result<Response, RuntimeError> { + let safe = path.system_path().to_owned(); + let string = fs::read_file_to_string(path.system_path()).await?; + let template = Templated::from_str(&string) + .map_err(|_| RuntimeError::not_a_template(path.system_path().to_path_buf()))?; + let tvars = TemplateVariables::build(safe.as_path(), &template.frontmatter); + + let pt_path = match tvars.template { + Some(name) => { + let path = RequestPath::verify_safe(settings.template_dir(), &format!("{name}.html"))?; + path.system_path().to_owned() + } + None => settings.default_template_path(), + }; + + let mut page_template = get_template(pt_path).await?; + tvars.apply_title(&mut page_template); + tvars.apply_styles(&mut page_template); + tvars.apply_published(&mut page_template); + tvars.apply_pathnav(&mut page_template, path); + page_template.set("main", template.content); + + Response::builder() + .status(200) + .header("content-type", "text/html") + .body(Body::from(page_template.compile())) + .map_err(|_| RuntimeError::Unknown) +} + +async fn get_template(path: SafePathBuf) -> Result<Document, RuntimeError> { + let options = Options::new().include_path(IncludeMethod::Path(path.as_std_path().to_owned())); + let str = fs::read_file_to_string(path.as_path()).await?; + + Document::from_str(str, options) + .map_err(|perr| RuntimeError::template_parse_error(perr, path.to_path_buf())) +} + +pub struct TemplateVariables<'fm> { + template: Option<&'fm str>, + + style: Vec<&'fm str>, + + title: &'fm str, + description: Option<&'fm str>, + published: Option<OffsetDateTime>, + + skip_path: Option<usize>, +} + +impl<'fm> TemplateVariables<'fm> { + pub fn build(safe: SafePath<'fm>, fm: &'fm Frontmatter) -> Self { + let filename = safe.file_name().unwrap_or("unknown"); + + Self { + template: fm.get("template"), + title: fm.get("title").unwrap_or_else(|| filename), + style: fm.get_many("style"), + description: fm.get("description"), + published: fm + .get("published") + .map(|raw| timeparse::parse(raw).ok()) + .flatten(), + skip_path: fm.get("skip-path").map(|s| s.parse().ok()).flatten(), + } + } + + pub fn apply_styles(&self, doc: &mut Document) { + if let Some(styles) = doc.get_pattern("styles") { + self.style.iter().for_each(|style| { + let mut cloned = styles.clone(); + cloned.set("style", style); + doc.set_pattern(cloned); + }); + } + } + + pub fn apply_title(&self, doc: &mut Document) { + doc.set("title", self.title); + } + + pub fn apply_published(&self, doc: &mut Document) { + if let Some(mut published_pattern) = doc.get_pattern("published") { + if let Some(published) = self.published.as_ref() { + let published_human = timeparse::format::human_long(published); + let published_machine = timeparse::format::iso8601(published); + + variables!(published_pattern, published_human, published_machine); + doc.set_pattern(published_pattern); + } + } + } + + pub fn apply_pathnav(&self, doc: &mut Document, path: RequestPath) { + let webpath = path.web_path(); + + let mut components: Vec<&str> = webpath.split('/').collect(); + // we don't want the name itself + components.pop(); + + if let Some(path_pattern) = doc.get_pattern("pathnav") { + let mut home = path_pattern.clone(); + home.set("pathnav_link", "/"); + home.set("pathnav_name", "home"); + doc.set_pattern(home); + + let count = components.len().saturating_sub(self.skip_path.unwrap_or(0)); + let mut link = String::from("/"); + for part in components.iter().take(count) { + link.push_str(&format!("{part}/")); + + let mut pat = path_pattern.clone(); + pat.set("pathnav_link", &link); + pat.set("pathnav_name", part); + doc.set_pattern(pat); + } + } + } +} |
