Change get_url(cachebust=true) to use a hash (#1032)

Cache-busting was previously done with a compile-time timestamp. Change
to the SHA-256 hash of the file to avoid refreshing unchanged files.

The implementation could be used to add a new global fn (say,
get_file_hash) for subresource integrity use, but that's for another
commit.

Fixes #519.

Co-authored-by: Vincent Prouillet <balthek@gmail.com>
This commit is contained in:
Hannu Hartikainen 2020-05-23 12:46:50 +03:00 committed by GitHub
parent e1c8c01149
commit 36ec33f042
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 14 deletions

13
Cargo.lock generated
View file

@ -2251,6 +2251,17 @@ dependencies = [
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "sha2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "0.1.1" version = "0.1.1"
@ -2430,6 +2441,7 @@ dependencies = [
"pulldown-cmark 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "pulldown-cmark 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)",
"sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tera 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tera 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
"url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -3210,6 +3222,7 @@ dependencies = [
"checksum serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)" = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" "checksum serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)" = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2"
"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" "checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97"
"checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" "checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
"checksum sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0"
"checksum shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" "checksum shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2"
"checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
"checksum siphasher 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" "checksum siphasher 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7"

View file

@ -533,7 +533,7 @@ impl Site {
pub fn register_early_global_fns(&mut self) { pub fn register_early_global_fns(&mut self) {
self.tera.register_function( self.tera.register_function(
"get_url", "get_url",
global_fns::GetUrl::new(self.config.clone(), self.permalinks.clone()), global_fns::GetUrl::new(self.config.clone(), self.permalinks.clone(), self.content_path.clone()),
); );
self.tera.register_function( self.tera.register_function(
"resize_image", "resize_image",

View file

@ -13,6 +13,7 @@ toml = "0.5"
csv = "1" csv = "1"
image = "0.23" image = "0.23"
serde_json = "1.0" serde_json = "1.0"
sha2 = "0.8"
url = "2" url = "2"
errors = { path = "../errors" } errors = { path = "../errors" }

View file

@ -1,7 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use std::{fs, io, result};
use sha2::{Digest, Sha256};
use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value}; use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value};
use config::Config; use config::Config;
@ -47,10 +49,11 @@ impl TeraFn for Trans {
pub struct GetUrl { pub struct GetUrl {
config: Config, config: Config,
permalinks: HashMap<String, String>, permalinks: HashMap<String, String>,
content_path: PathBuf,
} }
impl GetUrl { impl GetUrl {
pub fn new(config: Config, permalinks: HashMap<String, String>) -> Self { pub fn new(config: Config, permalinks: HashMap<String, String>, content_path: PathBuf) -> Self {
Self { config, permalinks } Self { config, permalinks, content_path }
} }
} }
@ -71,6 +74,13 @@ fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result<Stri
Ok(splitted_path.join(".")) Ok(splitted_path.join("."))
} }
fn compute_file_sha256(path: &PathBuf) -> result::Result<String, io::Error> {
let mut file = fs::File::open(path)?;
let mut hasher = Sha256::new();
io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.result()))
}
impl TeraFn for GetUrl { impl TeraFn for GetUrl {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> { fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
let cachebust = let cachebust =
@ -110,7 +120,11 @@ impl TeraFn for GetUrl {
} }
if cachebust { if cachebust {
permalink = format!("{}?t={}", permalink, self.config.build_timestamp.unwrap()); let full_path = self.content_path.join(&path);
permalink = match compute_file_sha256(&full_path) {
Ok(digest) => format!("{}?h={}", permalink, digest),
Err(_) => return Err(format!("Could not read file `{}`. Expected location: {}", path, full_path.to_str().unwrap()).into()),
};
} }
Ok(to_value(permalink).unwrap()) Ok(to_value(permalink).unwrap())
} }
@ -368,28 +382,56 @@ mod tests {
use super::{GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans}; use super::{GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans};
use std::collections::HashMap; use std::collections::HashMap;
use std::env::temp_dir;
use std::fs::remove_dir_all;
use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use lazy_static::lazy_static;
use tera::{to_value, Function, Value}; use tera::{to_value, Function, Value};
use config::{Config, Taxonomy as TaxonomyConfig}; use config::{Config, Taxonomy as TaxonomyConfig};
use library::{Library, Taxonomy, TaxonomyItem}; use library::{Library, Taxonomy, TaxonomyItem};
use utils::fs::{create_directory, create_file};
use utils::slugs::SlugifyStrategy; use utils::slugs::SlugifyStrategy;
struct TestContext {
content_path: PathBuf,
}
impl TestContext {
fn setup() -> Self {
let dir = temp_dir().join("test_global_fns");
create_directory(&dir).expect("Could not create test directory");
create_file(&dir.join("app.css"), "// Hello world!")
.expect("Could not create test content (app.css)");
Self { content_path: dir }
}
}
impl Drop for TestContext {
fn drop(&mut self) {
remove_dir_all(&self.content_path).expect("Could not free test directory");
}
}
lazy_static! {
static ref TEST_CONTEXT: TestContext = TestContext::setup();
}
#[test] #[test]
fn can_add_cachebust_to_url() { fn can_add_cachebust_to_url() {
let config = Config::default(); let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new()); let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("cachebust".to_string(), to_value(true).unwrap()); args.insert("cachebust".to_string(), to_value(true).unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css?t=1"); assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css?h=572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840");
} }
#[test] #[test]
fn can_add_trailing_slashes() { fn can_add_trailing_slashes() {
let config = Config::default(); let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new()); let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("trailing_slash".to_string(), to_value(true).unwrap()); args.insert("trailing_slash".to_string(), to_value(true).unwrap());
@ -399,18 +441,18 @@ mod tests {
#[test] #[test]
fn can_add_slashes_and_cachebust() { fn can_add_slashes_and_cachebust() {
let config = Config::default(); let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new()); let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("trailing_slash".to_string(), to_value(true).unwrap()); args.insert("trailing_slash".to_string(), to_value(true).unwrap());
args.insert("cachebust".to_string(), to_value(true).unwrap()); args.insert("cachebust".to_string(), to_value(true).unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/?t=1"); assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/?h=572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840");
} }
#[test] #[test]
fn can_link_to_some_static_file() { fn can_link_to_some_static_file() {
let config = Config::default(); let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new()); let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("path".to_string(), to_value("app.css").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css"); assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css");
@ -597,7 +639,7 @@ title = "A title"
#[test] #[test]
fn error_when_language_not_available() { fn error_when_language_not_available() {
let config = Config::parse(TRANS_CONFIG).unwrap(); let config = Config::parse(TRANS_CONFIG).unwrap();
let static_fn = GetUrl::new(config, HashMap::new()); let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("it").unwrap()); args.insert("lang".to_string(), to_value("it").unwrap());
@ -620,7 +662,7 @@ title = "A title"
"a_section/a_page.en.md".to_string(), "a_section/a_page.en.md".to_string(),
"https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(),
); );
let static_fn = GetUrl::new(config, permalinks); let static_fn = GetUrl::new(config, permalinks, TEST_CONTEXT.content_path.clone());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("fr").unwrap()); args.insert("lang".to_string(), to_value("fr").unwrap());
@ -642,7 +684,7 @@ title = "A title"
"a_section/a_page.en.md".to_string(), "a_section/a_page.en.md".to_string(),
"https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(),
); );
let static_fn = GetUrl::new(config, permalinks); let static_fn = GetUrl::new(config, permalinks, TEST_CONTEXT.content_path.clone());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("en").unwrap()); args.insert("lang".to_string(), to_value("en").unwrap());

View file

@ -142,7 +142,7 @@ An example is:
{{/* get_url(path="css/app.css", trailing_slash=true) */}} {{/* get_url(path="css/app.css", trailing_slash=true) */}}
``` ```
In the case of non-internal links, you can also add a cachebust of the format `?t=1290192` at the end of a URL In the case of non-internal links, you can also add a cachebust of the format `?h=<sha256>` at the end of a URL
by passing `cachebust=true` to the `get_url` function. by passing `cachebust=true` to the `get_url` function.