use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, event::ModifyKind};
use crate::policy;
#[derive(Debug)]
pub enum Command {
Batch(Vec<Change>),
Rescan,
Shutdown,
}
#[derive(Debug, Clone)]
pub struct Change {
pub root: u16,
pub rel: String,
pub kind: ChangeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeKind {
Upsert,
Remove,
}
#[cfg(test)]
pub(crate) fn log_watcher_unavailable(e: &dyn std::fmt::Display) {
tracing::warn!(error = %e, "file watcher unavailable; index will not self-refresh");
}
pub fn spawn(roots: &[PathBuf], tx: Sender<Command>) -> Option<RecommendedWatcher> {
let watched: Vec<PathBuf> = roots.to_vec();
let watcher = RecommendedWatcher::new(
move |res: Result<Event, notify::Error>| {
let Ok(event) = res else { return };
let changes = map_event(&watched, &event);
if !changes.is_empty() {
let _ = tx.send(Command::Batch(changes));
}
},
notify::Config::default(),
);
let mut watcher = watcher.ok()?;
let mut any = false;
for root in roots {
match watcher.watch(root, RecursiveMode::Recursive) {
Ok(()) => any = true,
Err(e) => {
tracing::warn!(root = %root.display(), error = %e, "cannot watch root");
}
}
}
any.then_some(watcher)
}
pub(crate) fn map_event(roots: &[PathBuf], event: &Event) -> Vec<Change> {
use notify::event::{CreateKind, RemoveKind};
let mut out = Vec::new();
match event.kind {
EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => {
for path in &event.paths {
push_change(roots, path, ChangeKind::Upsert, &mut out);
}
}
EventKind::Remove(RemoveKind::File | RemoveKind::Folder | RemoveKind::Any) => {
for path in &event.paths {
push_change(roots, path, ChangeKind::Remove, &mut out);
}
}
EventKind::Modify(ModifyKind::Name(_)) => {
if event.paths.len() == 2 {
push_change(roots, &event.paths[0], ChangeKind::Remove, &mut out);
push_change(roots, &event.paths[1], ChangeKind::Upsert, &mut out);
} else {
for path in &event.paths {
push_change(roots, path, ChangeKind::Upsert, &mut out);
}
}
}
EventKind::Modify(_) => {
for path in &event.paths {
push_change(roots, path, ChangeKind::Upsert, &mut out);
}
}
_ => {} }
out
}
fn push_change(roots: &[PathBuf], path: &Path, kind: ChangeKind, out: &mut Vec<Change>) {
for (idx, root) in roots.iter().enumerate() {
let Ok(rel) = path.strip_prefix(root) else {
continue;
};
let rel = rel.to_string_lossy().replace('\\', "/");
if rel.is_empty() || !policy::rel_path_allowed(&rel) {
return;
}
out.push(Change {
root: idx as u16,
rel,
kind,
});
return; }
}
#[cfg(test)]
#[path = "watch_tests.rs"]
mod tests;