use std::path::{Path, PathBuf};
use notify::Event;
pub use notify::{Error, RecommendedWatcher, RecursiveMode, Watcher};
use super::model::Change;
pub fn spawn(
root: &Path,
mut on_change: impl FnMut(Change) + Send + 'static,
) -> Result<RecommendedWatcher, Error> {
let root = root.to_path_buf();
let watched_root = root.clone();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
if let Ok(event) = res {
for change in event_to_changes(&event, &root) {
on_change(change);
}
}
})?;
watcher.watch(&watched_root, RecursiveMode::NonRecursive)?;
Ok(watcher)
}
pub fn event_to_changes(event: &Event, root: &Path) -> Vec<Change> {
if event.need_rescan() {
return vec![Change::Rescan];
}
use notify::event::{EventKind, ModifyKind, RenameMode};
let mut changes = Vec::new();
match &event.kind {
EventKind::Create(_) => push_change(&mut changes, event.paths.first(), root, |p| {
Change::Created { path: p }
}),
EventKind::Remove(_) => push_change(&mut changes, event.paths.first(), root, |p| {
Change::Removed { path: p }
}),
EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
if let (Some(from), Some(to)) = (event.paths.first(), event.paths.get(1))
&& under_root(root, from) {
changes.push(Change::Renamed { from: from.clone(), to: to.clone() });
}
}
EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
push_change(&mut changes, event.paths.first(), root, |p| Change::Created { path: p });
}
EventKind::Modify(ModifyKind::Name(mode))
if !matches!(mode, RenameMode::To | RenameMode::Both) =>
{
push_change(&mut changes, event.paths.first(), root, |p| Change::Removed {
path: p,
});
}
EventKind::Modify(ModifyKind::Metadata(_)) => {
push_change(&mut changes, event.paths.first(), root, |p| Change::Modified {
path: p,
});
}
_ => {}
}
changes
}
fn push_change(
changes: &mut Vec<Change>,
path: Option<&PathBuf>,
root: &Path,
build: impl Fn(PathBuf) -> Change,
) {
if let Some(path) = path
&& under_root(root, path)
{
changes.push(build(path.to_path_buf()));
}
}
fn under_root(root: &Path, path: &Path) -> bool {
path.starts_with(root)
}
#[cfg(test)]
mod tests {
use super::*;
use notify::event::{CreateKind, EventKind, ModifyKind, RemoveKind, RenameMode};
fn ev(kind: EventKind, paths: &[&str]) -> Event {
let mut event = Event::new(kind);
for p in paths {
event = event.add_path(PathBuf::from(p));
}
event
}
const ROOT: &str = "/home/u/src";
#[test]
fn creates_and_removes_map_directly() {
let e = ev(EventKind::Create(CreateKind::File), &["/home/u/src/a.txt"]);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![Change::Created {
path: "/home/u/src/a.txt".into()
}]);
let e = ev(EventKind::Remove(RemoveKind::Folder), &["/home/u/src/dir"]);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![Change::Removed {
path: "/home/u/src/dir".into()
}]);
}
#[test]
fn rename_both_preserves_from_and_to() {
let e = ev(
EventKind::Modify(ModifyKind::Name(RenameMode::Both)),
&["/home/u/src/old.txt", "/home/u/src/new.txt"],
);
assert_eq!(
event_to_changes(&e, Path::new(ROOT)),
vec![Change::Renamed {
from: "/home/u/src/old.txt".into(),
to: "/home/u/src/new.txt".into()
}]
);
}
#[test]
fn rename_single_sides_become_removed_and_created() {
let from_only =
ev(EventKind::Modify(ModifyKind::Name(RenameMode::From)), &["/home/u/src/x"]);
assert_eq!(event_to_changes(&from_only, Path::new(ROOT)), vec![Change::Removed {
path: "/home/u/src/x".into()
}]);
let to_only =
ev(EventKind::Modify(ModifyKind::Name(RenameMode::To)), &["/home/u/src/y"]);
assert_eq!(event_to_changes(&to_only, Path::new(ROOT)), vec![Change::Created {
path: "/home/u/src/y".into()
}]);
}
#[test]
fn metadata_change_becomes_modified() {
let e = ev(
EventKind::Modify(ModifyKind::Metadata(notify::event::MetadataKind::WriteTime)),
&["/home/u/src/a.txt"],
);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![Change::Modified {
path: "/home/u/src/a.txt".into()
}]);
}
#[test]
fn data_writes_are_ignored() {
let e = ev(
EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Content)),
&["/home/u/src/log.txt"],
);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
}
#[test]
fn events_outside_root_are_dropped() {
let e = ev(EventKind::Create(CreateKind::File), &["/tmp/elsewhere/f.txt"]);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
}
#[test]
fn root_itself_is_in_scope() {
let e = ev(EventKind::Create(CreateKind::Folder), &[ROOT]);
assert_eq!(event_to_changes(&e, Path::new(ROOT)).len(), 1);
}
#[test]
fn events_without_paths_are_silently_ignored() {
let e = ev(EventKind::Create(CreateKind::File), &[]);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
}
#[test]
fn generic_kinds_are_ignored() {
let e = ev(EventKind::Any, &["/home/u/src/a"]);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
let e = ev(EventKind::Access(notify::event::AccessKind::Open(notify::event::AccessMode::Any)), &["/home/u/src/a"]);
assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
}
#[test]
fn rescan_flag_dominates_everything() {
let e = Event::new(EventKind::Create(CreateKind::File))
.add_path("/home/u/src/a.txt".into());
let needs_rescan = Event {
attrs: {
let mut a = notify::event::EventAttributes::new();
a.set_flag(notify::event::Flag::Rescan);
a
},
..e
};
assert_eq!(event_to_changes(&needs_rescan, Path::new(ROOT)), vec![Change::Rescan]);
}
}