use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::mpsc;
#[derive(Debug, Clone)]
pub enum WatchEvent {
Created(PathBuf),
Modified(PathBuf),
Deleted(PathBuf),
Error(String),
}
pub struct FileWatcher {
_watcher: RecommendedWatcher,
receiver: mpsc::UnboundedReceiver<WatchEvent>,
}
impl FileWatcher {
pub fn new(paths: &[PathBuf]) -> Result<Self, Box<dyn std::error::Error>> {
let (tx, rx) = mpsc::unbounded_channel();
let tx = Arc::new(tx);
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
let event = match res {
Ok(event) => match event.kind {
EventKind::Create(_) => {
if let Some(path) = event.paths.first() {
WatchEvent::Created(path.clone())
} else {
return;
}
}
EventKind::Modify(_) => {
if let Some(path) = event.paths.first() {
WatchEvent::Modified(path.clone())
} else {
return;
}
}
EventKind::Remove(_) => {
if let Some(path) = event.paths.first() {
WatchEvent::Deleted(path.clone())
} else {
return;
}
}
_ => return,
},
Err(e) => WatchEvent::Error(e.to_string()),
};
let _ = tx.send(event);
})?;
for path in paths {
watcher.watch(path, RecursiveMode::Recursive)?;
}
Ok(Self {
_watcher: watcher,
receiver: rx,
})
}
pub async fn next_event(&mut self) -> Option<WatchEvent> {
self.receiver.recv().await
}
pub fn try_next_event(&mut self) -> Option<WatchEvent> {
self.receiver.try_recv().ok()
}
}
impl Drop for FileWatcher {
fn drop(&mut self) {
self.receiver.close();
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
pub struct FileWatcherBuilder {
paths: Vec<PathBuf>,
}
impl FileWatcherBuilder {
pub fn new() -> Self {
Self { paths: Vec::new() }
}
pub fn watch_path(mut self, path: PathBuf) -> Self {
self.paths.push(path);
self
}
pub fn build(self) -> Result<FileWatcher, Box<dyn std::error::Error>> {
FileWatcher::new(&self.paths)
}
}
impl Default for FileWatcherBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_watch_event_variants() {
let event = WatchEvent::Created(PathBuf::from("test.txt"));
assert!(matches!(event, WatchEvent::Created(_)));
let event = WatchEvent::Modified(PathBuf::from("test.txt"));
assert!(matches!(event, WatchEvent::Modified(_)));
let event = WatchEvent::Deleted(PathBuf::from("test.txt"));
assert!(matches!(event, WatchEvent::Deleted(_)));
let event = WatchEvent::Error("test error".to_string());
assert!(matches!(event, WatchEvent::Error(_)));
}
#[tokio::test]
#[serial(file_watcher)]
async fn test_file_watcher_creation() {
let temp_dir = TempDir::new().unwrap();
let paths = vec![temp_dir.path().to_path_buf()];
let watcher = FileWatcher::new(&paths);
assert!(watcher.is_ok());
drop(watcher);
}
#[tokio::test]
#[serial(file_watcher)]
async fn test_file_watcher_detects_changes() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
let paths = vec![temp_dir.path().to_path_buf()];
let mut watcher = FileWatcher::new(&paths).unwrap();
fs::write(&test_file, "test content").unwrap();
let event =
tokio::time::timeout(std::time::Duration::from_secs(2), watcher.next_event()).await;
let event = event.unwrap();
assert!(event.is_some());
drop(watcher);
}
#[tokio::test]
#[serial(file_watcher)]
async fn test_file_watcher_try_next() {
let temp_dir = TempDir::new().unwrap();
let paths = vec![temp_dir.path().to_path_buf()];
let mut watcher = FileWatcher::new(&paths).unwrap();
let event = watcher.try_next_event();
assert!(event.is_none());
drop(watcher);
}
#[test]
fn test_builder_new() {
let builder = FileWatcherBuilder::new();
assert_eq!(builder.paths.len(), 0);
}
#[test]
fn test_builder_watch_path() {
let builder = FileWatcherBuilder::new()
.watch_path(PathBuf::from("./static"))
.watch_path(PathBuf::from("./templates"));
assert_eq!(builder.paths.len(), 2);
}
#[test]
fn test_builder_default() {
let builder = FileWatcherBuilder::default();
assert_eq!(builder.paths.len(), 0);
}
}