More more things to fs_utils file
This commit is contained in:
parent
5b3a57b1ac
commit
7294c76f6b
239
src/cmd/serve.rs
239
src/cmd/serve.rs
@ -22,8 +22,6 @@
|
|||||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::read_dir;
|
|
||||||
use std::future::IntoFuture;
|
use std::future::IntoFuture;
|
||||||
use std::net::{IpAddr, SocketAddr, TcpListener};
|
use std::net::{IpAddr, SocketAddr, TcpListener};
|
||||||
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
|
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
|
||||||
@ -41,7 +39,6 @@ use mime_guess::from_path as mimetype_from_path;
|
|||||||
use time::macros::format_description;
|
use time::macros::format_description;
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
|
|
||||||
use libs::globset::GlobSet;
|
|
||||||
use libs::percent_encoding;
|
use libs::percent_encoding;
|
||||||
use libs::relative_path::{RelativePath, RelativePathBuf};
|
use libs::relative_path::{RelativePath, RelativePathBuf};
|
||||||
use libs::serde_json;
|
use libs::serde_json;
|
||||||
@ -52,23 +49,12 @@ use errors::{anyhow, Context, Error, Result};
|
|||||||
use pathdiff::diff_paths;
|
use pathdiff::diff_paths;
|
||||||
use site::sass::compile_sass;
|
use site::sass::compile_sass;
|
||||||
use site::{Site, SITE_CONTENT};
|
use site::{Site, SITE_CONTENT};
|
||||||
use utils::fs::{clean_site_output_folder, copy_file, is_temp_file};
|
use utils::fs::{clean_site_output_folder, copy_file};
|
||||||
|
|
||||||
use crate::fs_event_utils::{get_relevant_event_kind, SimpleFSEventKind};
|
use crate::fs_utils::{filter_events, ChangeKind, SimpleFileSystemEventKind};
|
||||||
use crate::messages;
|
use crate::messages;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq)]
|
|
||||||
enum ChangeKind {
|
|
||||||
Content,
|
|
||||||
Templates,
|
|
||||||
Themes,
|
|
||||||
StaticFiles,
|
|
||||||
Sass,
|
|
||||||
Config,
|
|
||||||
}
|
|
||||||
impl Eq for ChangeKind {}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum WatchMode {
|
enum WatchMode {
|
||||||
Required,
|
Required,
|
||||||
@ -206,7 +192,7 @@ async fn response_error_injector(
|
|||||||
.map(|req| {
|
.map(|req| {
|
||||||
req.headers()
|
req.headers()
|
||||||
.get(header::CONTENT_TYPE)
|
.get(header::CONTENT_TYPE)
|
||||||
.map(|val| val != &HeaderValue::from_static("text/html"))
|
.map(|val| val != HeaderValue::from_static("text/html"))
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
})
|
})
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
@ -401,7 +387,7 @@ fn create_new_site(
|
|||||||
|
|
||||||
let mut constructed_base_url = construct_url(&base_url, no_port_append, interface_port);
|
let mut constructed_base_url = construct_url(&base_url, no_port_append, interface_port);
|
||||||
|
|
||||||
if !site.config.base_url.ends_with("/") && constructed_base_url != "/" {
|
if !site.config.base_url.ends_with('/') && constructed_base_url != "/" {
|
||||||
constructed_base_url.truncate(constructed_base_url.len() - 1);
|
constructed_base_url.truncate(constructed_base_url.len() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,7 +534,7 @@ pub fn serve(
|
|||||||
&constructed_base_url, &bind_address
|
&constructed_base_url, &bind_address
|
||||||
);
|
);
|
||||||
if open {
|
if open {
|
||||||
if let Err(err) = open::that(format!("{}", &constructed_base_url)) {
|
if let Err(err) = open::that(&constructed_base_url) {
|
||||||
eprintln!("Failed to open URL in your browser: {}", err);
|
eprintln!("Failed to open URL in your browser: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -693,66 +679,16 @@ pub fn serve(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
match rx.recv() {
|
match rx.recv() {
|
||||||
Ok(Ok(mut events)) => {
|
Ok(Ok(events)) => {
|
||||||
// Arrange events from oldest to newest.
|
let changes = filter_events(
|
||||||
events.sort_by(|e1, e2| e1.time.cmp(&e2.time));
|
events,
|
||||||
|
root_dir,
|
||||||
// Use a map to keep only the last event that occurred for a particular path.
|
&config_path,
|
||||||
// Map `full_path -> (partial_path, simple_event_kind, change_kind)`.
|
&site.config.ignored_content_globset,
|
||||||
let mut meaningful_events: HashMap<
|
);
|
||||||
PathBuf,
|
if changes.is_empty() {
|
||||||
(PathBuf, SimpleFSEventKind, ChangeKind),
|
|
||||||
> = HashMap::new();
|
|
||||||
|
|
||||||
for event in events.iter() {
|
|
||||||
let simple_kind = get_relevant_event_kind(&event.event.kind);
|
|
||||||
if simple_kind.is_none() {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We currently only handle notify events that report a single path per event.
|
|
||||||
if event.event.paths.len() != 1 {
|
|
||||||
console::error(&format!(
|
|
||||||
"Skipping unsupported file system event with multiple paths: {:?}",
|
|
||||||
event.event.kind
|
|
||||||
));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let path = event.event.paths[0].clone();
|
|
||||||
|
|
||||||
if is_ignored_file(&site.config.ignored_content_globset, &path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_temp_file(&path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only care about changes in non-empty folders
|
|
||||||
if path.is_dir() && is_folder_empty(&path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (change_k, partial_p) = detect_change_kind(&root_dir, &path, &config_path);
|
|
||||||
meaningful_events.insert(path, (partial_p, simple_kind.unwrap(), change_k));
|
|
||||||
}
|
|
||||||
|
|
||||||
if meaningful_events.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bin changes by change kind to support later iteration over batches of changes.
|
|
||||||
// Map of change_kind -> (partial_path, full_path, event_kind).
|
|
||||||
let mut changes: HashMap<
|
|
||||||
ChangeKind,
|
|
||||||
Vec<(&PathBuf, &PathBuf, &SimpleFSEventKind)>,
|
|
||||||
> = HashMap::new();
|
|
||||||
for (full_path, (partial_path, event_kind, change_kind)) in meaningful_events.iter()
|
|
||||||
{
|
|
||||||
let c = changes.entry(*change_kind).or_insert(vec![]);
|
|
||||||
c.push((partial_path, full_path, event_kind));
|
|
||||||
}
|
|
||||||
|
|
||||||
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||||
|
|
||||||
for (change_kind, change_group) in changes.iter() {
|
for (change_kind, change_group) in changes.iter() {
|
||||||
@ -774,7 +710,8 @@ pub fn serve(
|
|||||||
full_path.display()
|
full_path.display()
|
||||||
));
|
));
|
||||||
|
|
||||||
let can_do_fast_reload = **event_kind != SimpleFSEventKind::Remove;
|
let can_do_fast_reload =
|
||||||
|
*event_kind != SimpleFileSystemEventKind::Remove;
|
||||||
|
|
||||||
if fast_rebuild {
|
if fast_rebuild {
|
||||||
if can_do_fast_reload {
|
if can_do_fast_reload {
|
||||||
@ -783,9 +720,9 @@ pub fn serve(
|
|||||||
.unwrap_or_else(|| OsStr::new(""))
|
.unwrap_or_else(|| OsStr::new(""))
|
||||||
.to_string_lossy();
|
.to_string_lossy();
|
||||||
let res = if filename == "_index.md" {
|
let res = if filename == "_index.md" {
|
||||||
site.add_and_render_section(&full_path)
|
site.add_and_render_section(full_path)
|
||||||
} else if filename.ends_with(".md") {
|
} else if filename.ends_with(".md") {
|
||||||
site.add_and_render_page(&full_path)
|
site.add_and_render_page(full_path)
|
||||||
} else {
|
} else {
|
||||||
// an asset changed? a folder renamed?
|
// an asset changed? a folder renamed?
|
||||||
// should we make it smarter so it doesn't reload the whole site?
|
// should we make it smarter so it doesn't reload the whole site?
|
||||||
@ -816,9 +753,9 @@ pub fn serve(
|
|||||||
}
|
}
|
||||||
ChangeKind::Templates => {
|
ChangeKind::Templates => {
|
||||||
let partial_paths: Vec<&PathBuf> =
|
let partial_paths: Vec<&PathBuf> =
|
||||||
change_group.iter().map(|(p, _, _)| *p).collect();
|
change_group.iter().map(|(p, _, _)| p).collect();
|
||||||
let full_paths: Vec<&PathBuf> =
|
let full_paths: Vec<&PathBuf> =
|
||||||
change_group.iter().map(|(_, p, _)| *p).collect();
|
change_group.iter().map(|(_, p, _)| p).collect();
|
||||||
let combined_paths = full_paths
|
let combined_paths = full_paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.display().to_string())
|
.map(|p| p.display().to_string())
|
||||||
@ -842,11 +779,11 @@ pub fn serve(
|
|||||||
}
|
}
|
||||||
ChangeKind::StaticFiles => {
|
ChangeKind::StaticFiles => {
|
||||||
for (partial_path, full_path, _) in change_group.iter() {
|
for (partial_path, full_path, _) in change_group.iter() {
|
||||||
copy_static(&site, &full_path, &partial_path);
|
copy_static(&site, full_path, partial_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChangeKind::Sass => {
|
ChangeKind::Sass => {
|
||||||
let full_paths = change_group.iter().map(|(_, p, _)| *p).collect();
|
let full_paths = change_group.iter().map(|(_, p, _)| p).collect();
|
||||||
reload_sass(&site, &full_paths);
|
reload_sass(&site, &full_paths);
|
||||||
}
|
}
|
||||||
ChangeKind::Themes => {
|
ChangeKind::Themes => {
|
||||||
@ -875,141 +812,9 @@ pub fn serve(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_ignored_file(ignored_content_globset: &Option<GlobSet>, path: &Path) -> bool {
|
|
||||||
match ignored_content_globset {
|
|
||||||
Some(gs) => gs.is_match(path),
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect what changed from the given path so we have an idea what needs
|
|
||||||
/// to be reloaded
|
|
||||||
fn detect_change_kind(pwd: &Path, path: &Path, config_path: &Path) -> (ChangeKind, PathBuf) {
|
|
||||||
let mut partial_path = PathBuf::from("/");
|
|
||||||
partial_path.push(path.strip_prefix(pwd).unwrap_or(path));
|
|
||||||
|
|
||||||
let change_kind = if partial_path.starts_with("/templates") {
|
|
||||||
ChangeKind::Templates
|
|
||||||
} else if partial_path.starts_with("/themes") {
|
|
||||||
ChangeKind::Themes
|
|
||||||
} else if partial_path.starts_with("/content") {
|
|
||||||
ChangeKind::Content
|
|
||||||
} else if partial_path.starts_with("/static") {
|
|
||||||
ChangeKind::StaticFiles
|
|
||||||
} else if partial_path.starts_with("/sass") {
|
|
||||||
ChangeKind::Sass
|
|
||||||
} else if path == config_path {
|
|
||||||
ChangeKind::Config
|
|
||||||
} else {
|
|
||||||
unreachable!("Got a change in an unexpected path: {}", partial_path.display());
|
|
||||||
};
|
|
||||||
|
|
||||||
(change_kind, partial_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the directory at path contains any file
|
|
||||||
fn is_folder_empty(dir: &Path) -> bool {
|
|
||||||
// Can panic if we don't have the rights I guess?
|
|
||||||
|
|
||||||
read_dir(dir).expect("Failed to read a directory to see if it was empty").next().is_none()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::{Path, PathBuf};
|
use super::construct_url;
|
||||||
|
|
||||||
use super::{construct_url, detect_change_kind, is_temp_file, ChangeKind};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_recognize_temp_files() {
|
|
||||||
let test_cases = vec![
|
|
||||||
Path::new("hello.swp"),
|
|
||||||
Path::new("hello.swx"),
|
|
||||||
Path::new(".DS_STORE"),
|
|
||||||
Path::new("hello.tmp"),
|
|
||||||
Path::new("hello.html.__jb_old___"),
|
|
||||||
Path::new("hello.html.__jb_tmp___"),
|
|
||||||
Path::new("hello.html.__jb_bak___"),
|
|
||||||
Path::new("hello.html~"),
|
|
||||||
Path::new("#hello.html"),
|
|
||||||
Path::new(".index.md.kate-swp"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for t in test_cases {
|
|
||||||
assert!(is_temp_file(t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_detect_kind_of_changes() {
|
|
||||||
let test_cases = vec![
|
|
||||||
(
|
|
||||||
(ChangeKind::Templates, PathBuf::from("/templates/hello.html")),
|
|
||||||
Path::new("/home/vincent/site"),
|
|
||||||
Path::new("/home/vincent/site/templates/hello.html"),
|
|
||||||
Path::new("/home/vincent/site/config.toml"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(ChangeKind::Themes, PathBuf::from("/themes/hello.html")),
|
|
||||||
Path::new("/home/vincent/site"),
|
|
||||||
Path::new("/home/vincent/site/themes/hello.html"),
|
|
||||||
Path::new("/home/vincent/site/config.toml"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(ChangeKind::StaticFiles, PathBuf::from("/static/site.css")),
|
|
||||||
Path::new("/home/vincent/site"),
|
|
||||||
Path::new("/home/vincent/site/static/site.css"),
|
|
||||||
Path::new("/home/vincent/site/config.toml"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(ChangeKind::Content, PathBuf::from("/content/posts/hello.md")),
|
|
||||||
Path::new("/home/vincent/site"),
|
|
||||||
Path::new("/home/vincent/site/content/posts/hello.md"),
|
|
||||||
Path::new("/home/vincent/site/config.toml"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(ChangeKind::Sass, PathBuf::from("/sass/print.scss")),
|
|
||||||
Path::new("/home/vincent/site"),
|
|
||||||
Path::new("/home/vincent/site/sass/print.scss"),
|
|
||||||
Path::new("/home/vincent/site/config.toml"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(ChangeKind::Config, PathBuf::from("/config.toml")),
|
|
||||||
Path::new("/home/vincent/site"),
|
|
||||||
Path::new("/home/vincent/site/config.toml"),
|
|
||||||
Path::new("/home/vincent/site/config.toml"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(ChangeKind::Config, PathBuf::from("/config.staging.toml")),
|
|
||||||
Path::new("/home/vincent/site"),
|
|
||||||
Path::new("/home/vincent/site/config.staging.toml"),
|
|
||||||
Path::new("/home/vincent/site/config.staging.toml"),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (expected, pwd, path, config_filename) in test_cases {
|
|
||||||
assert_eq!(expected, detect_change_kind(pwd, path, config_filename));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn windows_path_handling() {
|
|
||||||
let expected = (ChangeKind::Templates, PathBuf::from("/templates/hello.html"));
|
|
||||||
let pwd = Path::new(r#"C:\Users\johan\site"#);
|
|
||||||
let path = Path::new(r#"C:\Users\johan\site\templates\hello.html"#);
|
|
||||||
let config_filename = Path::new(r#"C:\Users\johan\site\config.toml"#);
|
|
||||||
assert_eq!(expected, detect_change_kind(pwd, path, config_filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn relative_path() {
|
|
||||||
let expected = (ChangeKind::Templates, PathBuf::from("/templates/hello.html"));
|
|
||||||
let pwd = Path::new("/home/johan/site");
|
|
||||||
let path = Path::new("templates/hello.html");
|
|
||||||
let config_filename = Path::new("config.toml");
|
|
||||||
assert_eq!(expected, detect_change_kind(pwd, path, config_filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_construct_url_base_url_is_slash() {
|
fn test_construct_url_base_url_is_slash() {
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
//! Utilities to simplify working with events raised by the `notify*` family of file system
|
|
||||||
//! event-watching libraries.
|
|
||||||
|
|
||||||
use notify_debouncer_full::notify::event::*;
|
|
||||||
|
|
||||||
/// This enum abstracts over the fine-grained group of enums in `notify`.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum SimpleFSEventKind {
|
|
||||||
Create,
|
|
||||||
Modify,
|
|
||||||
Remove,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Filter `notify_debouncer_full` events. For events that we care about,
|
|
||||||
/// return our internal simplified representation. For events we don't care about,
|
|
||||||
/// return `None`.
|
|
||||||
pub fn get_relevant_event_kind(event_kind: &EventKind) -> Option<SimpleFSEventKind> {
|
|
||||||
match event_kind {
|
|
||||||
EventKind::Create(CreateKind::File) | EventKind::Create(CreateKind::Folder) => {
|
|
||||||
Some(SimpleFSEventKind::Create)
|
|
||||||
}
|
|
||||||
EventKind::Modify(ModifyKind::Data(DataChange::Size))
|
|
||||||
| EventKind::Modify(ModifyKind::Data(DataChange::Content))
|
|
||||||
// Intellij modifies file metadata on edit.
|
|
||||||
// https://github.com/passcod/notify/issues/150#issuecomment-494912080
|
|
||||||
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime))
|
|
||||||
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions))
|
|
||||||
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership))
|
|
||||||
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => Some(SimpleFSEventKind::Modify),
|
|
||||||
EventKind::Remove(RemoveKind::File) | EventKind::Remove(RemoveKind::Folder) => {
|
|
||||||
Some(SimpleFSEventKind::Remove)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use notify_debouncer_full::notify::event::*;
|
|
||||||
|
|
||||||
use super::{get_relevant_event_kind, SimpleFSEventKind};
|
|
||||||
|
|
||||||
// This test makes sure we at least have code coverage on the `notify` event kinds we care
|
|
||||||
// about when watching the file system for site changes. This is to make sure changes to the
|
|
||||||
// event mapping and filtering don't cause us to accidentally ignore things we care about.
|
|
||||||
#[test]
|
|
||||||
fn test_get_relative_event_kind() {
|
|
||||||
let cases = vec![
|
|
||||||
(EventKind::Create(CreateKind::File), Some(SimpleFSEventKind::Create)),
|
|
||||||
(EventKind::Create(CreateKind::Folder), Some(SimpleFSEventKind::Create)),
|
|
||||||
(
|
|
||||||
EventKind::Modify(ModifyKind::Data(DataChange::Size)),
|
|
||||||
Some(SimpleFSEventKind::Modify),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
EventKind::Modify(ModifyKind::Data(DataChange::Content)),
|
|
||||||
Some(SimpleFSEventKind::Modify),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)),
|
|
||||||
Some(SimpleFSEventKind::Modify),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)),
|
|
||||||
Some(SimpleFSEventKind::Modify),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)),
|
|
||||||
Some(SimpleFSEventKind::Modify),
|
|
||||||
),
|
|
||||||
(EventKind::Modify(ModifyKind::Name(RenameMode::To)), Some(SimpleFSEventKind::Modify)),
|
|
||||||
(EventKind::Remove(RemoveKind::File), Some(SimpleFSEventKind::Remove)),
|
|
||||||
(EventKind::Remove(RemoveKind::Folder), Some(SimpleFSEventKind::Remove)),
|
|
||||||
];
|
|
||||||
for (case, expected) in cases.iter() {
|
|
||||||
let ek = get_relevant_event_kind(&case);
|
|
||||||
assert_eq!(ek, *expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
293
src/fs_utils.rs
Normal file
293
src/fs_utils.rs
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
//! Utilities to simplify working with events raised by the `notify*` family of file system
|
||||||
|
//! event-watching libraries.
|
||||||
|
|
||||||
|
use libs::ahash::HashMap;
|
||||||
|
use libs::globset::GlobSet;
|
||||||
|
use notify_debouncer_full::notify::event::*;
|
||||||
|
use notify_debouncer_full::DebouncedEvent;
|
||||||
|
use std::fs::read_dir;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use utils::fs::is_temp_file;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub enum ChangeKind {
|
||||||
|
Content,
|
||||||
|
Templates,
|
||||||
|
Themes,
|
||||||
|
StaticFiles,
|
||||||
|
Sass,
|
||||||
|
Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This enum abstracts over the fine-grained group of enums in `notify`.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum SimpleFileSystemEventKind {
|
||||||
|
Create,
|
||||||
|
Modify,
|
||||||
|
Remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
// (partial path, full path, ..)
|
||||||
|
pub type MeaningfulEvent = (PathBuf, PathBuf, SimpleFileSystemEventKind);
|
||||||
|
|
||||||
|
/// Filter `notify_debouncer_full` events. For events that we care about,
|
||||||
|
/// return our internal simplified representation. For events we don't care about,
|
||||||
|
/// return `None`.
|
||||||
|
fn get_relevant_event_kind(event_kind: &EventKind) -> Option<SimpleFileSystemEventKind> {
|
||||||
|
match event_kind {
|
||||||
|
EventKind::Create(CreateKind::File) | EventKind::Create(CreateKind::Folder) => {
|
||||||
|
Some(SimpleFileSystemEventKind::Create)
|
||||||
|
}
|
||||||
|
EventKind::Modify(ModifyKind::Data(DataChange::Size))
|
||||||
|
| EventKind::Modify(ModifyKind::Data(DataChange::Content))
|
||||||
|
// Intellij modifies file metadata on edit.
|
||||||
|
// https://github.com/passcod/notify/issues/150#issuecomment-494912080
|
||||||
|
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime))
|
||||||
|
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions))
|
||||||
|
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership))
|
||||||
|
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => Some(SimpleFileSystemEventKind::Modify),
|
||||||
|
EventKind::Remove(RemoveKind::File) | EventKind::Remove(RemoveKind::Folder) => {
|
||||||
|
Some(SimpleFileSystemEventKind::Remove)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter_events(
|
||||||
|
mut events: Vec<DebouncedEvent>,
|
||||||
|
root_dir: &Path,
|
||||||
|
config_path: &Path,
|
||||||
|
ignored_content_globset: &Option<GlobSet>,
|
||||||
|
) -> HashMap<ChangeKind, Vec<MeaningfulEvent>> {
|
||||||
|
// Arrange events from oldest to newest.
|
||||||
|
events.sort_by(|e1, e2| e1.time.cmp(&e2.time));
|
||||||
|
|
||||||
|
// Use a map to keep only the last event that occurred for a particular path.
|
||||||
|
// Map `full_path -> (partial_path, simple_event_kind, change_kind)`.
|
||||||
|
let mut meaningful_events: HashMap<PathBuf, (PathBuf, SimpleFileSystemEventKind, ChangeKind)> =
|
||||||
|
HashMap::default();
|
||||||
|
|
||||||
|
for event in events.iter() {
|
||||||
|
let simple_kind = get_relevant_event_kind(&event.event.kind);
|
||||||
|
if simple_kind.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We currently only handle notify events that report a single path per event.
|
||||||
|
if event.event.paths.len() != 1 {
|
||||||
|
console::error(&format!(
|
||||||
|
"Skipping unsupported file system event with multiple paths: {:?}",
|
||||||
|
event.event.kind
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = event.event.paths[0].clone();
|
||||||
|
|
||||||
|
if is_ignored_file(ignored_content_globset, &path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_temp_file(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only care about changes in non-empty folders
|
||||||
|
if path.is_dir() && is_folder_empty(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (change_k, partial_p) = detect_change_kind(root_dir, &path, config_path);
|
||||||
|
meaningful_events.insert(path, (partial_p, simple_kind.unwrap(), change_k));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bin changes by change kind to support later iteration over batches of changes.
|
||||||
|
let mut changes = HashMap::default();
|
||||||
|
for (full_path, (partial_path, event_kind, change_kind)) in meaningful_events.into_iter() {
|
||||||
|
let c = changes.entry(change_kind).or_insert(vec![]);
|
||||||
|
c.push((partial_path, full_path, event_kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ignored_file(ignored_content_globset: &Option<GlobSet>, path: &Path) -> bool {
|
||||||
|
match ignored_content_globset {
|
||||||
|
Some(gs) => gs.is_match(path),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the directory at path contains any file
|
||||||
|
fn is_folder_empty(dir: &Path) -> bool {
|
||||||
|
// Can panic if we don't have the rights I guess?
|
||||||
|
|
||||||
|
read_dir(dir).expect("Failed to read a directory to see if it was empty").next().is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect what changed from the given path so we have an idea what needs
|
||||||
|
/// to be reloaded
|
||||||
|
fn detect_change_kind(pwd: &Path, path: &Path, config_path: &Path) -> (ChangeKind, PathBuf) {
|
||||||
|
let mut partial_path = PathBuf::from("/");
|
||||||
|
partial_path.push(path.strip_prefix(pwd).unwrap_or(path));
|
||||||
|
|
||||||
|
let change_kind = if partial_path.starts_with("/templates") {
|
||||||
|
ChangeKind::Templates
|
||||||
|
} else if partial_path.starts_with("/themes") {
|
||||||
|
ChangeKind::Themes
|
||||||
|
} else if partial_path.starts_with("/content") {
|
||||||
|
ChangeKind::Content
|
||||||
|
} else if partial_path.starts_with("/static") {
|
||||||
|
ChangeKind::StaticFiles
|
||||||
|
} else if partial_path.starts_with("/sass") {
|
||||||
|
ChangeKind::Sass
|
||||||
|
} else if path == config_path {
|
||||||
|
ChangeKind::Config
|
||||||
|
} else {
|
||||||
|
unreachable!("Got a change in an unexpected path: {}", partial_path.display());
|
||||||
|
};
|
||||||
|
|
||||||
|
(change_kind, partial_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use notify_debouncer_full::notify::event::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
detect_change_kind, get_relevant_event_kind, is_temp_file, ChangeKind,
|
||||||
|
SimpleFileSystemEventKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This test makes sure we at least have code coverage on the `notify` event kinds we care
|
||||||
|
// about when watching the file system for site changes. This is to make sure changes to the
|
||||||
|
// event mapping and filtering don't cause us to accidentally ignore things we care about.
|
||||||
|
#[test]
|
||||||
|
fn test_get_relative_event_kind() {
|
||||||
|
let cases = vec![
|
||||||
|
(EventKind::Create(CreateKind::File), Some(SimpleFileSystemEventKind::Create)),
|
||||||
|
(EventKind::Create(CreateKind::Folder), Some(SimpleFileSystemEventKind::Create)),
|
||||||
|
(
|
||||||
|
EventKind::Modify(ModifyKind::Data(DataChange::Size)),
|
||||||
|
Some(SimpleFileSystemEventKind::Modify),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EventKind::Modify(ModifyKind::Data(DataChange::Content)),
|
||||||
|
Some(SimpleFileSystemEventKind::Modify),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)),
|
||||||
|
Some(SimpleFileSystemEventKind::Modify),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)),
|
||||||
|
Some(SimpleFileSystemEventKind::Modify),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)),
|
||||||
|
Some(SimpleFileSystemEventKind::Modify),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EventKind::Modify(ModifyKind::Name(RenameMode::To)),
|
||||||
|
Some(SimpleFileSystemEventKind::Modify),
|
||||||
|
),
|
||||||
|
(EventKind::Remove(RemoveKind::File), Some(SimpleFileSystemEventKind::Remove)),
|
||||||
|
(EventKind::Remove(RemoveKind::Folder), Some(SimpleFileSystemEventKind::Remove)),
|
||||||
|
];
|
||||||
|
for (case, expected) in cases.iter() {
|
||||||
|
let ek = get_relevant_event_kind(&case);
|
||||||
|
assert_eq!(ek, *expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_recognize_temp_files() {
|
||||||
|
let test_cases = vec![
|
||||||
|
Path::new("hello.swp"),
|
||||||
|
Path::new("hello.swx"),
|
||||||
|
Path::new(".DS_STORE"),
|
||||||
|
Path::new("hello.tmp"),
|
||||||
|
Path::new("hello.html.__jb_old___"),
|
||||||
|
Path::new("hello.html.__jb_tmp___"),
|
||||||
|
Path::new("hello.html.__jb_bak___"),
|
||||||
|
Path::new("hello.html~"),
|
||||||
|
Path::new("#hello.html"),
|
||||||
|
Path::new(".index.md.kate-swp"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for t in test_cases {
|
||||||
|
assert!(is_temp_file(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_detect_kind_of_changes() {
|
||||||
|
let test_cases = vec![
|
||||||
|
(
|
||||||
|
(ChangeKind::Templates, PathBuf::from("/templates/hello.html")),
|
||||||
|
Path::new("/home/vincent/site"),
|
||||||
|
Path::new("/home/vincent/site/templates/hello.html"),
|
||||||
|
Path::new("/home/vincent/site/config.toml"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(ChangeKind::Themes, PathBuf::from("/themes/hello.html")),
|
||||||
|
Path::new("/home/vincent/site"),
|
||||||
|
Path::new("/home/vincent/site/themes/hello.html"),
|
||||||
|
Path::new("/home/vincent/site/config.toml"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(ChangeKind::StaticFiles, PathBuf::from("/static/site.css")),
|
||||||
|
Path::new("/home/vincent/site"),
|
||||||
|
Path::new("/home/vincent/site/static/site.css"),
|
||||||
|
Path::new("/home/vincent/site/config.toml"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(ChangeKind::Content, PathBuf::from("/content/posts/hello.md")),
|
||||||
|
Path::new("/home/vincent/site"),
|
||||||
|
Path::new("/home/vincent/site/content/posts/hello.md"),
|
||||||
|
Path::new("/home/vincent/site/config.toml"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(ChangeKind::Sass, PathBuf::from("/sass/print.scss")),
|
||||||
|
Path::new("/home/vincent/site"),
|
||||||
|
Path::new("/home/vincent/site/sass/print.scss"),
|
||||||
|
Path::new("/home/vincent/site/config.toml"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(ChangeKind::Config, PathBuf::from("/config.toml")),
|
||||||
|
Path::new("/home/vincent/site"),
|
||||||
|
Path::new("/home/vincent/site/config.toml"),
|
||||||
|
Path::new("/home/vincent/site/config.toml"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(ChangeKind::Config, PathBuf::from("/config.staging.toml")),
|
||||||
|
Path::new("/home/vincent/site"),
|
||||||
|
Path::new("/home/vincent/site/config.staging.toml"),
|
||||||
|
Path::new("/home/vincent/site/config.staging.toml"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (expected, pwd, path, config_filename) in test_cases {
|
||||||
|
assert_eq!(expected, detect_change_kind(pwd, path, config_filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn windows_path_handling() {
|
||||||
|
let expected = (ChangeKind::Templates, PathBuf::from("/templates/hello.html"));
|
||||||
|
let pwd = Path::new(r#"C:\Users\johan\site"#);
|
||||||
|
let path = Path::new(r#"C:\Users\johan\site\templates\hello.html"#);
|
||||||
|
let config_filename = Path::new(r#"C:\Users\johan\site\config.toml"#);
|
||||||
|
assert_eq!(expected, detect_change_kind(pwd, path, config_filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_path() {
|
||||||
|
let expected = (ChangeKind::Templates, PathBuf::from("/templates/hello.html"));
|
||||||
|
let pwd = Path::new("/home/johan/site");
|
||||||
|
let path = Path::new("templates/hello.html");
|
||||||
|
let config_filename = Path::new("config.toml");
|
||||||
|
assert_eq!(expected, detect_change_kind(pwd, path, config_filename));
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ use time::UtcOffset;
|
|||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod fs_event_utils;
|
mod fs_utils;
|
||||||
mod messages;
|
mod messages;
|
||||||
mod prompt;
|
mod prompt;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user