Skip to content

Commit

Permalink
Change get_url(cachebust=true) to use a hash (#1032)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
dancek and Keats authored May 23, 2020
1 parent e1c8c01 commit 36ec33f
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 14 deletions.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion components/site/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ impl Site {
pub fn register_early_global_fns(&mut self) {
self.tera.register_function(
"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(
"resize_image",
Expand Down
1 change: 1 addition & 0 deletions components/templates/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ toml = "0.5"
csv = "1"
image = "0.23"
serde_json = "1.0"
sha2 = "0.8"
url = "2"

errors = { path = "../errors" }
Expand Down
66 changes: 54 additions & 12 deletions components/templates/src/global_fns/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::path::PathBuf;
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 config::Config;
Expand Down Expand Up @@ -47,10 +49,11 @@ impl TeraFn for Trans {
pub struct GetUrl {
config: Config,
permalinks: HashMap<String, String>,
content_path: PathBuf,
}
impl GetUrl {
pub fn new(config: Config, permalinks: HashMap<String, String>) -> Self {
Self { config, permalinks }
pub fn new(config: Config, permalinks: HashMap<String, String>, content_path: PathBuf) -> Self {
Self { config, permalinks, content_path }
}
}

Expand All @@ -71,6 +74,13 @@ fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result<Stri
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 {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
let cachebust =
Expand Down Expand Up @@ -110,7 +120,11 @@ impl TeraFn for GetUrl {
}

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())
}
Expand Down Expand Up @@ -368,28 +382,56 @@ mod tests {
use super::{GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans};

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 lazy_static::lazy_static;

use tera::{to_value, Function, Value};

use config::{Config, Taxonomy as TaxonomyConfig};
use library::{Library, Taxonomy, TaxonomyItem};
use utils::fs::{create_directory, create_file};
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]
fn can_add_cachebust_to_url() {
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();
args.insert("path".to_string(), to_value("app.css").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]
fn can_add_trailing_slashes() {
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();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("trailing_slash".to_string(), to_value(true).unwrap());
Expand All @@ -399,18 +441,18 @@ mod tests {
#[test]
fn can_add_slashes_and_cachebust() {
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();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("trailing_slash".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]
fn can_link_to_some_static_file() {
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();
args.insert("path".to_string(), to_value("app.css").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css");
Expand Down Expand Up @@ -597,7 +639,7 @@ title = "A title"
#[test]
fn error_when_language_not_available() {
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();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("it").unwrap());
Expand All @@ -620,7 +662,7 @@ title = "A title"
"a_section/a_page.en.md".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();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("fr").unwrap());
Expand All @@ -642,7 +684,7 @@ title = "A title"
"a_section/a_page.en.md".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();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("en").unwrap());
Expand Down
2 changes: 1 addition & 1 deletion docs/content/documentation/templates/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ An example is:
{{/* 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.


Expand Down

0 comments on commit 36ec33f

Please sign in to comment.