caching static resources

This commit is contained in:
りき萌 2024-07-19 18:04:11 +02:00
parent 902191d91c
commit 9bf3409197
7 changed files with 168 additions and 21 deletions

View file

@ -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"

View file

@ -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")?;

View file

@ -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<HashMap<String, String>>,
}
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<String, io::Error> {
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<ScopedJson<'reg, 'rc>, 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"))
}
}

View file

@ -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<Arc<Server>>) -> Response {
.into_response()
}
async fn static_file(Path(path): Path<String>, State(state): State<Arc<Server>>) -> Response {
#[derive(Deserialize)]
struct StaticFileQuery {
cache: Option<String>,
}
async fn static_file(
Path(path): Path<String>,
Query(query): Query<StaticFileQuery>,
State(state): State<Arc<Server>>,
) -> 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<String>, State(state): State<Arc<Server>>)
} 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