use notify::{
event::{Event, EventKind},
RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
};
use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::{debug, error, info, warn};
pub mod config;
pub use config::{ConfigWatcher, HotConfig};
#[derive(Error, Debug)]
pub enum WatchError {
#[error("Failed to watch path: {path}")]
WatchFailed {
path: PathBuf,
#[source]
source: notify::Error,
},
#[error("Failed to read file: {0}")]
ReadFailed(#[from] std::io::Error),
#[error("Failed to parse config: {0}")]
ParseFailed(String),
#[error("Invalid path: {0}")]
InvalidPath(PathBuf),
}
pub type Result<T> = std::result::Result<T, WatchError>;
pub struct FileWatcher {
_watcher: RecommendedWatcher,
path: PathBuf,
}
impl FileWatcher {
pub fn new<P, F>(path: P, mut callback: F) -> Result<Self>
where
P: AsRef<Path>,
F: FnMut(&Path) + Send + 'static,
{
let path = path.as_ref().to_path_buf();
if !path.exists() {
warn!("Watch path does not exist yet: {:?}", path);
}
let watch_path = path.clone();
let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
match res {
Ok(event) => {
if matches!(
event.kind,
EventKind::Modify(_) | EventKind::Create(_)
) {
debug!("File change detected: {:?}", event.paths);
for p in &event.paths {
callback(p);
}
}
}
Err(e) => error!("Watch error: {:?}", e),
}
})
.map_err(|e| WatchError::WatchFailed {
path: path.clone(),
source: e,
})?;
watcher
.watch(&watch_path, RecursiveMode::NonRecursive)
.map_err(|e| WatchError::WatchFailed {
path: path.clone(),
source: e,
})?;
info!("Watching file: {:?}", path);
Ok(Self {
_watcher: watcher,
path,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
pub fn watch<P, F>(path: P, callback: F) -> Result<FileWatcher>
where
P: AsRef<Path>,
F: FnMut(&Path) + Send + 'static,
{
FileWatcher::new(path, callback)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tempfile::NamedTempFile;
#[test]
fn test_watcher_creation() {
let temp = NamedTempFile::new().unwrap();
let count = Arc::new(AtomicUsize::new(0));
let count_clone = count.clone();
let _watcher = FileWatcher::new(temp.path(), move |_| {
count_clone.fetch_add(1, Ordering::SeqCst);
});
assert!(_watcher.is_ok());
}
}