about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock96
-rw-r--r--Cargo.toml3
-rw-r--r--readme.md56
-rw-r--r--src/error.rs28
-rw-r--r--src/fs.rs52
-rw-r--r--src/main.rs150
-rw-r--r--src/settings.rs6
-rw-r--r--src/templated.rs125
8 files changed, 490 insertions, 26 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ea690e2..0976342 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -39,8 +39,11 @@ name = "awake"
 version = "0.1.0"
 dependencies = [
  "axum",
+ "bempline",
  "camino",
  "snafu",
+ "tokio",
+ "tokio-util",
 ]
 
 [[package]]
@@ -114,6 +117,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "bempline"
+version = "0.9.0"
+source = "git+https://github.com/gennyble/bempline#8de6d7f96e98e948acb6e823003775571417a8a2"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
 name = "bytes"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -235,6 +249,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
+name = "hermit-abi"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
+
+[[package]]
 name = "http"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -338,6 +358,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
 
 [[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
 name = "log"
 version = "0.4.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -382,6 +412,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
 name = "object"
 version = "0.32.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -397,6 +437,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
 
 [[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
 name = "percent-encoding"
 version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -453,6 +516,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
 name = "rustc-demangle"
 version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -471,6 +543,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
 
 [[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
 name = "serde"
 version = "1.0.197"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -524,6 +602,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "slab"
 version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -533,6 +620,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "smallvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+
+[[package]]
 name = "snafu"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -590,7 +683,10 @@ dependencies = [
  "bytes",
  "libc",
  "mio",
+ "num_cpus",
+ "parking_lot",
  "pin-project-lite",
+ "signal-hook-registry",
  "socket2",
  "tokio-macros",
  "windows-sys",
diff --git a/Cargo.toml b/Cargo.toml
index 628bff6..220d7e3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,3 +9,6 @@ edition = "2021"
 axum = "0.7.4"
 camino = "1.1.6"
 snafu = "0.8.0"
+bempline = { git = "https://github.com/gennyble/bempline" }
+tokio = { version = "1.36.0", features = ["full"] }
+tokio-util = { version = "0.7.10", features = ["io"] }
diff --git a/readme.md b/readme.md
index c7a1258..b7a3651 100644
--- a/readme.md
+++ b/readme.md
@@ -1,3 +1,11 @@
+# For Later
+**Single-tile weather radar**  
+Using the RainViewer API for the radar and an OpenStreetMap tileserver for the
+map tile. Make a gif with a few frames of radar so we can embed it on the site.
+
+Zoomed out far enough that I'm not worried about opsec and with no marker for
+where I am in the tile.
+
 ## Dirfiles
 These are files that match the name of the directory.
 
@@ -10,4 +18,50 @@ the directory name changes.
 
 What do you name the root file, then? You shouldn't have to match the webroot's
 directory name. Perhaps it should be configurable. I think for now we'll
-hard-code in `home.html`, though.
\ No newline at end of file
+hard-code in `home.html`, though.
+
+## Page-content uses templates
+Because writing the same outer-html for everything poses a few problems. I'll
+enumerate them for fun!
+1) it makes keeping a consistent page style difficult.
+2) making a style change or renaming a core-stylesheet would require going
+   through and editing a large number of files.
+3) helps separate the layout and content of the page.
+
+We're using the bempline templating engine. Not because it's the best or even
+really *that good*, but because I wrote it and I like it :)
+
+Page-content files will have frontmatter in the form:
+```
+---
+key=value
+---
+```
+
+blank lines and comments are acceptable. comments are lines starting with a `#`
+
+with these keys being common and defined.
+
+**title** (default: filename)  
+the `<title>` of the page
+
+== TODO: THE BELOW IS DREAMING ==
+
+**og_title** (default: page title)  
+the opengraph title of the webpage
+
+**og_type** (default: website)  
+TODO: not currently implemented. everything is a website.
+
+**og_description** (default: none)  
+a one or two sentence description of the page.
+
+```html
+<!-- required tags. us an image requried? i do not have one.-->
+<meta property="og:title" content="nyble.dev | gone" />
+<meta property="og:type" content="website" />
+<meta property="og:url" content="https://nyble.dev/" />
+
+<!-- not required but helps make better cards, etc. -->
+<meta property="og:description" content="gone for now. stepped out for a bit." />
+```
\ No newline at end of file
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..56539d0
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,28 @@
+use std::io;
+
+use camino::Utf8PathBuf;
+
+#[derive(Debug, snafu::Snafu)]
+pub enum RuntimeError {
+	#[snafu(display("the path was not found: {path}"))]
+	NotFound {
+		source: io::Error,
+		path: Utf8PathBuf,
+	},
+	#[snafu(display("io error: {path}: {source}"))]
+	UnknownIo {
+		source: io::Error,
+		path: Utf8PathBuf,
+	},
+	#[snafu(display("path tried to go below webroot: {path}"))]
+	PathTooLow { path: String },
+}
+
+impl RuntimeError {
+	pub fn from_io(source: io::Error, path: Utf8PathBuf) -> Self {
+		match source.kind() {
+			io::ErrorKind::NotFound => RuntimeError::NotFound { source, path },
+			_ => RuntimeError::UnknownIo { source, path },
+		}
+	}
+}
diff --git a/src/fs.rs b/src/fs.rs
index e13f405..d9b8810 100644
--- a/src/fs.rs
+++ b/src/fs.rs
@@ -1,6 +1,9 @@
 use camino::{Utf8Path, Utf8PathBuf};
+use core::fmt;
 use std::{io, ops::Deref, str::FromStr};
 
+use crate::RuntimeError;
+
 /// Webpath is the path we get from HTTP requests. It's garunteed to not fall
 /// below the webroot and will never start or end with a slash.
 #[derive(Clone, Debug, PartialEq)]
@@ -60,9 +63,16 @@ impl PartialEq<str> for Webpath {
 	}
 }
 
+impl fmt::Display for Webpath {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "/{}", self.webcanon)
+	}
+}
+
 const ROOT_INDEX: &str = "home.html";
 const DIRFILE_EXT: &str = "html";
 
+#[derive(Clone, Debug)]
 pub struct Filesystem {
 	webroot: Utf8PathBuf,
 }
@@ -74,7 +84,8 @@ impl Filesystem {
 		}
 	}
 
-	fn resolve(&self, webpath: &Webpath) -> Result<Utf8PathBuf, RuntimeError> {
+	pub fn resolve(&self, webpath: &Webpath) -> Result<Utf8PathBuf, RuntimeError> {
+		println!("resolve = {webpath}");
 		if webpath.is_index() || webpath == ROOT_INDEX {
 			return Ok(self.webroot.join(ROOT_INDEX));
 		}
@@ -102,35 +113,28 @@ impl Filesystem {
 		}
 	}
 
-	fn metadata<P: AsRef<Utf8Path>>(path: P) -> Result<std::fs::Metadata, RuntimeError> {
+	pub fn metadata<P: AsRef<Utf8Path>>(path: P) -> Result<std::fs::Metadata, RuntimeError> {
 		path.as_ref()
 			.metadata()
 			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
 	}
-}
 
-#[derive(Debug, snafu::Snafu)]
-pub enum RuntimeError {
-	#[snafu(display("the path was not found: {path}"))]
-	NotFound {
-		source: io::Error,
-		path: Utf8PathBuf,
-	},
-	#[snafu(display("io error: {path}: {source}"))]
-	UnknownIo {
-		source: io::Error,
-		path: Utf8PathBuf,
-	},
-	#[snafu(display("path tried to go below webroot: {path}"))]
-	PathTooLow { path: String },
-}
+	pub async fn open<P: AsRef<Utf8Path>>(path: P) -> Result<tokio::fs::File, RuntimeError> {
+		tokio::fs::File::open(path.as_ref())
+			.await
+			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
+	}
 
-impl RuntimeError {
-	pub fn from_io(source: io::Error, path: Utf8PathBuf) -> Self {
-		match source.kind() {
-			io::ErrorKind::NotFound => RuntimeError::NotFound { source, path },
-			_ => RuntimeError::UnknownIo { source, path },
-		}
+	pub async fn read<P: AsRef<Utf8Path>>(path: P) -> Result<Vec<u8>, RuntimeError> {
+		tokio::fs::read(path.as_ref())
+			.await
+			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
+	}
+
+	pub async fn read_to_string<P: AsRef<Utf8Path>>(path: P) -> Result<String, RuntimeError> {
+		tokio::fs::read_to_string(path.as_ref())
+			.await
+			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
 	}
 }
 
diff --git a/src/main.rs b/src/main.rs
index 1ba94f7..c429e8a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,151 @@
+mod error;
 mod fs;
+mod settings;
+mod templated;
 
-fn main() {}
+use std::{os::unix::fs::MetadataExt, str::FromStr};
+
+use axum::{
+	body::Body, extract::Path, http::header, 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::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<Filesystem>, se: Extension<Settings>) -> Response {
+	handler(fse, se, Path(String::from("/"))).await
+}
+
+async fn handler(
+	Extension(fs): Extension<Filesystem>,
+	Extension(settings): Extension<Settings>,
+	Path(path): Path<String>,
+) -> 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<Response, RuntimeError> {
+	println!("raw = {path}");
+
+	let path = path.parse()?;
+
+	println!("path = {path}");
+
+	let filepath = fs.resolve(&path)?;
+
+	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()),
+		}
+	}
+}
+
+// 20 megabytes
+const STREAM_AFTER: u64 = 20 * 1024 * 1024;
+
+async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
+	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<Response, RuntimeError> {
+	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),
+	);
+
+	template.set("main", templated.content);
+
+	Ok(Response::builder()
+		.header(header::CONTENT_TYPE, "text/html")
+		.body(Body::from(template.compile()))
+		.unwrap())
+}
diff --git a/src/settings.rs b/src/settings.rs
new file mode 100644
index 0000000..9a31d56
--- /dev/null
+++ b/src/settings.rs
@@ -0,0 +1,6 @@
+use camino::Utf8PathBuf;
+
+#[derive(Clone, Debug)]
+pub struct Settings {
+	pub template_dir: Utf8PathBuf,
+}
diff --git a/src/templated.rs b/src/templated.rs
new file mode 100644
index 0000000..c6daac8
--- /dev/null
+++ b/src/templated.rs
@@ -0,0 +1,125 @@
+use std::str::FromStr;
+
+/// The content part of a file that fills in a template.
+pub struct Templated {
+	pub frontmatter: Frontmatter,
+	pub content: String,
+}
+
+impl FromStr for Templated {
+	type Err = TemplateError;
+
+	fn from_str(raw: &str) -> Result<Self, Self::Err> {
+		let (front, content) = match raw.strip_prefix("---\n") {
+			None => return Err(TemplateError::MissingFrontmatter),
+			Some(no_start) => match no_start.split_once("\n---\n") {
+				None => return Err(TemplateError::MissingFrontmatter),
+				Some((front, content)) => (front, content),
+			},
+		};
+
+		Ok(Self {
+			frontmatter: front.parse()?,
+			content: content.to_owned(),
+		})
+	}
+}
+
+pub struct Frontmatter {
+	entries: Vec<(String, String)>,
+}
+
+impl FromStr for Frontmatter {
+	type Err = TemplateError;
+
+	fn from_str(raw: &str) -> Result<Self, Self::Err> {
+		let mut entries = vec![];
+
+		for line in raw.lines() {
+			let trimmed = line.trim();
+			if trimmed.is_empty() || trimmed.starts_with('#') {
+				// Skip over blank lines and comments
+				continue;
+			}
+
+			let (key, value) = match trimmed.split_once('=') {
+				None => {
+					return Err(TemplateError::MalformedFrontmatterEntry {
+						line: trimmed.to_owned(),
+					});
+				}
+				Some(tup) => tup,
+			};
+
+			entries.push((key.trim().to_owned(), value.trim().to_owned()))
+		}
+
+		Ok(Self { entries })
+	}
+}
+
+impl Frontmatter {
+	pub fn get(&self, key: &str) -> Option<&str> {
+		self.entries
+			.iter()
+			.find(|(k, _)| k == key)
+			.map(|(_, v)| v.as_str())
+	}
+
+	pub fn get_many(&self, key: &str) -> Vec<&str> {
+		// this could probably be a filter_map() but the explicit filter and
+		// then map seems cleaner
+		self.entries
+			.iter()
+			.filter(|(k, _)| k == key)
+			.map(|(_, v)| v.as_str())
+			.collect()
+	}
+}
+
+#[derive(Debug, snafu::Snafu)]
+pub enum TemplateError {
+	#[snafu(display("file is missing the frontmatter"))]
+	MissingFrontmatter,
+	#[snafu(display("the frontmatter entry is malformed: {line}"))]
+	MalformedFrontmatterEntry { line: String },
+}
+
+#[cfg(test)]
+mod test {
+	use std::str::FromStr;
+
+	use crate::templated::{Frontmatter, Templated};
+
+	#[test]
+	fn frontmatter_parse() {
+		let frntmtr = "key=value\nkey2=value2";
+		assert!(Frontmatter::from_str(frntmtr).is_ok())
+	}
+
+	#[test]
+	fn frontmatter_fails_on_invalid_line() {
+		let front = "key1=value1\nincorrect line";
+		assert!(Frontmatter::from_str(front).is_err())
+	}
+
+	#[test]
+	fn templated_parse() {
+		let tempalted = "---\ntitle=Title!\n---\nContent line!";
+		assert!(Templated::from_str(tempalted).is_ok())
+	}
+
+	#[test]
+	fn templated_doesnt_eat_frontmatter_line() {
+		let templated = "---\ntitle=Title---\nContent";
+		assert!(Templated::from_str(templated).is_err());
+	}
+
+	#[test]
+	fn templated_parses_correct() {
+		let raw = "---\ntitle=Title!\n---\n<p>Paragraph!</p>";
+		let tmpl = Templated::from_str(raw).unwrap();
+		assert_eq!(tmpl.frontmatter.get("title").unwrap(), "Title!");
+		assert_eq!(tmpl.content, "<p>Paragraph!</p>")
+	}
+}