use crate::sandbox;
use notify::{
event::EventKind, RecommendedWatcher, RecursiveMode, Result as NotifyResult,
Watcher as NotifyWatcher,
};
use std::{
collections::VecDeque,
path::{Path, PathBuf},
};
use tokio::sync::mpsc::{self, Receiver};
use tracing::debug;
#[derive(Debug, PartialEq, Eq)]
pub enum WatcherEvent {
Created { file_path: PathBuf },
Removed { file_path: PathBuf },
Changed { file_path: PathBuf },
}
#[must_use]
pub struct Watcher {
_inner: RecommendedWatcher,
base_dir: PathBuf,
notify_receiver: Receiver<NotifyResult<notify::Event>>,
out_queue: VecDeque<WatcherEvent>,
}
impl Watcher {
pub fn new(dir: &Path) -> Self {
let (tx, rx) = mpsc::channel(1);
let mut watcher = notify::recommended_watcher(move |res: NotifyResult<notify::Event>| {
futures::executor::block_on(async {
tx.send(res).await.unwrap();
});
})
.expect("Could not construct watcher");
watcher
.watch(dir, RecursiveMode::Recursive)
.expect("Failed to watch directory");
Self {
_inner: watcher,
base_dir: dir.to_path_buf(),
notify_receiver: rx,
out_queue: VecDeque::new(),
}
}
#[must_use]
pub async fn next(&mut self) -> Option<WatcherEvent> {
loop {
if let Some(event) = self.out_queue.pop_front() {
return Some(event);
}
let event = self.notify_receiver.recv().await.unwrap().unwrap();
match event.kind {
EventKind::Create(notify::event::CreateKind::File) => {
assert!(event.paths.len() == 1);
if let Some(e) = self.maybe_created(&event.paths[0]) {
return Some(e);
}
}
EventKind::Remove(notify::event::RemoveKind::File) => {
assert!(event.paths.len() == 1);
if let Some(e) = self.maybe_removed(&event.paths[0]) {
return Some(e);
}
}
EventKind::Modify(notify::event::ModifyKind::Data(_)) => {
assert!(event.paths.len() == 1);
if let Some(e) = self.maybe_modified(&event.paths[0]) {
return Some(e);
}
}
EventKind::Modify(notify::event::ModifyKind::Name(
notify::event::RenameMode::Both,
)) => {
assert!(event.paths.len() == 2);
let removed = self.maybe_removed(&event.paths[0]);
let created = self.maybe_created(&event.paths[1]);
if let Some(e) = created {
self.out_queue.push_back(e);
}
if let Some(e) = removed {
return Some(e);
}
}
EventKind::Modify(notify::event::ModifyKind::Name(
notify::event::RenameMode::Any,
)) => {
assert!(event.paths.len() == 1);
let file_path = event.paths[0].clone();
match sandbox::exists(&self.base_dir, &file_path) {
Ok(path_exists) => {
if path_exists {
if let Some(e) = self.maybe_created(&file_path) {
return Some(e);
}
} else if let Some(e) = self.maybe_removed(&file_path) {
return Some(e);
}
}
Err(error) => {
debug!(
"Ignoring creation/removal of '{}' because of an error: {}",
&file_path.display(),
error
);
}
}
}
EventKind::Access(_) => {
}
e => {
debug!("Unhandled event in {:?}: {e:?}", event.paths);
}
}
}
}
#[must_use]
fn maybe_created(&self, file_path: &Path) -> Option<WatcherEvent> {
match sandbox::ignored(&self.base_dir, file_path) {
Ok(is_ignored) => {
if is_ignored {
debug!("Ignoring creation of '{}'", file_path.display());
return None;
}
}
Err(error) => {
debug!(
"Ignoring creation of '{}' because of an error: {}",
file_path.display(),
error
);
return None;
}
}
Some(WatcherEvent::Created {
file_path: file_path.to_path_buf(),
})
}
#[must_use]
#[expect(clippy::unnecessary_wraps, clippy::unused_self)]
fn maybe_removed(&self, file_path: &Path) -> Option<WatcherEvent> {
Some(WatcherEvent::Removed {
file_path: file_path.to_path_buf(),
})
}
#[must_use]
fn maybe_modified(&self, file_path: &Path) -> Option<WatcherEvent> {
match sandbox::ignored(&self.base_dir, file_path) {
Ok(is_ignored) => {
if is_ignored {
debug!("Ignoring modification of '{}'", file_path.display());
return None;
}
}
Err(error) => {
debug!(
"Ignoring modification of '{}' because of an error: {}",
file_path.display(),
error
);
return None;
}
}
Some(WatcherEvent::Changed {
file_path: file_path.to_path_buf(),
})
}
}
#[cfg(test)]
#[cfg(target_os = "linux")] mod tests {
use temp_dir::TempDir;
use super::*;
#[tokio::test]
async fn create() {
let dir = TempDir::new().expect("Failed to create temp directory");
let dir_path = dir.path().canonicalize().unwrap();
let mut file = dir_path.clone();
file.push("file");
let mut watcher = Watcher::new(&dir_path);
sandbox::write_file(&dir_path, &file, b"hi").unwrap();
assert_eq!(
watcher.next().await,
Some(WatcherEvent::Created {
file_path: file,
})
);
}
#[tokio::test]
async fn change() {
let dir = TempDir::new().expect("Failed to create temp directory");
let dir_path = dir.path().canonicalize().unwrap();
let mut file = dir_path.clone();
file.push("file");
sandbox::write_file(&dir_path, &file, b"hi").unwrap();
let mut watcher = Watcher::new(&dir_path);
sandbox::write_file(&dir_path, &file, b"yo").unwrap();
assert_eq!(
watcher.next().await,
Some(WatcherEvent::Changed { file_path: file })
);
}
#[tokio::test]
async fn remove() {
let dir = TempDir::new().expect("Failed to create temp directory");
let dir_path = dir.path().canonicalize().unwrap();
let mut file = dir_path.clone();
file.push("file");
sandbox::write_file(&dir_path, &file, b"hi").unwrap();
let mut watcher = Watcher::new(&dir_path);
sandbox::remove_file(&dir_path, &file).unwrap();
assert_eq!(
watcher.next().await,
Some(WatcherEvent::Removed { file_path: file })
);
}
#[tokio::test]
async fn rename() {
let dir = TempDir::new().expect("Failed to create temp directory");
let dir_path = dir.path().canonicalize().unwrap();
let mut file = dir_path.clone();
file.push("file");
let mut file_new = dir_path.clone();
file_new.push("file2");
sandbox::write_file(&dir_path, &file, b"hi").unwrap();
let mut watcher = Watcher::new(&dir_path);
sandbox::rename_file(&dir_path, &file, &file_new).unwrap();
assert_eq!(
watcher.next().await,
Some(WatcherEvent::Removed { file_path: file })
);
assert_eq!(
watcher.next().await,
Some(WatcherEvent::Created {
file_path: file_new,
})
);
}
#[tokio::test]
async fn ignore() {
let dir = TempDir::new().expect("Failed to create temp directory");
let dir_path = dir.path().canonicalize().unwrap();
let mut gitignore = dir_path.clone();
gitignore.push(".ignore");
sandbox::write_file(&dir_path, &gitignore, b"file").unwrap();
let mut watcher = Watcher::new(&dir_path);
let mut file = dir_path.clone();
file.push("file");
let mut file2 = dir_path.clone();
file2.push("file2");
sandbox::write_file(&dir_path, &file, b"hi").unwrap();
sandbox::write_file(&dir_path, &file2, b"ho").unwrap();
assert_eq!(
watcher.next().await,
Some(WatcherEvent::Created { file_path: file2 })
);
}
}