diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index 2cda17212..06dd1a6b8 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -28,6 +28,18 @@ pub enum Mode { Check, } +fn build_ignore_glob_set(ignore: &Vec, name: &str) -> Result { + let mut glob_set_builder = GlobSetBuilder::new(); + for pat in ignore { + let glob = match Glob::new(pat) { + Ok(g) => g, + Err(e) => bail!("Invalid ignored_{} glob pattern: {}, error = {}", name, pat, e), + }; + glob_set_builder.add(glob); + } + Ok(glob_set_builder.build().expect(&format!("Bad ignored_{} in config file.", name))) +} + #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub struct Config { @@ -74,6 +86,11 @@ pub struct Config { #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed pub ignored_content_globset: Option, + /// A list of file glob patterns to ignore when processing the static folder. Defaults to none. + pub ignored_static: Vec, + #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed + pub ignored_static_globset: Option, + /// The mode Zola is currently being ran on. Some logging/feature can differ depending on the /// command being used. #[serde(skip_serializing)] @@ -140,16 +157,13 @@ impl Config { // globset matcher to always exist (even though it has to be an inside an Option at the // moment because of the TOML serializer); if the glob set is empty the `is_match` function // of the globber always returns false. - let mut glob_set_builder = GlobSetBuilder::new(); - for pat in &config.ignored_content { - let glob = match Glob::new(pat) { - Ok(g) => g, - Err(e) => bail!("Invalid ignored_content glob pattern: {}, error = {}", pat, e), - }; - glob_set_builder.add(glob); - } - config.ignored_content_globset = - Some(glob_set_builder.build().expect("Bad ignored_content in config file.")); + let glob_set = build_ignore_glob_set(&config.ignored_content, "content")?; + config.ignored_content_globset = Some(glob_set); + } + + if !config.ignored_static.is_empty() { + let glob_set = build_ignore_glob_set(&config.ignored_static, "static")?; + config.ignored_static_globset = Some(glob_set); } Ok(config) @@ -386,6 +400,8 @@ impl Default for Config { build_search_index: false, ignored_content: Vec::new(), ignored_content_globset: None, + ignored_static: Vec::new(), + ignored_static_globset: None, translations: HashMap::new(), output_dir: "public".to_string(), preserve_dotfiles_in_output: false, @@ -648,6 +664,18 @@ base_url = "example.com" assert!(config.ignored_content_globset.is_none()); } + #[test] + fn missing_ignored_static_results_in_empty_vector_and_empty_globset() { + let config_str = r#" +title = "My site" +base_url = "example.com" + "#; + let config = Config::parse(config_str).unwrap(); + let v = config.ignored_static; + assert_eq!(v.len(), 0); + assert!(config.ignored_static_globset.is_none()); + } + #[test] fn empty_ignored_content_results_in_empty_vector_and_empty_globset() { let config_str = r#" @@ -661,6 +689,19 @@ ignored_content = [] assert!(config.ignored_content_globset.is_none()); } + #[test] + fn empty_ignored_static_results_in_empty_vector_and_empty_globset() { + let config_str = r#" +title = "My site" +base_url = "example.com" +ignored_static = [] + "#; + + let config = Config::parse(config_str).unwrap(); + assert_eq!(config.ignored_static.len(), 0); + assert!(config.ignored_static_globset.is_none()); + } + #[test] fn non_empty_ignored_content_results_in_vector_of_patterns_and_configured_globset() { let config_str = r#" @@ -690,6 +731,35 @@ ignored_content = ["*.{graphml,iso}", "*.py?", "**/{target,temp_folder}"] assert!(g.is_match("content/poetry/zen.py2")); } + #[test] + fn non_empty_ignored_static_results_in_vector_of_patterns_and_configured_globset() { + let config_str = r#" +title = "My site" +base_url = "example.com" +ignored_static = ["*.{graphml,iso}", "*.py?", "**/{target,temp_folder}"] + "#; + + let config = Config::parse(config_str).unwrap(); + let v = config.ignored_static; + assert_eq!(v, vec!["*.{graphml,iso}", "*.py?", "**/{target,temp_folder}"]); + + let g = config.ignored_static_globset.unwrap(); + assert_eq!(g.len(), 3); + assert!(g.is_match("foo.graphml")); + assert!(g.is_match("foo/bar/foo.graphml")); + assert!(g.is_match("foo.iso")); + assert!(!g.is_match("foo.png")); + assert!(g.is_match("foo.py2")); + assert!(g.is_match("foo.py3")); + assert!(!g.is_match("foo.py")); + assert!(g.is_match("foo/bar/target")); + assert!(g.is_match("foo/bar/baz/temp_folder")); + assert!(g.is_match("foo/bar/baz/temp_folder/target")); + assert!(g.is_match("temp_folder")); + assert!(g.is_match("my/isos/foo.iso")); + assert!(g.is_match("content/poetry/zen.py2")); + } + #[test] fn link_checker_skip_anchor_prefixes() { let config_str = r#" diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 2d26b1545..91af54616 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -587,11 +587,26 @@ impl Site { &self.base_path.join("themes").join(theme).join("static"), &self.output_path, false, + None, )?; } // We're fine with missing static folders if self.static_path.exists() { - copy_directory(&self.static_path, &self.output_path, self.config.hard_link_static)?; + if let Some(gs) = &self.config.ignored_static_globset { + copy_directory( + &self.static_path, + &self.output_path, + self.config.hard_link_static, + Some(gs), + )?; + } else { + copy_directory( + &self.static_path, + &self.output_path, + self.config.hard_link_static, + None, + )?; + } } Ok(()) diff --git a/components/utils/src/fs.rs b/components/utils/src/fs.rs index c29edba68..f2e5093f8 100644 --- a/components/utils/src/fs.rs +++ b/components/utils/src/fs.rs @@ -1,4 +1,5 @@ use libs::filetime::{set_file_mtime, FileTime}; +use libs::globset::GlobSet; use libs::walkdir::WalkDir; use std::fs::{copy, create_dir_all, metadata, remove_dir_all, remove_file, File}; use std::io::prelude::*; @@ -115,11 +116,23 @@ pub fn copy_file_if_needed(src: &Path, dest: &Path, hard_link: bool) -> Result<( Ok(()) } -pub fn copy_directory(src: &Path, dest: &Path, hard_link: bool) -> Result<()> { +pub fn copy_directory( + src: &Path, + dest: &Path, + hard_link: bool, + ignore_globset: Option<&GlobSet>, +) -> Result<()> { for entry in WalkDir::new(src).follow_links(true).into_iter().filter_map(std::result::Result::ok) { let relative_path = entry.path().strip_prefix(src).unwrap(); + + if let Some(gs) = ignore_globset { + if gs.is_match(&relative_path) { + continue; + } + } + let target_path = dest.join(relative_path); if entry.path().is_dir() { diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index ac85b7242..273b3935c 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -59,6 +59,11 @@ minify_html = false # ignored_content = ["*.{graphml,xlsx}", "temp.*", "**/build_folder"] ignored_content = [] +# Similar to ignored_content, a list of glob patterns specifying asset files to +# ignore when the static directory is processed. Defaults to none, which means +# that all asset files are copied over to the `public` directory +ignored_static = [] + # When set to "true", a feed is automatically generated. generate_feed = false diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 52b1d585b..5b719775c 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -475,6 +475,12 @@ pub fn serve( }; let copy_static = |site: &Site, path: &Path, partial_path: &Path| { + // Do nothing if the file/dir is on the ignore list + if let Some(gs) = &site.config.ignored_static_globset { + if gs.is_match(partial_path) { + return; + } + } // Do nothing if the file/dir was deleted if !path.exists() { return;