diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index b41ccc27..40ea154f 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -78,6 +78,8 @@ pub struct Config { pub mode: Mode, pub output_dir: String, + /// Whether dotfiles inside the output directory are preserved when rebuilding the site + pub preserve_dotfiles_in_output: bool, pub link_checker: link_checker::LinkChecker, /// The setup for which slugification strategies to use for paths, taxonomies and anchors @@ -371,6 +373,7 @@ impl Default for Config { ignored_content_globset: None, translations: HashMap::new(), output_dir: "public".to_string(), + preserve_dotfiles_in_output: false, link_checker: link_checker::LinkChecker::default(), slugify: slugify::Slugify::default(), search: search::Search::default(), diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 2f0c8268..f25b6af8 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -6,7 +6,7 @@ pub mod sitemap; pub mod tpls; use std::collections::HashMap; -use std::fs::remove_dir_all; +use std::fs::{remove_dir_all, remove_file}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, RwLock}; @@ -23,6 +23,7 @@ use std::time::Instant; use templates::{load_tera, render_redirect_template}; use utils::fs::{ copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists, + is_dotfile, }; use utils::net::get_available_port; use utils::templates::{render_template, ShortcodeDefinition}; @@ -583,11 +584,34 @@ impl Site { imageproc.do_process() } - /// Deletes the `public` directory if it exists + /// Deletes the `public` directory if it exists and the `preserve_dotfiles_in_output` option is set to false, + /// or if set to true: its contents except for the dotfiles at the root level. pub fn clean(&self) -> Result<()> { if self.output_path.exists() { - // Delete current `public` directory so we can start fresh - remove_dir_all(&self.output_path).context("Couldn't delete output directory")?; + if !self.config.preserve_dotfiles_in_output { + return remove_dir_all(&self.output_path) + .context("Couldn't delete output directory"); + } + + for entry in self.output_path.read_dir().context(format!( + "Couldn't read output directory `{}`", + self.output_path.display() + ))? { + let entry = entry.context("Couldn't read entry in output directory")?.path(); + + // Skip dotfiles if the preserve_dotfiles_in_output configuration option is set + if is_dotfile(&entry) { + continue; + } + + if entry.is_dir() { + remove_dir_all(entry) + .context("Couldn't delete folder while cleaning the output directory")?; + } else { + remove_file(entry) + .context("Couldn't delete file while cleaning the output directory")?; + } + } } Ok(()) diff --git a/components/utils/src/fs.rs b/components/utils/src/fs.rs index c5567f07..9b927d42 100644 --- a/components/utils/src/fs.rs +++ b/components/utils/src/fs.rs @@ -166,6 +166,14 @@ where time_source.and_then(|ts| time_target.map(|tt| ts > tt)).unwrap_or(true) } +/// Checks if the file or folder for the given path is a dotfile, meaning starts with '.' +pub fn is_dotfile
(path: P) -> bool
+where
+ P: AsRef