From 9bf34091978f513482169180ea4b24393ce48c4f Mon Sep 17 00:00:00 2001 From: liquidev Date: Fri, 19 Jul 2024 18:04:11 +0200 Subject: [PATCH] caching static resources --- Cargo.lock | 32 ++++++++ crates/treehouse/Cargo.toml | 1 + crates/treehouse/src/cli/generate.rs | 16 +++- .../treehouse/src/cli/generate/static_urls.rs | 76 +++++++++++++++++++ crates/treehouse/src/cli/serve.rs | 25 +++++- static/css/main.css | 9 ++- template/components/_head.hbs | 30 ++++---- 7 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 crates/treehouse/src/cli/generate/static_urls.rs diff --git a/Cargo.lock b/Cargo.lock index 2d9a9d5..3351f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,18 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-trait" version = "0.1.80" @@ -207,6 +219,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "blake3" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9ec96fe9a81b5e365f9db71fe00edc4fe4ca2cc7dcb7861f0603012a7caa210" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -328,6 +353,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "copy_dir" version = "0.1.3" @@ -1502,6 +1533,7 @@ dependencies = [ "anyhow", "axum", "base64", + "blake3", "chrono", "clap", "codespan-reporting", diff --git a/crates/treehouse/Cargo.toml b/crates/treehouse/Cargo.toml index cdd3438..feba157 100644 --- a/crates/treehouse/Cargo.toml +++ b/crates/treehouse/Cargo.toml @@ -9,6 +9,7 @@ treehouse-format = { workspace = true } anyhow = "1.0.75" axum = "0.7.4" +blake3 = "1.5.3" clap = { version = "4.3.22", features = ["derive"] } codespan-reporting = "0.11.1" copy_dir = "0.1.3" diff --git a/crates/treehouse/src/cli/generate.rs b/crates/treehouse/src/cli/generate.rs index 17c3887..a37948f 100644 --- a/crates/treehouse/src/cli/generate.rs +++ b/crates/treehouse/src/cli/generate.rs @@ -1,3 +1,5 @@ +mod static_urls; + use std::{ collections::HashMap, ffi::OsStr, @@ -11,9 +13,10 @@ use codespan_reporting::{ files::Files as _, }; use copy_dir::copy_dir; -use handlebars::Handlebars; +use handlebars::{handlebars_helper, Handlebars}; use log::{debug, error, info}; use serde::Serialize; +use static_urls::StaticUrls; use walkdir::WalkDir; use crate::{ @@ -214,6 +217,17 @@ impl Generator { let mut handlebars = Handlebars::new(); let mut config_derived_data = ConfigDerivedData::default(); + handlebars_helper!(cat: |a: String, b: String| a + &b); + + handlebars.register_helper("cat", Box::new(cat)); + handlebars.register_helper( + "asset", + Box::new(StaticUrls::new( + paths.static_dir.to_owned(), + format!("{}/static", config.site), + )), + ); + let mut template_file_ids = HashMap::new(); for entry in WalkDir::new(paths.template_dir) { let entry = entry.context("cannot read directory entry")?; diff --git a/crates/treehouse/src/cli/generate/static_urls.rs b/crates/treehouse/src/cli/generate/static_urls.rs new file mode 100644 index 0000000..2828eaf --- /dev/null +++ b/crates/treehouse/src/cli/generate/static_urls.rs @@ -0,0 +1,76 @@ +use std::{ + collections::HashMap, + fs::File, + io::{self, BufReader}, + path::PathBuf, + sync::RwLock, +}; + +use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; +use serde_json::Value; + +pub struct StaticUrls { + base_dir: PathBuf, + base_url: String, + // Really annoying that we have to use an RwLock for this. We only ever generate in a + // single-threaded environment. + // Honestly it would be a lot more efficient if Handlebars just assumed single-threadedness + // and required you to clone it over to different threads. + // Stuff like this is why I really want to implement my own templating engine... + hash_cache: RwLock>, +} + +impl StaticUrls { + pub fn new(base_dir: PathBuf, base_url: String) -> Self { + Self { + base_dir, + base_url, + hash_cache: RwLock::new(HashMap::new()), + } + } + + pub fn get(&self, filename: &str) -> Result { + let hash_cache = self.hash_cache.read().unwrap(); + if let Some(cached) = hash_cache.get(filename) { + return Ok(cached.to_owned()); + } + drop(hash_cache); + + let mut hasher = blake3::Hasher::new(); + let file = BufReader::new(File::open(self.base_dir.join(filename))?); + hasher.update_reader(file)?; + // NOTE: Here the hash is truncated to 8 characters. This is fine, because we don't + // care about security here - only detecting changes in files. + let hash = format!( + "{}/{}?cache=b3-{}", + self.base_url, + filename, + &hasher.finalize().to_hex()[0..8] + ); + { + let mut hash_cache = self.hash_cache.write().unwrap(); + hash_cache.insert(filename.to_owned(), hash.clone()); + } + Ok(hash) + } +} + +impl HelperDef for StaticUrls { + fn call_inner<'reg: 'rc, 'rc>( + &self, + helper: &Helper<'reg, 'rc>, + _: &'reg Handlebars<'reg>, + _: &'rc Context, + _: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) { + return Ok(ScopedJson::Derived(Value::String( + self.get(param).map_err(|error| { + RenderError::new(format!("cannot get asset url for {param}: {error}")) + })?, + ))); + } + + Err(RenderError::new("asset path must be provided")) + } +} diff --git a/crates/treehouse/src/cli/serve.rs b/crates/treehouse/src/cli/serve.rs index f0138c6..99269b3 100644 --- a/crates/treehouse/src/cli/serve.rs +++ b/crates/treehouse/src/cli/serve.rs @@ -5,9 +5,9 @@ use std::{net::Ipv4Addr, path::PathBuf, sync::Arc}; use anyhow::Context; use axum::{ - extract::{Path, RawQuery, State}, + extract::{Path, Query, RawQuery, State}, http::{ - header::{CONTENT_TYPE, LOCATION}, + header::{CACHE_CONTROL, CONTENT_TYPE, LOCATION}, HeaderValue, StatusCode, }, response::{Html, IntoResponse, Response}, @@ -16,6 +16,7 @@ use axum::{ }; use log::{error, info}; use pulldown_cmark::escape::escape_html; +use serde::Deserialize; use tokio::net::TcpListener; use crate::{ @@ -111,9 +112,19 @@ async fn four_oh_four(State(state): State>) -> Response { .into_response() } -async fn static_file(Path(path): Path, State(state): State>) -> Response { +#[derive(Deserialize)] +struct StaticFileQuery { + cache: Option, +} + +async fn static_file( + Path(path): Path, + Query(query): Query, + State(state): State>, +) -> Response { if let Ok(file) = tokio::fs::read(state.target_dir.join("static").join(&path)).await { let mut response = file.into_response(); + if let Some(content_type) = get_content_type(&path) { response .headers_mut() @@ -121,6 +132,14 @@ async fn static_file(Path(path): Path, State(state): State>) } else { response.headers_mut().remove(CONTENT_TYPE); } + + if query.cache.is_some() { + response.headers_mut().insert( + CACHE_CONTROL, + HeaderValue::from_static("public, max-age=31536000, immutable"), + ); + } + response } else { four_oh_four(State(state)).await diff --git a/static/css/main.css b/static/css/main.css index f228330..691c500 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -98,12 +98,17 @@ body::selection { @font-face { font-family: 'RecVar'; - src: url('../font/Recursive_VF_1.085.woff2'); + /* NOTE: I put the hash in here manually instead of adding the complexity of piping CSS through + Handlebars because I don't really think it's worth it for this single asset. + Other assets are referenced rarely enough that caching probably isn't gonna make too much of + an impact. + It's unlikely I'll ever update the font anyways, so eh, whatever. */ + src: url('../font/Recursive_VF_1.085.woff2?cache=b3-445487d5'); } @font-face { font-family: 'RecVarMono'; - src: url('../font/Recursive_VF_1.085.woff2'); + src: url('../font/Recursive_VF_1.085.woff2?cache=b3-445487d5'); font-variation-settings: "MONO" 1.0; } diff --git a/template/components/_head.hbs b/template/components/_head.hbs index 2b4dfdd..310db9c 100644 --- a/template/components/_head.hbs +++ b/template/components/_head.hbs @@ -4,10 +4,10 @@ - - - + +