Fix cachebust for files in the output path (#1586)

* Consider the site's output path in search_for_file

The search_for_file helper function now accepts an optional
output path. If passed, the file will also be searched there.
This is used in the get_url function to search in the
Site::output_path.

In practice, this means cachebust works for files in the
output path.

* Make output_dir required in search_for_file

* Update docs for file searching logic

* Add test for new file searching behavior
This commit is contained in:
Koen Bolhuis 2021-08-19 07:55:40 +02:00 committed by GitHub
parent c658d171f8
commit 5ca2b29c49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 113 additions and 67 deletions

View file

@ -23,6 +23,7 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> {
site.base_path.clone(), site.base_path.clone(),
site.config.clone(), site.config.clone(),
site.permalinks.clone(), site.permalinks.clone(),
site.output_path.clone(),
), ),
); );
site.tera.register_function( site.tera.register_function(
@ -31,15 +32,24 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> {
site.base_path.clone(), site.base_path.clone(),
site.imageproc.clone(), site.imageproc.clone(),
site.config.theme.clone(), site.config.theme.clone(),
site.output_path.clone(),
), ),
); );
site.tera.register_function( site.tera.register_function(
"get_image_metadata", "get_image_metadata",
global_fns::GetImageMetadata::new(site.base_path.clone(), site.config.theme.clone()), global_fns::GetImageMetadata::new(
site.base_path.clone(),
site.config.theme.clone(),
site.output_path.clone(),
),
); );
site.tera.register_function( site.tera.register_function(
"load_data", "load_data",
global_fns::LoadData::new(site.base_path.clone(), site.config.theme.clone()), global_fns::LoadData::new(
site.base_path.clone(),
site.config.theme.clone(),
site.output_path.clone(),
),
); );
site.tera.register_function("trans", global_fns::Trans::new(site.config.clone())); site.tera.register_function("trans", global_fns::Trans::new(site.config.clone()));
site.tera.register_function( site.tera.register_function(
@ -52,7 +62,11 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> {
); );
site.tera.register_function( site.tera.register_function(
"get_file_hash", "get_file_hash",
global_fns::GetFileHash::new(site.base_path.clone(), site.config.theme.clone()), global_fns::GetFileHash::new(
site.base_path.clone(),
site.config.theme.clone(),
site.output_path.clone(),
),
); );
Ok(()) Ok(())

View file

@ -31,11 +31,17 @@ pub struct GetUrl {
base_path: PathBuf, base_path: PathBuf,
config: Config, config: Config,
permalinks: HashMap<String, String>, permalinks: HashMap<String, String>,
output_path: PathBuf,
} }
impl GetUrl { impl GetUrl {
pub fn new(base_path: PathBuf, config: Config, permalinks: HashMap<String, String>) -> Self { pub fn new(
Self { base_path, config, permalinks } base_path: PathBuf,
config: Config,
permalinks: HashMap<String, String>,
output_path: PathBuf,
) -> Self {
Self { base_path, config, permalinks, output_path }
} }
} }
@ -111,7 +117,7 @@ impl TeraFn for GetUrl {
} }
if cachebust { if cachebust {
match search_for_file(&self.base_path, &path_with_lang, &self.config.theme) match search_for_file(&self.base_path, &path_with_lang, &self.config.theme, &self.output_path)
.map_err(|e| format!("`get_url`: {}", e))? .map_err(|e| format!("`get_url`: {}", e))?
.and_then(|(p, _)| fs::File::open(&p).ok()) .and_then(|(p, _)| fs::File::open(&p).ok())
.and_then(|f| compute_file_hash::<Sha256>(f, false).ok()) .and_then(|f| compute_file_hash::<Sha256>(f, false).ok())
@ -142,10 +148,11 @@ impl TeraFn for GetUrl {
pub struct GetFileHash { pub struct GetFileHash {
base_path: PathBuf, base_path: PathBuf,
theme: Option<String>, theme: Option<String>,
output_path: PathBuf,
} }
impl GetFileHash { impl GetFileHash {
pub fn new(base_path: PathBuf, theme: Option<String>) -> Self { pub fn new(base_path: PathBuf, theme: Option<String>, output_path: PathBuf) -> Self {
Self { base_path, theme } Self { base_path, theme, output_path }
} }
} }
@ -169,7 +176,7 @@ impl TeraFn for GetFileHash {
) )
.unwrap_or(true); .unwrap_or(true);
let file_path = match search_for_file(&self.base_path, &path, &self.theme) let file_path = match search_for_file(&self.base_path, &path, &self.theme, &self.output_path)
.map_err(|e| format!("`get_file_hash`: {}", e))? .map_err(|e| format!("`get_file_hash`: {}", e))?
{ {
Some((f, _)) => f, Some((f, _)) => f,
@ -204,6 +211,8 @@ mod tests {
use super::{GetFileHash, GetUrl}; use super::{GetFileHash, GetUrl};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::create_dir;
use std::path::PathBuf;
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use tera::{to_value, Function}; use tera::{to_value, Function};
@ -232,7 +241,7 @@ title = "A title"
#[test] #[test]
fn can_add_cachebust_to_url() { fn can_add_cachebust_to_url() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new(), PathBuf::new());
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());
@ -242,7 +251,7 @@ title = "A title"
#[test] #[test]
fn can_add_trailing_slashes() { fn can_add_trailing_slashes() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new(), PathBuf::new());
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());
@ -252,7 +261,7 @@ title = "A title"
#[test] #[test]
fn can_add_slashes_and_cachebust() { fn can_add_slashes_and_cachebust() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new(), PathBuf::new());
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());
@ -263,7 +272,7 @@ title = "A title"
#[test] #[test]
fn can_link_to_some_static_file() { fn can_link_to_some_static_file() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new(), PathBuf::new());
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");
@ -273,11 +282,25 @@ title = "A title"
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");
} }
#[test]
fn can_link_to_file_in_output_path() {
let dir = create_temp_dir();
let public = dir.path().join("public");
create_dir(&public).expect("Failed to create output directory");
create_file(&public.join("style.css"), "// Hello world")
.expect("Failed to create file in output directory");
let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new(), public);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("style.css").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/style.css");
}
#[test] #[test]
fn error_when_language_not_available() { fn error_when_language_not_available() {
let config = Config::parse(CONFIG_DATA).unwrap(); let config = Config::parse(CONFIG_DATA).unwrap();
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), config, HashMap::new()); let static_fn = GetUrl::new(dir.path().to_path_buf(), config, HashMap::new(), PathBuf::new());
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());
@ -301,7 +324,7 @@ title = "A title"
); );
let config = Config::parse(CONFIG_DATA).unwrap(); let config = Config::parse(CONFIG_DATA).unwrap();
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks); let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks, PathBuf::new());
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());
@ -324,7 +347,7 @@ title = "A title"
"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 dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks); let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks, PathBuf::new());
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());
@ -338,7 +361,7 @@ title = "A title"
fn can_get_feed_url_with_default_language() { fn can_get_feed_url_with_default_language() {
let config = Config::parse(CONFIG_DATA).unwrap(); let config = Config::parse(CONFIG_DATA).unwrap();
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new()); let static_fn = GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); args.insert("path".to_string(), to_value(config.feed_filename).unwrap());
args.insert("lang".to_string(), to_value("fr").unwrap()); args.insert("lang".to_string(), to_value("fr").unwrap());
@ -349,7 +372,7 @@ title = "A title"
fn can_get_feed_url_with_other_language() { fn can_get_feed_url_with_other_language() {
let config = Config::parse(CONFIG_DATA).unwrap(); let config = Config::parse(CONFIG_DATA).unwrap();
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new()); let static_fn = GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); args.insert("path".to_string(), to_value(config.feed_filename).unwrap());
args.insert("lang".to_string(), to_value("en").unwrap()); args.insert("lang".to_string(), to_value("en").unwrap());
@ -359,7 +382,7 @@ title = "A title"
#[test] #[test]
fn can_get_file_hash_sha256_no_base64() { fn can_get_file_hash_sha256_no_base64() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path(), None); let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new());
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("sha_type".to_string(), to_value(256).unwrap()); args.insert("sha_type".to_string(), to_value(256).unwrap());
@ -373,7 +396,7 @@ title = "A title"
#[test] #[test]
fn can_get_file_hash_sha256_base64() { fn can_get_file_hash_sha256_base64() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path(), None); let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new());
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("sha_type".to_string(), to_value(256).unwrap()); args.insert("sha_type".to_string(), to_value(256).unwrap());
@ -384,7 +407,7 @@ title = "A title"
#[test] #[test]
fn can_get_file_hash_sha384_no_base64() { fn can_get_file_hash_sha384_no_base64() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path(), None); let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new());
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("base64".to_string(), to_value(false).unwrap()); args.insert("base64".to_string(), to_value(false).unwrap());
@ -397,7 +420,7 @@ title = "A title"
#[test] #[test]
fn can_get_file_hash_sha384() { fn can_get_file_hash_sha384() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path(), None); let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new());
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!( assert_eq!(
@ -409,7 +432,7 @@ title = "A title"
#[test] #[test]
fn can_get_file_hash_sha512_no_base64() { fn can_get_file_hash_sha512_no_base64() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path(), None); let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new());
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("sha_type".to_string(), to_value(512).unwrap()); args.insert("sha_type".to_string(), to_value(512).unwrap());
@ -423,7 +446,7 @@ title = "A title"
#[test] #[test]
fn can_get_file_hash_sha512() { fn can_get_file_hash_sha512() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path(), None); let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new());
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("sha_type".to_string(), to_value(512).unwrap()); args.insert("sha_type".to_string(), to_value(512).unwrap());
@ -436,7 +459,7 @@ title = "A title"
#[test] #[test]
fn error_when_file_not_found_for_hash() { fn error_when_file_not_found_for_hash() {
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path(), None); let static_fn = GetFileHash::new(dir.into_path(), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("doesnt-exist").unwrap()); args.insert("path".to_string(), to_value("doesnt-exist").unwrap());
let err = format!("{}", static_fn.call(&args).unwrap_err()); let err = format!("{}", static_fn.call(&args).unwrap_err());

View file

@ -5,11 +5,12 @@ use errors::{bail, Result};
use utils::fs::is_path_in_directory; use utils::fs::is_path_in_directory;
/// This is used by a few Tera functions to search for files on the filesystem. /// This is used by a few Tera functions to search for files on the filesystem.
/// This does try to find the file in 3 different spots: /// This does try to find the file in 5 different spots:
/// 1. base_path + path /// 1. base_path + path
/// 2. base_path + static + path /// 2. base_path + static + path
/// 3. base_path + content + path /// 3. base_path + content + path
/// 4. base_path + themes + {current_theme} + static + path /// 4. base_path + {output dir} + path
/// 5. base_path + themes + {current_theme} + static + path
/// A path starting with @/ will replace it with `content/` and a path starting with `/` will have /// A path starting with @/ will replace it with `content/` and a path starting with `/` will have
/// it removed. /// it removed.
/// It also returns the unified path so it can be used as unique hash for a given file. /// It also returns the unified path so it can be used as unique hash for a given file.
@ -18,8 +19,9 @@ pub fn search_for_file(
base_path: &Path, base_path: &Path,
path: &str, path: &str,
theme: &Option<String>, theme: &Option<String>,
output_path: &Path,
) -> Result<Option<(PathBuf, String)>> { ) -> Result<Option<(PathBuf, String)>> {
let mut search_paths = vec![base_path.join("static"), base_path.join("content")]; let mut search_paths = vec![base_path.join("static"), base_path.join("content"), base_path.join(output_path)];
if let Some(t) = theme { if let Some(t) = theme {
search_paths.push(base_path.join("themes").join(t).join("static")); search_paths.push(base_path.join("themes").join(t).join("static"));
} }
@ -37,7 +39,7 @@ pub fn search_for_file(
} }
if !file_exists { if !file_exists {
// we need to search in both search folders now // we need to search in all search folders now
for dir in &search_paths { for dir in &search_paths {
let p = dir.join(&*actual_path); let p = dir.join(&*actual_path);
if p.exists() { if p.exists() {

View file

@ -12,6 +12,7 @@ pub struct ResizeImage {
base_path: PathBuf, base_path: PathBuf,
theme: Option<String>, theme: Option<String>,
imageproc: Arc<Mutex<imageproc::Processor>>, imageproc: Arc<Mutex<imageproc::Processor>>,
output_path: PathBuf,
} }
impl ResizeImage { impl ResizeImage {
@ -19,8 +20,9 @@ impl ResizeImage {
base_path: PathBuf, base_path: PathBuf,
imageproc: Arc<Mutex<imageproc::Processor>>, imageproc: Arc<Mutex<imageproc::Processor>>,
theme: Option<String>, theme: Option<String>,
output_path: PathBuf,
) -> Self { ) -> Self {
Self { base_path, imageproc, theme } Self { base_path, imageproc, theme, output_path }
} }
} }
@ -60,7 +62,7 @@ impl TeraFn for ResizeImage {
} }
let mut imageproc = self.imageproc.lock().unwrap(); let mut imageproc = self.imageproc.lock().unwrap();
let (file_path, unified_path) = match search_for_file(&self.base_path, &path, &self.theme) let (file_path, unified_path) = match search_for_file(&self.base_path, &path, &self.theme, &self.output_path)
.map_err(|e| format!("`resize_image`: {}", e))? .map_err(|e| format!("`resize_image`: {}", e))?
{ {
Some(f) => f, Some(f) => f,
@ -83,11 +85,12 @@ pub struct GetImageMetadata {
base_path: PathBuf, base_path: PathBuf,
theme: Option<String>, theme: Option<String>,
result_cache: Arc<Mutex<HashMap<String, Value>>>, result_cache: Arc<Mutex<HashMap<String, Value>>>,
output_path: PathBuf,
} }
impl GetImageMetadata { impl GetImageMetadata {
pub fn new(base_path: PathBuf, theme: Option<String>) -> Self { pub fn new(base_path: PathBuf, theme: Option<String>, output_path: PathBuf) -> Self {
Self { base_path, result_cache: Arc::new(Mutex::new(HashMap::new())), theme } Self { base_path, result_cache: Arc::new(Mutex::new(HashMap::new())), theme, output_path }
} }
} }
@ -105,7 +108,7 @@ impl TeraFn for GetImageMetadata {
) )
.unwrap_or(false); .unwrap_or(false);
let (src_path, unified_path) = match search_for_file(&self.base_path, &path, &self.theme) let (src_path, unified_path) = match search_for_file(&self.base_path, &path, &self.theme, &self.output_path)
.map_err(|e| format!("`get_image_metadata`: {}", e))? .map_err(|e| format!("`get_image_metadata`: {}", e))?
{ {
Some((f, p)) => (f, p), Some((f, p)) => (f, p),
@ -139,7 +142,7 @@ mod tests {
use std::fs::{copy, create_dir_all}; use std::fs::{copy, create_dir_all};
use config::Config; use config::Config;
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use tera::{to_value, Function}; use tera::{to_value, Function};
@ -172,6 +175,7 @@ mod tests {
dir.path().to_path_buf(), dir.path().to_path_buf(),
Arc::new(Mutex::new(imageproc)), Arc::new(Mutex::new(imageproc)),
Some("name".to_owned()), Some("name".to_owned()),
PathBuf::new(),
); );
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("height".to_string(), to_value(40).unwrap()); args.insert("height".to_string(), to_value(40).unwrap());
@ -246,7 +250,7 @@ mod tests {
fn can_get_image_metadata() { fn can_get_image_metadata() {
let dir = create_dir_with_image(); let dir = create_dir_with_image();
let static_fn = GetImageMetadata::new(dir.path().to_path_buf(), None); let static_fn = GetImageMetadata::new(dir.path().to_path_buf(), None, PathBuf::new());
// Let's test a few scenarii // Let's test a few scenarii
let mut args = HashMap::new(); let mut args = HashMap::new();

View file

@ -89,13 +89,14 @@ impl DataSource {
url_arg: Option<String>, url_arg: Option<String>,
base_path: &Path, base_path: &Path,
theme: &Option<String>, theme: &Option<String>,
output_path: &Path,
) -> Result<Option<Self>> { ) -> Result<Option<Self>> {
if path_arg.is_some() && url_arg.is_some() { if path_arg.is_some() && url_arg.is_some() {
return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into()); return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into());
} }
if let Some(path) = path_arg { if let Some(path) = path_arg {
return match search_for_file(base_path, &path, theme) return match search_for_file(&base_path, &path, &theme, &output_path)
.map_err(|e| format!("`load_data`: {}", e))? .map_err(|e| format!("`load_data`: {}", e))?
{ {
Some((f, _)) => Ok(Some(DataSource::Path(f))), Some((f, _)) => Ok(Some(DataSource::Path(f))),
@ -169,9 +170,10 @@ pub struct LoadData {
theme: Option<String>, theme: Option<String>,
client: Arc<Mutex<Client>>, client: Arc<Mutex<Client>>,
result_cache: Arc<Mutex<HashMap<u64, Value>>>, result_cache: Arc<Mutex<HashMap<u64, Value>>>,
output_path: PathBuf,
} }
impl LoadData { impl LoadData {
pub fn new(base_path: PathBuf, theme: Option<String>) -> Self { pub fn new(base_path: PathBuf, theme: Option<String>, output_path: PathBuf) -> Self {
let client = Arc::new(Mutex::new( let client = Arc::new(Mutex::new(
Client::builder() Client::builder()
.user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))) .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
@ -179,7 +181,7 @@ impl LoadData {
.expect("reqwest client build"), .expect("reqwest client build"),
)); ));
let result_cache = Arc::new(Mutex::new(HashMap::new())); let result_cache = Arc::new(Mutex::new(HashMap::new()));
Self { base_path, client, result_cache, theme } Self { base_path, client, result_cache, theme, output_path }
} }
} }
@ -224,7 +226,7 @@ impl TeraFn for LoadData {
// If the file doesn't exist, source is None // If the file doesn't exist, source is None
let data_source = match ( let data_source = match (
DataSource::from_args(path_arg.clone(), url_arg, &self.base_path, &self.theme), DataSource::from_args(path_arg.clone(), url_arg, &self.base_path, &self.theme, &self.output_path),
required, required,
) { ) {
// If the file was not required, return a Null value to the template // If the file was not required, return a Null value to the template
@ -478,7 +480,7 @@ mod tests {
#[test] #[test]
fn fails_illegal_method_parameter() { fn fails_illegal_method_parameter() {
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value("https://example.com").unwrap()); args.insert("url".to_string(), to_value("https://example.com").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -505,7 +507,7 @@ mod tests {
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y"); let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y");
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(url).unwrap()); args.insert("url".to_string(), to_value(url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -533,7 +535,7 @@ mod tests {
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4yw"); let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4yw");
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(url).unwrap()); args.insert("url".to_string(), to_value(url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -562,7 +564,7 @@ mod tests {
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y"); let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y");
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(url).unwrap()); args.insert("url".to_string(), to_value(url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -578,7 +580,7 @@ mod tests {
#[test] #[test]
fn fails_when_missing_file() { fn fails_when_missing_file() {
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap()); args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap());
let result = static_fn.call(&args); let result = static_fn.call(&args);
@ -588,7 +590,7 @@ mod tests {
#[test] #[test]
fn doesnt_fail_when_missing_file_is_not_required() { fn doesnt_fail_when_missing_file_is_not_required() {
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap()); args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap());
args.insert("required".to_string(), to_value(false).unwrap()); args.insert("required".to_string(), to_value(false).unwrap());
@ -607,7 +609,7 @@ mod tests {
.unwrap(); .unwrap();
copy(get_test_file("test.css"), dir.path().join("static").join("test.css")).unwrap(); copy(get_test_file("test.css"), dir.path().join("static").join("test.css")).unwrap();
let static_fn = LoadData::new(dir.path().to_path_buf(), None); let static_fn = LoadData::new(dir.path().to_path_buf(), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
let val = if cfg!(windows) { ".hello {}\r\n" } else { ".hello {}\n" }; let val = if cfg!(windows) { ".hello {}\r\n" } else { ".hello {}\n" };
@ -634,7 +636,7 @@ mod tests {
#[test] #[test]
fn cannot_load_outside_base_dir() { fn cannot_load_outside_base_dir() {
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../README.md").unwrap()); args.insert("path".to_string(), to_value("../../README.md").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -711,7 +713,7 @@ mod tests {
.create(); .create();
let url = format!("{}{}", mockito::server_url(), "/zpydpkjj67"); let url = format!("{}{}", mockito::server_url(), "/zpydpkjj67");
let static_fn = LoadData::new(PathBuf::new(), None); let static_fn = LoadData::new(PathBuf::new(), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap()); args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap()); args.insert("format".to_string(), to_value("json").unwrap());
@ -728,7 +730,7 @@ mod tests {
.create(); .create();
let url = format!("{}{}", mockito::server_url(), "/aazeow0kog"); let url = format!("{}{}", mockito::server_url(), "/aazeow0kog");
let static_fn = LoadData::new(PathBuf::new(), None); let static_fn = LoadData::new(PathBuf::new(), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap()); args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap()); args.insert("format".to_string(), to_value("json").unwrap());
@ -749,7 +751,7 @@ mod tests {
.create(); .create();
let url = format!("{}{}", mockito::server_url(), "/aazeow0kog"); let url = format!("{}{}", mockito::server_url(), "/aazeow0kog");
let static_fn = LoadData::new(PathBuf::new(), None); let static_fn = LoadData::new(PathBuf::new(), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap()); args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap()); args.insert("format".to_string(), to_value("json").unwrap());
@ -776,7 +778,7 @@ mod tests {
.create(); .create();
let url = format!("{}{}", mockito::server_url(), "/chu8aizahBiy"); let url = format!("{}{}", mockito::server_url(), "/chu8aizahBiy");
let static_fn = LoadData::new(PathBuf::new(), None); let static_fn = LoadData::new(PathBuf::new(), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap()); args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap()); args.insert("format".to_string(), to_value("json").unwrap());
@ -786,7 +788,7 @@ mod tests {
#[test] #[test]
fn can_load_toml() { fn can_load_toml() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.toml").unwrap()); args.insert("path".to_string(), to_value("test.toml").unwrap());
let result = static_fn.call(&args.clone()).unwrap(); let result = static_fn.call(&args.clone()).unwrap();
@ -806,7 +808,7 @@ mod tests {
#[test] #[test]
fn unknown_extension_defaults_to_plain() { fn unknown_extension_defaults_to_plain() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.css").unwrap()); args.insert("path".to_string(), to_value("test.css").unwrap());
let result = static_fn.call(&args.clone()).unwrap(); let result = static_fn.call(&args.clone()).unwrap();
@ -821,7 +823,7 @@ mod tests {
#[test] #[test]
fn can_override_known_extension_with_format() { fn can_override_known_extension_with_format() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.csv").unwrap()); args.insert("path".to_string(), to_value("test.csv").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -839,7 +841,7 @@ mod tests {
#[test] #[test]
fn will_use_format_on_unknown_extension() { fn will_use_format_on_unknown_extension() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.css").unwrap()); args.insert("path".to_string(), to_value("test.css").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -854,7 +856,7 @@ mod tests {
#[test] #[test]
fn can_load_csv() { fn can_load_csv() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.csv").unwrap()); args.insert("path".to_string(), to_value("test.csv").unwrap());
let result = static_fn.call(&args.clone()).unwrap(); let result = static_fn.call(&args.clone()).unwrap();
@ -874,7 +876,7 @@ mod tests {
// Test points to bad csv file with uneven row lengths // Test points to bad csv file with uneven row lengths
#[test] #[test]
fn bad_csv_should_result_in_error() { fn bad_csv_should_result_in_error() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("uneven_rows.csv").unwrap()); args.insert("path".to_string(), to_value("uneven_rows.csv").unwrap());
let result = static_fn.call(&args.clone()); let result = static_fn.call(&args.clone());
@ -894,7 +896,7 @@ mod tests {
#[test] #[test]
fn bad_csv_should_result_in_error_even_when_not_required() { fn bad_csv_should_result_in_error_even_when_not_required() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("uneven_rows.csv").unwrap()); args.insert("path".to_string(), to_value("uneven_rows.csv").unwrap());
args.insert("required".to_string(), to_value(false).unwrap()); args.insert("required".to_string(), to_value(false).unwrap());
@ -915,7 +917,7 @@ mod tests {
#[test] #[test]
fn can_load_json() { fn can_load_json() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None); let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.json").unwrap()); args.insert("path".to_string(), to_value("test.json").unwrap());
let result = static_fn.call(&args.clone()).unwrap(); let result = static_fn.call(&args.clone()).unwrap();
@ -941,7 +943,7 @@ mod tests {
.create(); .create();
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y3"); let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y3");
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap()); args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
@ -973,7 +975,7 @@ mod tests {
.create(); .create();
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y2"); let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y2");
let static_fn = LoadData::new(PathBuf::from("../utils"), None); let static_fn = LoadData::new(PathBuf::from("../utils"), None, PathBuf::new());
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap()); args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());

View file

@ -109,11 +109,12 @@ logic applies.
1. The base directory is the Zola root directory, where the `config.toml` is 1. The base directory is the Zola root directory, where the `config.toml` is
2. For the given path: if it starts with `@/`, replace that with `content/` instead and trim any leading `/` 2. For the given path: if it starts with `@/`, replace that with `content/` instead and trim any leading `/`
3. Search in the following 3 or 4 locations in this order, returning the first where the file exists: 3. Search in the following locations in this order, returning the first where the file exists:
a. $base_directory + $path 1. $base_directory + $path
b. $base_directory + "static/" + $path 2. $base_directory + "static/" + $path
c. $base_directory + "content/" + $path 3. $base_directory + "content/" + $path
d. $base_directory + "themes" + $theme + "static/" + $path only if using a theme 4. $base_directory + $output_path + $path
5. $base_directory + "themes" + $theme + "static/" + $path (only if using a theme)
In practice this means that `@/some/image.jpg`, `/content/some/image.jpg` and `content/some/image.jpg` will point to the In practice this means that `@/some/image.jpg`, `/content/some/image.jpg` and `content/some/image.jpg` will point to the
same thing. same thing.