use std::collections::HashMap;
use std::path::Path;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use crate::error::RustMinidbError;
use crate::storage::engine::SharedEngine;
use crate::storage::redb_engine::RedbEngine;
#[derive(Debug, Clone)]
pub enum FileChangeEvent {
DatabaseModified {
path: String,
db_name: String,
},
DatabaseCreated {
path: String,
db_name: String,
},
DatabaseDeleted {
path: String,
db_name: String,
},
}
pub type ChangeCallback = Arc<dyn Fn(FileChangeEvent) + Send + Sync>;
pub struct FileWatcher {
watcher: Option<RecommendedWatcher>,
handle: Option<thread::JoinHandle<()>>,
running: Arc<Mutex<bool>>,
}
impl FileWatcher {
pub fn new() -> Self {
Self {
watcher: None,
handle: None,
running: Arc::new(Mutex::new(false)),
}
}
pub fn watch<P: AsRef<Path>>(
&mut self,
dir: P,
callback: ChangeCallback,
) -> crate::error::Result<()> {
let dir_path = dir.as_ref().to_path_buf();
if !dir_path.exists() {
return Err(RustMinidbError::Config(format!(
"Directory does not exist: {}",
dir_path.display()
)));
}
let (tx, rx) = mpsc::channel::<crate::error::Result<Event>>();
let running = self.running.clone();
let event_tx = tx.clone();
let mut watcher = RecommendedWatcher::new(
move |res: notify::Result<Event>| {
match res {
Ok(event) => {
let _ = event_tx.send(Ok(event));
}
Err(e) => {
let _ = event_tx.send(Err(RustMinidbError::Config(
format!("File watch error: {}", e),
)));
}
}
},
Config::default(),
)
.map_err(|e| {
RustMinidbError::Config(format!("Failed to create file watcher: {}", e))
})?;
watcher
.watch(&dir_path, RecursiveMode::NonRecursive)
.map_err(|e| {
RustMinidbError::Config(format!(
"Failed to watch directory {}: {}",
dir_path.display(),
e
))
})?;
*running.lock().unwrap() = true;
let running_clone = running.clone();
let cb = callback.clone();
let handle = thread::spawn(move || {
let debounce_map: Arc<Mutex<HashMap<String, std::time::Instant>>> =
Arc::new(Mutex::new(HashMap::new()));
loop {
if !*running_clone.lock().unwrap() {
break;
}
match rx.recv_timeout(Duration::from_millis(500)) {
Ok(Ok(event)) => {
let db_events: Vec<FileChangeEvent> = event
.paths
.iter()
.filter(|p| {
p.extension()
.map(|e| e == "db")
.unwrap_or(false)
})
.filter_map(|path| {
let path_str = path.to_string_lossy().to_string();
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
{
let mut debounce = debounce_map.lock().unwrap();
if let Some(last) = debounce.get(&path_str) {
if last.elapsed() < Duration::from_secs(2) {
return None;
}
}
debounce.insert(path_str.clone(), std::time::Instant::now());
}
match &event.kind {
EventKind::Modify(_) => Some(FileChangeEvent::DatabaseModified {
path: path_str,
db_name: file_name,
}),
EventKind::Create(_) => Some(FileChangeEvent::DatabaseCreated {
path: path_str,
db_name: file_name,
}),
EventKind::Remove(_) => Some(FileChangeEvent::DatabaseDeleted {
path: path_str,
db_name: file_name,
}),
_ => None,
}
})
.collect();
for evt in db_events {
cb(evt);
}
}
Ok(Err(_e)) => {
}
Err(mpsc::RecvTimeoutError::Timeout) => {
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
break;
}
}
}
});
self.watcher = Some(watcher);
self.handle = Some(handle);
Ok(())
}
pub fn stop(&mut self) {
*self.running.lock().unwrap() = false;
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
self.watcher = None;
}
}
impl Drop for FileWatcher {
fn drop(&mut self) {
self.stop();
}
}
pub fn reload_engine(path: &str) -> crate::error::Result<SharedEngine> {
let engine = RedbEngine::open(path)?;
Ok(Arc::new(engine))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use tempfile::TempDir;
#[test]
fn test_watcher_create_and_drop() {
let dir = TempDir::new().unwrap();
let mut watcher = FileWatcher::new();
let flag = Arc::new(AtomicBool::new(false));
let f = flag.clone();
let cb: ChangeCallback = Arc::new(move |_evt| {
f.store(true, Ordering::SeqCst);
});
let result = watcher.watch(dir.path(), cb);
assert!(result.is_ok());
watcher.stop();
}
}