caching static resources
This commit is contained in:
parent
902191d91c
commit
9bf3409197
32
Cargo.lock
generated
32
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")?;
|
||||
|
|
76
crates/treehouse/src/cli/generate/static_urls.rs
Normal file
76
crates/treehouse/src/cli/generate/static_urls.rs
Normal 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"))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="preload" href="{{ config.site }}/static/font/Recursive_VF_1.085.woff2" as="font" type="font/woff2"
|
||||
<link rel="preload" href="{{ asset 'font/Recursive_VF_1.085.woff2' }}" as="font" type="font/woff2"
|
||||
crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
|
||||
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
|
||||
<link rel="stylesheet" href="{{ asset 'css/main.css' }}">
|
||||
<link rel="stylesheet" href="{{ asset 'css/tree.css' }}">
|
||||
|
||||
<script type="importmap">{
|
||||
"imports": {
|
||||
|
@ -42,15 +42,15 @@ It just needs to be a string replacement.
|
|||
<meta property="og:image:alt" content="{{ page.thumbnail.alt }}">
|
||||
{{/if}}
|
||||
|
||||
<link rel="icon" sizes="16x16" href="{{ config.site }}/static/favicon/{{ season }}@1x.png">
|
||||
<link rel="icon" sizes="32x32" href="{{ config.site }}/static/favicon/{{ season }}@2x.png">
|
||||
<link rel="icon" sizes="64x64" href="{{ config.site }}/static/favicon/{{ season }}@4x.png">
|
||||
<link rel="icon" sizes="128x128" href="{{ config.site }}/static/favicon/{{ season }}@8x.png">
|
||||
<link rel="icon" sizes="256x256" href="{{ config.site }}/static/favicon/{{ season }}@16x.png">
|
||||
<link rel="icon" sizes="512x512" href="{{ config.site }}/static/favicon/{{ season }}@32x.png">
|
||||
<link rel="apple-touch-icon" sizes="16x16" href="{{ config.site }}/static/favicon/{{ season }}@1x.png">
|
||||
<link rel="apple-touch-icon" sizes="32x32" href="{{ config.site }}/static/favicon/{{ season }}@2x.png">
|
||||
<link rel="apple-touch-icon" sizes="64x64" href="{{ config.site }}/static/favicon/{{ season }}@4x.png">
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="{{ config.site }}/static/favicon/{{ season }}@8x.png">
|
||||
<link rel="apple-touch-icon" sizes="256x256" href="{{ config.site }}/static/favicon/{{ season }}@16x.png">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{ config.site }}/static/favicon/{{ season }}@32x.png">
|
||||
<link rel="icon" sizes="16x16" href="{{ asset (cat (cat 'favicon/' season) '@1x.png') }}">
|
||||
<link rel="icon" sizes="32x32" href="{{ asset (cat (cat 'favicon/' season) '@2x.png') }}">
|
||||
<link rel="icon" sizes="64x64" href="{{ asset (cat (cat 'favicon/' season) '@4x.png') }}">
|
||||
<link rel="icon" sizes="128x128" href="{{ asset (cat (cat 'favicon/' season) '@8x.png') }}">
|
||||
<link rel="icon" sizes="256x256" href="{{ asset (cat (cat 'favicon/' season) '@16x.png') }}">
|
||||
<link rel="icon" sizes="512x512" href="{{ asset (cat (cat 'favicon/' season) '@32x.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="16x16" href="{{ asset (cat (cat 'favicon/' season) '@1x.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="32x32" href="{{ asset (cat (cat 'favicon/' season) '@2x.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="64x64" href="{{ asset (cat (cat 'favicon/' season) '@4x.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="{{ asset (cat (cat 'favicon/' season) '@8x.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="256x256" href="{{ asset (cat (cat 'favicon/' season) '@16x.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{ asset (cat (cat 'favicon/' season) '@32x.png') }}">
|
||||
|
|
Loading…
Reference in a new issue