use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[cfg(feature = "tokio")]
use std::sync::Mutex;
#[derive(Debug, Clone)]
pub enum FileEvent {
Modified(PathBuf),
Created(PathBuf),
Deleted(PathBuf),
Renamed {
from: PathBuf,
to: PathBuf,
},
AccessChanged(PathBuf),
}
pub struct FileWatcherConfig {
pub recursive: bool,
pub poll_interval_ms: u64,
pub ignore_patterns: Vec<String>,
}
impl Default for FileWatcherConfig {
fn default() -> Self {
Self {
recursive: false,
poll_interval_ms: 500,
ignore_patterns: vec![
".git".into(),
"node_modules".into(),
"target".into(),
".DS_Store".into(),
],
}
}
}
pub struct FileWatcher {
watchers: HashMap<PathBuf, Arc<dyn Fn(FileEvent) + Send + Sync>>,
config: FileWatcherConfig,
last_state: HashMap<PathBuf, std::time::SystemTime>,
}
impl FileWatcher {
pub fn new() -> Self {
Self {
watchers: HashMap::new(),
config: FileWatcherConfig::default(),
last_state: HashMap::new(),
}
}
pub fn with_config(config: FileWatcherConfig) -> Self {
Self {
watchers: HashMap::new(),
config,
last_state: HashMap::new(),
}
}
pub fn watch<F>(&mut self, path: impl AsRef<Path>, callback: F)
where
F: Fn(FileEvent) + Send + Sync + 'static,
{
let path = path.as_ref().to_path_buf();
self.watchers.insert(path, Arc::new(callback));
}
pub fn unwatch(&mut self, path: &Path) {
self.watchers.remove(path);
}
pub fn check_changes(&mut self) -> Vec<FileEvent> {
let mut events = Vec::new();
for (path, callback) in &self.watchers {
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(modified) = metadata.modified() {
let last = self.last_state.get(path).cloned();
self.last_state.insert(path.clone(), modified);
if let Some(last_time) = last {
if modified > last_time {
let event = FileEvent::Modified(path.clone());
callback(event.clone());
events.push(event);
}
} else {
}
}
} else {
if self.last_state.contains_key(path) {
let event = FileEvent::Deleted(path.clone());
callback(event.clone());
events.push(event);
self.last_state.remove(path);
}
}
}
events
}
pub fn watched_paths(&self) -> Vec<&Path> {
self.watchers.keys().map(|p| p.as_path()).collect()
}
pub fn config(&self) -> &FileWatcherConfig {
&self.config
}
}
impl Default for FileWatcher {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "tokio")]
pub struct AsyncFileWatcher {
inner: Arc<Mutex<FileWatcher>>,
_task: Option<tokio::task::JoinHandle<()>>,
}
#[cfg(feature = "tokio")]
impl AsyncFileWatcher {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(FileWatcher::new())),
_task: None,
}
}
pub async fn watch_async<F>(&self, path: impl AsRef<Path>, callback: F)
where
F: Fn(FileEvent) + Send + Sync + 'static,
{
let path = path.as_ref().to_path_buf();
self.inner.lock().unwrap().watch(path, callback);
}
pub fn start_polling(&mut self, interval_ms: u64) {
let inner = self.inner.clone();
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_millis(interval_ms));
loop {
interval.tick().await;
let _events = inner.lock().unwrap().check_changes();
}
});
self._task = Some(handle);
}
}
pub mod utils {
use super::*;
pub fn is_file_modified(path: &Path, last_check: std::time::SystemTime) -> bool {
std::fs::metadata(path)
.and_then(|m| m.modified())
.map(|modified| modified > last_check)
.unwrap_or(false)
}
pub fn last_modified(path: &Path) -> Option<std::time::SystemTime> {
std::fs::metadata(path).and_then(|m| m.modified()).ok()
}
pub fn should_ignore(path: &Path, patterns: &[String]) -> bool {
let path_str = path.to_string_lossy();
patterns
.iter()
.any(|pattern| path_str.contains(pattern.as_str()))
}
}