From 9e0cac6d4d1623918f63e08848b9a7a219621289 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 13 May 2021 22:20:47 +0200 Subject: [PATCH] Cleanup of get_url and get_file_hash --- CHANGELOG.md | 4 +- components/site/src/tpls.rs | 12 +- components/templates/src/global_fns/files.rs | 435 ++++++++++++++++ .../templates/src/global_fns/helpers.rs | 36 ++ components/templates/src/global_fns/images.rs | 5 +- .../templates/src/global_fns/load_data.rs | 44 +- components/templates/src/global_fns/mod.rs | 473 +----------------- test_site/themes/sample/templates/index.html | 2 +- 8 files changed, 505 insertions(+), 506 deletions(-) create mode 100644 components/templates/src/global_fns/files.rs create mode 100644 components/templates/src/global_fns/helpers.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c035759..02a0f0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ ### Breaking - Newlines are now required after the closing `+++` of front-matter -- `resize_image` now returns a map: `{url, static_path}` instead of just the URL so you can follow up with other functions +- `resize_image` now returns a map: `{url, static_path}` instead of just the URL so you can follow up with other functions +- `get_file_hash` now has the `base64` option set to `true` by default (from `false`) since it's mainly used for integrity hashes which are base64 +- `get_url` does not automatically strip leading `/` from paths anymore - i18n rework: languages now have their sections in `config.toml` to set up all their options 1. taxonomies don't have a `lang` anymore in the config, you need to declare them in their respective language section 2. the `config` variable in templates has been changed and is now a stripped down language aware version of the previous `config` diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index 0c3ec9d8..5b66731b 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -16,9 +16,9 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> { site.tera.register_function( "get_url", global_fns::GetUrl::new( + site.base_path.clone(), site.config.clone(), site.permalinks.clone(), - vec![site.static_path.clone(), site.output_path.clone(), site.content_path.clone()], ), ); site.tera.register_function( @@ -39,14 +39,8 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> { site.config.slugify.taxonomies, ), ); - site.tera.register_function( - "get_file_hash", - global_fns::GetFileHash::new(vec![ - site.static_path.clone(), - site.output_path.clone(), - site.content_path.clone(), - ]), - ); + site.tera + .register_function("get_file_hash", global_fns::GetFileHash::new(site.base_path.clone())); Ok(()) } diff --git a/components/templates/src/global_fns/files.rs b/components/templates/src/global_fns/files.rs new file mode 100644 index 00000000..914883e4 --- /dev/null +++ b/components/templates/src/global_fns/files.rs @@ -0,0 +1,435 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::{fs, io, result}; + +use base64::encode as encode_b64; +use sha2::{digest, Sha256, Sha384, Sha512}; +use tera::{from_value, to_value, Function as TeraFn, Result, Value}; +use config::Config; +use utils::site::resolve_internal_link; +use crate::global_fns::helpers::search_for_file; + + +fn compute_file_hash( + mut file: fs::File, + as_base64: bool, +) -> result::Result +where + digest::Output: core::fmt::LowerHex, + D: std::io::Write, +{ + let mut hasher = D::new(); + io::copy(&mut file, &mut hasher)?; + println!("base64: {}", as_base64); + if as_base64 { + Ok(encode_b64(hasher.finalize())) + } else { + Ok(format!("{:x}", hasher.finalize())) + } +} + +#[derive(Debug)] +pub struct GetUrl { + base_path: PathBuf, + config: Config, + permalinks: HashMap, +} + +impl GetUrl { + pub fn new(base_path: PathBuf, config: Config, permalinks: HashMap) -> Self { + Self { base_path, config, permalinks } + } +} + +fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result { + if lang == config.default_language { + return Ok(path); + } + + if !config.other_languages().contains_key(lang) { + return Err( + format!("`{}` is not an authorized language (check config.languages).", lang).into() + ); + } + + let mut split_path: Vec = path.split('.').map(String::from).collect(); + let ilast = split_path.len() - 1; + split_path[ilast] = format!("{}.{}", lang, split_path[ilast]); + Ok(split_path.join(".")) +} + +impl TeraFn for GetUrl { + fn call(&self, args: &HashMap) -> Result { + let path = required_arg!( + String, + args.get("path"), + "`get_url` requires a `path` argument with a string value" + ); + let cachebust = optional_arg!( + bool, + args.get("cachebust"), + "`get_url`: `cachebust` must be a boolean (true or false)" + ) + .unwrap_or(false); + let trailing_slash = optional_arg!( + bool, + args.get("trailing_slash"), + "`get_url`: `trailing_slash` must be a boolean (true or false)" + ) + .unwrap_or(false); + let lang = optional_arg!(String, args.get("lang"), "`get_url`: `lang` must be a string.") + .unwrap_or_else(|| self.config.default_language.clone()); + + // TODO: handle rss files with langs + // https://zola.discourse.group/t/rss-and-languages-do-not-work/878 + // TODO: clean up everything + // if it starts with @/, resolve it as an internal link + if path.starts_with("@/") { + let path_with_lang = match make_path_with_lang(path, &lang, &self.config) { + Ok(x) => x, + Err(e) => return Err(e), + }; + + match resolve_internal_link(&path_with_lang, &self.permalinks) { + Ok(resolved) => Ok(to_value(resolved.permalink).unwrap()), + Err(_) => { + Err(format!("Could not resolve URL for link `{}` not found.", path_with_lang) + .into()) + } + } + } else { + // anything else + let mut segments = vec![]; + + if lang != self.config.default_language { + segments.push(lang); + }; + + segments.push(path); + + let path_with_lang = segments.join("/"); + + let mut permalink = self.config.make_permalink(&path_with_lang); + if !trailing_slash && permalink.ends_with('/') { + permalink.pop(); // Removes the slash + } + + if cachebust { + match search_for_file(&self.base_path, &path_with_lang) + .and_then(|p| fs::File::open(&p).ok()) + .and_then(|f| compute_file_hash::(f, false).ok()) + { + Some(hash) => { + permalink = format!("{}?h={}", permalink, hash); + } + None => { + return Err(format!("Could not find or open file {}", path_with_lang).into()) + } + }; + } + + Ok(to_value(permalink).unwrap()) + } + } +} + +#[derive(Debug)] +pub struct GetFileHash { + base_path: PathBuf, +} +impl GetFileHash { + pub fn new(base_path: PathBuf) -> Self { + Self { base_path } + } +} + +impl TeraFn for GetFileHash { + fn call(&self, args: &HashMap) -> Result { + let path = required_arg!( + String, + args.get("path"), + "`get_file_hash` requires a `path` argument with a string value" + ); + let sha_type = optional_arg!( + u16, + args.get("sha_type"), + "`get_file_hash`: `sha_type` must be 256, 384 or 512" + ) + .unwrap_or(384); + let base64 = optional_arg!( + bool, + args.get("base64"), + "`get_file_hash`: `base64` must be true or false" + ) + .unwrap_or(true); + + let file_path = match search_for_file(&self.base_path, &path) { + Some(f) => f, + None => { + return Err(format!("`get_file_hash`: Cannot find file: {}", path).into()); + } + }; + + let f = match std::fs::File::open(file_path) { + Ok(f) => f, + Err(e) => { + return Err(format!("File {} could not be open: {}", path, e).into()); + } + }; + + let hash = match sha_type { + 256 => compute_file_hash::(f, base64), + 384 => compute_file_hash::(f, base64), + 512 => compute_file_hash::(f, base64), + _ => return Err("`get_file_hash`: Invalid sha value".into()), + }; + + match hash { + Ok(digest) => Ok(to_value(digest).unwrap()), + Err(_) => Err("`get_file_hash`: could no compute hash".into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::{GetFileHash, GetUrl}; + + use std::collections::HashMap; + + use tempfile::{tempdir, TempDir}; + use tera::{to_value, Function}; + + use config::Config; + use utils::fs::create_file; + + fn create_temp_dir() -> TempDir { + let dir = tempdir().unwrap(); + create_file(&dir.path().join("app.css"), "// Hello world!").expect("Failed to create file"); + dir + } + + const CONFIG_DATA: &str = r#" +base_url = "https://remplace-par-ton-url.fr" +default_language = "fr" + +[translations] +title = "Un titre" + +[languages.en] +[languages.en.translations] +title = "A title" +"#; + + #[test] + fn can_add_cachebust_to_url() { + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); + 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?h=572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840"); + } + + #[test] + fn can_add_trailing_slashes() { + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); + 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()); + assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/"); + } + + #[test] + fn can_add_slashes_and_cachebust() { + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); + 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/?h=572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840"); + } + + #[test] + fn can_link_to_some_static_file() { + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new()); + 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"); + } + + #[test] + fn error_when_language_not_available() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), config, HashMap::new()); + 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()); + let err = static_fn.call(&args).unwrap_err(); + assert_eq!( + "`it` is not an authorized language (check config.languages).", + format!("{}", err) + ); + } + + #[test] + fn can_get_url_with_default_language() { + let mut permalinks = HashMap::new(); + permalinks.insert( + "a_section/a_page.md".to_string(), + "https://remplace-par-ton-url.fr/a_section/a_page/".to_string(), + ); + permalinks.insert( + "a_section/a_page.en.md".to_string(), + "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), + ); + let config = Config::parse(CONFIG_DATA).unwrap(); + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks); + 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()); + assert_eq!( + static_fn.call(&args).unwrap(), + "https://remplace-par-ton-url.fr/a_section/a_page/" + ); + } + + #[test] + fn can_get_url_with_other_language() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let mut permalinks = HashMap::new(); + permalinks.insert( + "a_section/a_page.md".to_string(), + "https://remplace-par-ton-url.fr/a_section/a_page/".to_string(), + ); + permalinks.insert( + "a_section/a_page.en.md".to_string(), + "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), + ); + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks); + 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()); + assert_eq!( + static_fn.call(&args).unwrap(), + "https://remplace-par-ton-url.fr/en/a_section/a_page/" + ); + } + + #[test] + fn can_get_feed_url_with_default_language() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); + args.insert("lang".to_string(), to_value("fr").unwrap()); + assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/atom.xml"); + } + + #[test] + fn can_get_feed_url_with_other_language() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let dir = create_temp_dir(); + let static_fn = GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); + args.insert("lang".to_string(), to_value("en").unwrap()); + assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/en/atom.xml"); + } + + #[test] + fn can_get_file_hash_sha256_no_base64() { + let dir = create_temp_dir(); + let static_fn = GetFileHash::new(dir.into_path()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(256).unwrap()); + args.insert("base64".to_string(), to_value(false).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840" + ); + } + + #[test] + fn can_get_file_hash_sha256_base64() { + let dir = create_temp_dir(); + let static_fn = GetFileHash::new(dir.into_path()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(256).unwrap()); + args.insert("base64".to_string(), to_value(true).unwrap()); + assert_eq!(static_fn.call(&args).unwrap(), "Vy5pHcaMP81lOuRjJhvbOPNdxvAXFdnOaHmTGd0ViEA="); + } + + #[test] + fn can_get_file_hash_sha384_no_base64() { + let dir = create_temp_dir(); + let static_fn = GetFileHash::new(dir.into_path()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("base64".to_string(), to_value(false).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "141c09bd28899773b772bbe064d8b718fa1d6f2852b7eafd5ed6689d26b74883b79e2e814cd69d5b52ab476aa284c414" + ); + } + + #[test] + fn can_get_file_hash_sha384() { + let dir = create_temp_dir(); + let static_fn = GetFileHash::new(dir.into_path()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "FBwJvSiJl3O3crvgZNi3GPodbyhSt+r9XtZonSa3SIO3ni6BTNadW1KrR2qihMQU" + ); + } + + #[test] + fn can_get_file_hash_sha512_no_base64() { + let dir = create_temp_dir(); + let static_fn = GetFileHash::new(dir.into_path()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(512).unwrap()); + args.insert("base64".to_string(), to_value(false).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "379dfab35123b9159d9e4e92dc90e2be44cf3c2f7f09b2e2df80a1b219b461de3556c93e1a9ceb3008e999e2d6a54b4f1d65ee9be9be63fa45ec88931623372f" + ); + } + + #[test] + fn can_get_file_hash_sha512() { + let dir = create_temp_dir(); + let static_fn = GetFileHash::new(dir.into_path()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(512).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "N536s1EjuRWdnk6S3JDivkTPPC9/CbLi34Chshm0Yd41Vsk+GpzrMAjpmeLWpUtPHWXum+m+Y/pF7IiTFiM3Lw==" + ); + } + + #[test] + fn error_when_file_not_found_for_hash() { + let dir = create_temp_dir(); + let static_fn = GetFileHash::new(dir.into_path()); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("doesnt-exist").unwrap()); + let err = format!("{}", static_fn.call(&args).unwrap_err()); + println!("{:?}", err); + + assert!(err.contains("Cannot find file")); + } +} diff --git a/components/templates/src/global_fns/helpers.rs b/components/templates/src/global_fns/helpers.rs new file mode 100644 index 00000000..4ea5c26f --- /dev/null +++ b/components/templates/src/global_fns/helpers.rs @@ -0,0 +1,36 @@ +use std::borrow::Cow; +use std::path::{Path, PathBuf}; + +/// 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: +/// 1. base_path + path +/// 2. base_path + static + path +/// 3. base_path + content + path +pub fn search_for_file(base_path: &Path, path: &str) -> Option { + let search_paths = [base_path.join("static"), base_path.join("content")]; + let actual_path = if path.starts_with("@/") { + Cow::Owned(path.replace("@/", "content/")) + } else { + Cow::Borrowed(path) + }; + let mut file_path = base_path.join(&*actual_path); + let mut file_exists = file_path.exists(); + + if !file_exists { + // we need to search in both search folders now + for dir in &search_paths { + let p = dir.join(&*actual_path); + if p.exists() { + file_path = p; + file_exists = true; + break; + } + } + } + + if file_exists { + Some(file_path) + } else { + None + } +} diff --git a/components/templates/src/global_fns/images.rs b/components/templates/src/global_fns/images.rs index ee46a7c7..1058c568 100644 --- a/components/templates/src/global_fns/images.rs +++ b/components/templates/src/global_fns/images.rs @@ -3,12 +3,13 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use crate::global_fns::search_for_file; use image::GenericImageView; use serde_derive::{Deserialize, Serialize}; use svg_metadata as svg; use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value}; +use crate::global_fns::helpers::search_for_file; + #[derive(Debug, Serialize, Deserialize)] struct ResizeImageResponse { /// The final URL for that asset @@ -69,7 +70,7 @@ impl TeraFn for ResizeImage { let file_path = match search_for_file(&self.base_path, &path) { Some(f) => f, None => { - return Err(format!("`resize_image`: Cannot find path: {}", path).into()); + return Err(format!("`resize_image`: Cannot find file: {}", path).into()); } }; diff --git a/components/templates/src/global_fns/load_data.rs b/components/templates/src/global_fns/load_data.rs index 6d003fc0..55695fac 100644 --- a/components/templates/src/global_fns/load_data.rs +++ b/components/templates/src/global_fns/load_data.rs @@ -1,20 +1,19 @@ +use std::collections::hash_map::DefaultHasher; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +use csv::Reader; +use reqwest::header::{HeaderValue, CONTENT_TYPE}; +use reqwest::{blocking::Client, header}; +use tera::{from_value, to_value, Error, Function as TeraFn, Map, Result, Value}; +use url::Url; use utils::de::fix_toml_dates; use utils::fs::{get_file_time, is_path_in_directory, read_file}; -use reqwest::header::{HeaderValue, CONTENT_TYPE}; -use reqwest::{blocking::Client, header}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use std::str::FromStr; -use url::Url; - -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; - -use crate::global_fns::search_for_file; -use csv::Reader; -use std::collections::HashMap; -use tera::{from_value, to_value, Error, Function as TeraFn, Map, Result, Value}; +use crate::global_fns::helpers::search_for_file; static GET_DATA_ARGUMENT_ERROR_MESSAGE: &str = "`load_data`: requires EITHER a `path` or `url` argument"; @@ -73,7 +72,7 @@ impl OutputFormat { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug)] enum DataSource { Url(Url), Path(PathBuf), @@ -88,7 +87,7 @@ impl DataSource { fn from_args( path_arg: Option, url_arg: Option, - base_path: &PathBuf, + base_path: &Path, ) -> Result> { if path_arg.is_some() && url_arg.is_some() { return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into()); @@ -104,7 +103,7 @@ impl DataSource { if let Some(url) = url_arg { return Url::parse(&url) .map(DataSource::Url) - .map(|x| Some(x)) + .map(Some) .map_err(|e| format!("Failed to parse {} as url: {}", url, e).into()); } @@ -140,7 +139,7 @@ impl Hash for DataSource { } } -fn read_local_data_file(base_path: &PathBuf, full_path: PathBuf) -> Result { +fn read_local_data_file(base_path: &Path, full_path: PathBuf) -> Result { if !is_path_in_directory(&base_path, &full_path) .map_err(|e| format!("Failed to read data file {}: {}", full_path.display(), e))? { @@ -274,8 +273,8 @@ impl TeraFn for LoadData { let mut resp = response_client .post(url.as_str()) .header(header::ACCEPT, file_format.as_accept_header()); - match post_content_type { - Some(content_type) => match HeaderValue::from_str(&content_type) { + if let Some(content_type) = post_content_type { + match HeaderValue::from_str(&content_type) { Ok(c) => { resp = resp.header(CONTENT_TYPE, c); } @@ -286,9 +285,8 @@ impl TeraFn for LoadData { ) .into()); } - }, - _ => {} - }; + } + } if let Some(body) = post_body_arg { resp = resp.body(body); } diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index e7514fd4..e817ba93 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -1,482 +1,15 @@ -use std::borrow::Cow; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::{fs, io, result}; - -use base64::encode as encode_b64; -use sha2::{digest, Sha256, Sha384, Sha512}; -use tera::{from_value, to_value, Function as TeraFn, Result, Value}; - -use config::Config; -use utils::site::resolve_internal_link; - #[macro_use] mod macros; mod content; +mod files; +mod helpers; mod i18n; mod images; mod load_data; pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyUrl}; +pub use self::files::{GetFileHash, GetUrl}; pub use self::i18n::Trans; pub use self::images::{GetImageMetadata, ResizeImage}; pub use self::load_data::LoadData; - -/// 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: -/// 1. base_path + path -/// 2. base_path + static + path -/// 3. base_path + content + path -pub fn search_for_file(base_path: &Path, path: &str) -> Option { - let search_paths = [base_path.join("static"), base_path.join("content")]; - let actual_path = if path.starts_with("@/") { - Cow::Owned(path.replace("@/", "content/")) - } else { - Cow::Borrowed(path) - }; - let mut file_path = base_path.join(&*actual_path); - let mut file_exists = file_path.exists(); - - if !file_exists { - // we need to search in both search folders now - for dir in &search_paths { - let p = dir.join(&*actual_path); - if p.exists() { - file_path = p; - file_exists = true; - break; - } - } - } - - if file_exists { - Some(file_path) - } else { - None - } -} - -#[derive(Debug)] -pub struct GetUrl { - config: Config, - permalinks: HashMap, - search_paths: Vec, -} -impl GetUrl { - pub fn new( - config: Config, - permalinks: HashMap, - search_paths: Vec, - ) -> Self { - Self { config, permalinks, search_paths } - } -} - -fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result { - if lang == config.default_language { - return Ok(path); - } - - if !config.other_languages().contains_key(lang) { - return Err( - format!("`{}` is not an authorized language (check config.languages).", lang).into() - ); - } - - let mut splitted_path: Vec = path.split('.').map(String::from).collect(); - let ilast = splitted_path.len() - 1; - splitted_path[ilast] = format!("{}.{}", lang, splitted_path[ilast]); - Ok(splitted_path.join(".")) -} - -fn open_file(search_paths: &[PathBuf], url: &str) -> result::Result { - let cleaned_url = url.trim_start_matches("@/").trim_start_matches('/'); - for base_path in search_paths { - match fs::File::open(base_path.join(cleaned_url)) { - Ok(f) => return Ok(f), - Err(_) => continue, - }; - } - Err(io::Error::from(io::ErrorKind::NotFound)) -} - -fn compute_file_hash( - mut file: fs::File, - base64: bool, -) -> result::Result -where - digest::Output: core::fmt::LowerHex, - D: std::io::Write, -{ - let mut hasher = D::new(); - io::copy(&mut file, &mut hasher)?; - if base64 { - Ok(encode_b64(hasher.finalize())) - } else { - Ok(format!("{:x}", hasher.finalize())) - } -} - -fn file_not_found_err(search_paths: &[PathBuf], url: &str) -> Result { - Err(format!( - "file `{}` not found; searched in{}", - url, - search_paths.iter().fold(String::new(), |acc, arg| acc + " " + arg.to_str().unwrap()) - ) - .into()) -} - -impl TeraFn for GetUrl { - fn call(&self, args: &HashMap) -> Result { - let cachebust = - args.get("cachebust").map_or(false, |c| from_value::(c.clone()).unwrap_or(false)); - - let trailing_slash = args - .get("trailing_slash") - .map_or(false, |c| from_value::(c.clone()).unwrap_or(false)); - - let path = required_arg!( - String, - args.get("path"), - "`get_url` requires a `path` argument with a string value" - ); - - let lang = optional_arg!(String, args.get("lang"), "`get_url`: `lang` must be a string.") - .unwrap_or_else(|| self.config.default_language.clone()); - - if path.starts_with("@/") { - let path_with_lang = match make_path_with_lang(path, &lang, &self.config) { - Ok(x) => x, - Err(e) => return Err(e), - }; - - match resolve_internal_link(&path_with_lang, &self.permalinks) { - Ok(resolved) => Ok(to_value(resolved.permalink).unwrap()), - Err(_) => { - Err(format!("Could not resolve URL for link `{}` not found.", path_with_lang) - .into()) - } - } - } else { - // anything else - let mut segments = vec![]; - - if lang != self.config.default_language { - segments.push(lang); - }; - - segments.push(path); - - let path_with_lang = segments.join("/"); - - let mut permalink = self.config.make_permalink(&path_with_lang); - if !trailing_slash && permalink.ends_with('/') { - permalink.pop(); // Removes the slash - } - - if cachebust { - match open_file(&self.search_paths, &path_with_lang) - .and_then(|f| compute_file_hash::(f, false)) - { - Ok(hash) => { - permalink = format!("{}?h={}", permalink, hash); - } - Err(_) => return file_not_found_err(&self.search_paths, &path_with_lang), - }; - } - - Ok(to_value(permalink).unwrap()) - } - } -} - -#[derive(Debug)] -pub struct GetFileHash { - search_paths: Vec, -} -impl GetFileHash { - pub fn new(search_paths: Vec) -> Self { - Self { search_paths } - } -} - -const DEFAULT_SHA_TYPE: u16 = 384; -const DEFAULT_BASE64: bool = false; - -impl TeraFn for GetFileHash { - fn call(&self, args: &HashMap) -> Result { - let path = required_arg!( - String, - args.get("path"), - "`get_file_hash` requires a `path` argument with a string value" - ); - let sha_type = optional_arg!( - u16, - args.get("sha_type"), - "`get_file_hash`: `sha_type` must be 256, 384 or 512" - ) - .unwrap_or(DEFAULT_SHA_TYPE); - let base64 = optional_arg!( - bool, - args.get("base64"), - "`get_file_hash`: `base64` must be true or false" - ) - .unwrap_or(DEFAULT_BASE64); - - let f = match open_file(&self.search_paths, &path) { - Ok(f) => f, - Err(e) => { - return Err(format!( - "File {} could not be open: {} (searched in {:?})", - path, e, self.search_paths - ) - .into()); - } - }; - - let hash = match sha_type { - 256 => compute_file_hash::(f, base64), - 384 => compute_file_hash::(f, base64), - 512 => compute_file_hash::(f, base64), - _ => return Err("`get_file_hash`: Invalid sha value".into()), - }; - - match hash { - Ok(digest) => Ok(to_value(digest).unwrap()), - Err(_) => file_not_found_err(&self.search_paths, &path), - } - } -} - -#[cfg(test)] -mod tests { - use super::{GetFileHash, GetUrl}; - - use std::collections::HashMap; - - use tempfile::{tempdir, TempDir}; - use tera::{to_value, Function}; - - use config::Config; - use utils::fs::create_file; - - fn create_temp_dir() -> TempDir { - let dir = tempdir().unwrap(); - create_file(&dir.path().join("app.css"), "// Hello world!").expect("Failed to create file"); - dir - } - - const CONFIG_DATA: &str = r#" -base_url = "https://remplace-par-ton-url.fr" -default_language = "fr" - -[translations] -title = "Un titre" - -[languages.en] -[languages.en.translations] -title = "A title" -"#; - - #[test] - fn can_add_cachebust_to_url() { - let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), vec![create_temp_dir().into_path()]); - 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?h=572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840"); - } - - #[test] - fn can_add_trailing_slashes() { - let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), vec![create_temp_dir().into_path()]); - 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()); - assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/"); - } - - #[test] - fn can_add_slashes_and_cachebust() { - let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), vec![create_temp_dir().into_path()]); - 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/?h=572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840"); - } - - #[test] - fn can_link_to_some_static_file() { - let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), vec![create_temp_dir().into_path()]); - 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"); - } - - #[test] - fn error_when_language_not_available() { - let config = Config::parse(CONFIG_DATA).unwrap(); - let static_fn = GetUrl::new(config, HashMap::new(), vec![create_temp_dir().into_path()]); - 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()); - let err = static_fn.call(&args).unwrap_err(); - assert_eq!( - "`it` is not an authorized language (check config.languages).", - format!("{}", err) - ); - } - - #[test] - fn can_get_url_with_default_language() { - let config = Config::parse(CONFIG_DATA).unwrap(); - let mut permalinks = HashMap::new(); - permalinks.insert( - "a_section/a_page.md".to_string(), - "https://remplace-par-ton-url.fr/a_section/a_page/".to_string(), - ); - permalinks.insert( - "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, vec![create_temp_dir().into_path()]); - 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()); - assert_eq!( - static_fn.call(&args).unwrap(), - "https://remplace-par-ton-url.fr/a_section/a_page/" - ); - } - - #[test] - fn can_get_url_with_other_language() { - let config = Config::parse(CONFIG_DATA).unwrap(); - let mut permalinks = HashMap::new(); - permalinks.insert( - "a_section/a_page.md".to_string(), - "https://remplace-par-ton-url.fr/a_section/a_page/".to_string(), - ); - permalinks.insert( - "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, vec![create_temp_dir().into_path()]); - 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()); - assert_eq!( - static_fn.call(&args).unwrap(), - "https://remplace-par-ton-url.fr/en/a_section/a_page/" - ); - } - - #[test] - fn can_get_feed_url_with_default_language() { - let config = Config::parse(CONFIG_DATA).unwrap(); - let static_fn = - GetUrl::new(config.clone(), HashMap::new(), vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); - args.insert("lang".to_string(), to_value("fr").unwrap()); - assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/atom.xml"); - } - - #[test] - fn can_get_feed_url_with_other_language() { - let config = Config::parse(CONFIG_DATA).unwrap(); - let static_fn = - GetUrl::new(config.clone(), HashMap::new(), vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); - args.insert("lang".to_string(), to_value("en").unwrap()); - assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/en/atom.xml"); - } - - #[test] - fn can_get_file_hash_sha256() { - let static_fn = GetFileHash::new(vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value("app.css").unwrap()); - args.insert("sha_type".to_string(), to_value(256).unwrap()); - assert_eq!( - static_fn.call(&args).unwrap(), - "572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840" - ); - } - - #[test] - fn can_get_file_hash_sha256_base64() { - let static_fn = GetFileHash::new(vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value("app.css").unwrap()); - args.insert("sha_type".to_string(), to_value(256).unwrap()); - args.insert("base64".to_string(), to_value(true).unwrap()); - assert_eq!(static_fn.call(&args).unwrap(), "Vy5pHcaMP81lOuRjJhvbOPNdxvAXFdnOaHmTGd0ViEA="); - } - - #[test] - fn can_get_file_hash_sha384() { - let static_fn = GetFileHash::new(vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value("app.css").unwrap()); - assert_eq!( - static_fn.call(&args).unwrap(), - "141c09bd28899773b772bbe064d8b718fa1d6f2852b7eafd5ed6689d26b74883b79e2e814cd69d5b52ab476aa284c414" - ); - } - - #[test] - fn can_get_file_hash_sha384_base64() { - let static_fn = GetFileHash::new(vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value("app.css").unwrap()); - args.insert("base64".to_string(), to_value(true).unwrap()); - assert_eq!( - static_fn.call(&args).unwrap(), - "FBwJvSiJl3O3crvgZNi3GPodbyhSt+r9XtZonSa3SIO3ni6BTNadW1KrR2qihMQU" - ); - } - - #[test] - fn can_get_file_hash_sha512() { - let static_fn = GetFileHash::new(vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value("app.css").unwrap()); - args.insert("sha_type".to_string(), to_value(512).unwrap()); - assert_eq!( - static_fn.call(&args).unwrap(), - "379dfab35123b9159d9e4e92dc90e2be44cf3c2f7f09b2e2df80a1b219b461de3556c93e1a9ceb3008e999e2d6a54b4f1d65ee9be9be63fa45ec88931623372f" - ); - } - - #[test] - fn can_get_file_hash_sha512_base64() { - let static_fn = GetFileHash::new(vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value("app.css").unwrap()); - args.insert("sha_type".to_string(), to_value(512).unwrap()); - args.insert("base64".to_string(), to_value(true).unwrap()); - assert_eq!( - static_fn.call(&args).unwrap(), - "N536s1EjuRWdnk6S3JDivkTPPC9/CbLi34Chshm0Yd41Vsk+GpzrMAjpmeLWpUtPHWXum+m+Y/pF7IiTFiM3Lw==" - ); - } - - #[test] - fn error_when_file_not_found_for_hash() { - let static_fn = GetFileHash::new(vec![create_temp_dir().into_path()]); - let mut args = HashMap::new(); - args.insert("path".to_string(), to_value("doesnt-exist").unwrap()); - let err = format!("{}", static_fn.call(&args).unwrap_err()); - println!("{:?}", err); - - assert!(err.contains("File doesnt-exist could not be open")); - } -} diff --git a/test_site/themes/sample/templates/index.html b/test_site/themes/sample/templates/index.html index e0a8d28d..bd1dabfa 100644 --- a/test_site/themes/sample/templates/index.html +++ b/test_site/themes/sample/templates/index.html @@ -7,7 +7,7 @@ - + {{ config.title }}