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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
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]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.80"
|
version = "0.1.80"
|
||||||
|
@ -207,6 +219,19 @@ version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
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]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
@ -328,6 +353,12 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "copy_dir"
|
name = "copy_dir"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
@ -1502,6 +1533,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
|
"blake3",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
|
|
|
@ -9,6 +9,7 @@ treehouse-format = { workspace = true }
|
||||||
|
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
axum = "0.7.4"
|
axum = "0.7.4"
|
||||||
|
blake3 = "1.5.3"
|
||||||
clap = { version = "4.3.22", features = ["derive"] }
|
clap = { version = "4.3.22", features = ["derive"] }
|
||||||
codespan-reporting = "0.11.1"
|
codespan-reporting = "0.11.1"
|
||||||
copy_dir = "0.1.3"
|
copy_dir = "0.1.3"
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod static_urls;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
|
@ -11,9 +13,10 @@ use codespan_reporting::{
|
||||||
files::Files as _,
|
files::Files as _,
|
||||||
};
|
};
|
||||||
use copy_dir::copy_dir;
|
use copy_dir::copy_dir;
|
||||||
use handlebars::Handlebars;
|
use handlebars::{handlebars_helper, Handlebars};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use static_urls::StaticUrls;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -214,6 +217,17 @@ impl Generator {
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
let mut config_derived_data = ConfigDerivedData::default();
|
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();
|
let mut template_file_ids = HashMap::new();
|
||||||
for entry in WalkDir::new(paths.template_dir) {
|
for entry in WalkDir::new(paths.template_dir) {
|
||||||
let entry = entry.context("cannot read directory entry")?;
|
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 anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, RawQuery, State},
|
extract::{Path, Query, RawQuery, State},
|
||||||
http::{
|
http::{
|
||||||
header::{CONTENT_TYPE, LOCATION},
|
header::{CACHE_CONTROL, CONTENT_TYPE, LOCATION},
|
||||||
HeaderValue, StatusCode,
|
HeaderValue, StatusCode,
|
||||||
},
|
},
|
||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
|
@ -16,6 +16,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use pulldown_cmark::escape::escape_html;
|
use pulldown_cmark::escape::escape_html;
|
||||||
|
use serde::Deserialize;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -111,9 +112,19 @@ async fn four_oh_four(State(state): State<Arc<Server>>) -> Response {
|
||||||
.into_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 {
|
if let Ok(file) = tokio::fs::read(state.target_dir.join("static").join(&path)).await {
|
||||||
let mut response = file.into_response();
|
let mut response = file.into_response();
|
||||||
|
|
||||||
if let Some(content_type) = get_content_type(&path) {
|
if let Some(content_type) = get_content_type(&path) {
|
||||||
response
|
response
|
||||||
.headers_mut()
|
.headers_mut()
|
||||||
|
@ -121,6 +132,14 @@ async fn static_file(Path(path): Path<String>, State(state): State<Arc<Server>>)
|
||||||
} else {
|
} else {
|
||||||
response.headers_mut().remove(CONTENT_TYPE);
|
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
|
response
|
||||||
} else {
|
} else {
|
||||||
four_oh_four(State(state)).await
|
four_oh_four(State(state)).await
|
||||||
|
|
|
@ -98,12 +98,17 @@ body::selection {
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'RecVar';
|
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-face {
|
||||||
font-family: 'RecVarMono';
|
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;
|
font-variation-settings: "MONO" 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
|
<link rel="stylesheet" href="{{ asset 'css/main.css' }}">
|
||||||
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
|
<link rel="stylesheet" href="{{ asset 'css/tree.css' }}">
|
||||||
|
|
||||||
<script type="importmap">{
|
<script type="importmap">{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
@ -42,15 +42,15 @@ It just needs to be a string replacement.
|
||||||
<meta property="og:image:alt" content="{{ page.thumbnail.alt }}">
|
<meta property="og:image:alt" content="{{ page.thumbnail.alt }}">
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<link rel="icon" sizes="16x16" href="{{ config.site }}/static/favicon/{{ season }}@1x.png">
|
<link rel="icon" sizes="16x16" href="{{ asset (cat (cat 'favicon/' season) '@1x.png') }}">
|
||||||
<link rel="icon" sizes="32x32" href="{{ config.site }}/static/favicon/{{ season }}@2x.png">
|
<link rel="icon" sizes="32x32" href="{{ asset (cat (cat 'favicon/' season) '@2x.png') }}">
|
||||||
<link rel="icon" sizes="64x64" href="{{ config.site }}/static/favicon/{{ season }}@4x.png">
|
<link rel="icon" sizes="64x64" href="{{ asset (cat (cat 'favicon/' season) '@4x.png') }}">
|
||||||
<link rel="icon" sizes="128x128" href="{{ config.site }}/static/favicon/{{ season }}@8x.png">
|
<link rel="icon" sizes="128x128" href="{{ asset (cat (cat 'favicon/' season) '@8x.png') }}">
|
||||||
<link rel="icon" sizes="256x256" href="{{ config.site }}/static/favicon/{{ season }}@16x.png">
|
<link rel="icon" sizes="256x256" href="{{ asset (cat (cat 'favicon/' season) '@16x.png') }}">
|
||||||
<link rel="icon" sizes="512x512" href="{{ config.site }}/static/favicon/{{ season }}@32x.png">
|
<link rel="icon" sizes="512x512" href="{{ asset (cat (cat '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="16x16" href="{{ asset (cat (cat '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="32x32" href="{{ asset (cat (cat '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="64x64" href="{{ asset (cat (cat '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="128x128" href="{{ asset (cat (cat '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="256x256" href="{{ asset (cat (cat 'favicon/' season) '@16x.png') }}">
|
||||||
<link rel="apple-touch-icon" sizes="512x512" href="{{ config.site }}/static/favicon/{{ season }}@32x.png">
|
<link rel="apple-touch-icon" sizes="512x512" href="{{ asset (cat (cat 'favicon/' season) '@32x.png') }}">
|
||||||
|
|
Loading…
Reference in a new issue