caching static resources
This commit is contained in:
parent
902191d91c
commit
9bf3409197
7 changed files with 168 additions and 21 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue