use crate::error::{Result, RustDriveSyncError};
use notify_debouncer_mini::{
new_debouncer, DebounceEventResult, DebouncedEvent, DebouncedEventKind, Debouncer,
};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::Duration;
use tracing::{debug, error, info};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileEventType {
Created,
Modified,
Deleted,
Other,
}
#[derive(Debug, Clone)]
pub struct FileEvent {
pub event_type: FileEventType,
pub path: PathBuf,
}
impl FileEvent {
pub fn new(event_type: FileEventType, path: PathBuf) -> Self {
Self { event_type, path }
}
pub fn needs_sync(&self) -> bool {
matches!(
self.event_type,
FileEventType::Created | FileEventType::Modified
)
}
}
#[derive(Debug, Clone)]
pub struct WatcherConfig {
pub debounce_duration: Duration,
pub ignore_patterns: Vec<String>,
}
impl Default for WatcherConfig {
fn default() -> Self {
Self {
debounce_duration: Duration::from_secs(2),
ignore_patterns: vec![
".git".to_string(),
".gitignore".to_string(),
"node_modules".to_string(),
"target".to_string(),
".DS_Store".to_string(),
"Thumbs.db".to_string(),
".sync_state.json".to_string(),
"state.json".to_string(),
],
}
}
}
impl WatcherConfig {
pub fn with_debounce(mut self, duration: Duration) -> Self {
self.debounce_duration = duration;
self
}
pub fn with_ignore_patterns(mut self, patterns: Vec<String>) -> Self {
self.ignore_patterns = patterns;
self
}
pub fn should_ignore(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in &self.ignore_patterns {
if path_str.contains(pattern.as_str()) {
return true;
}
}
false
}
}
pub struct FileWatcher {
_debouncer: Debouncer<notify::RecommendedWatcher>,
event_receiver: Receiver<FileEvent>,
config: WatcherConfig,
watched_path: PathBuf,
}
impl FileWatcher {
pub fn new<P: AsRef<Path>>(path: P, config: WatcherConfig) -> Result<Self> {
let path = path.as_ref();
if !path.exists() {
return Err(RustDriveSyncError::FileNotFound {
path: path.display().to_string(),
});
}
if !path.is_dir() {
return Err(RustDriveSyncError::DriveApiError {
message: format!("Path não é um diretório: {}", path.display()),
});
}
info!(
"Inicializando file watcher para: {} (debounce: {:?})",
path.display(),
config.debounce_duration
);
let (event_tx, event_rx) = channel::<FileEvent>();
let config_clone = config.clone();
let event_tx_clone = event_tx.clone();
let mut debouncer = new_debouncer(
config.debounce_duration,
move |result: DebounceEventResult| {
Self::handle_debounced_events(result, &config_clone, &event_tx_clone);
},
)
.map_err(|e| RustDriveSyncError::DriveApiError {
message: format!("Erro ao criar debouncer: {}", e),
})?;
debouncer
.watcher()
.watch(path, notify::RecursiveMode::Recursive)
.map_err(|e| RustDriveSyncError::DriveApiError {
message: format!("Erro ao iniciar monitoramento: {}", e),
})?;
info!("File watcher iniciado com sucesso");
Ok(Self {
_debouncer: debouncer,
event_receiver: event_rx,
config,
watched_path: path.to_path_buf(),
})
}
fn handle_debounced_events(
result: DebounceEventResult,
config: &WatcherConfig,
tx: &Sender<FileEvent>,
) {
match result {
Ok(events) => {
for event in events {
Self::process_event(event, config, tx);
}
}
Err(err) => {
error!("Erro no file watcher: {:?}", err);
}
}
}
fn process_event(event: DebouncedEvent, config: &WatcherConfig, tx: &Sender<FileEvent>) {
if config.should_ignore(&event.path) {
debug!("Ignorando evento para: {}", event.path.display());
return;
}
let event_type = match event.kind {
DebouncedEventKind::Any => {
if event.path.exists() {
FileEventType::Modified
} else {
FileEventType::Deleted
}
}
DebouncedEventKind::AnyContinuous => FileEventType::Modified,
_ => FileEventType::Other, };
let file_event = FileEvent::new(event_type.clone(), event.path.clone());
debug!(
"Evento detectado: {:?} - {}",
event_type,
event.path.display()
);
if let Err(e) = tx.send(file_event) {
error!("Erro ao enviar evento: {}", e);
}
}
pub fn next_event(&self) -> Result<FileEvent> {
self.event_receiver
.recv()
.map_err(|e| RustDriveSyncError::DriveApiError {
message: format!("Erro ao receber evento: {}", e),
})
}
pub fn try_next_event(&self) -> Option<FileEvent> {
self.event_receiver.try_recv().ok()
}
pub fn watched_path(&self) -> &Path {
&self.watched_path
}
pub fn config(&self) -> &WatcherConfig {
&self.config
}
}
pub struct FileWatcherIter<'a> {
watcher: &'a FileWatcher,
}
impl<'a> FileWatcherIter<'a> {
pub fn new(watcher: &'a FileWatcher) -> Self {
Self { watcher }
}
}
impl<'a> Iterator for FileWatcherIter<'a> {
type Item = FileEvent;
fn next(&mut self) -> Option<Self::Item> {
self.watcher.next_event().ok()
}
}
impl FileWatcher {
pub fn events(&self) -> FileWatcherIter<'_> {
FileWatcherIter::new(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_watcher_config_default() {
let config = WatcherConfig::default();
assert!(!config.ignore_patterns.is_empty());
assert_eq!(config.debounce_duration, Duration::from_secs(2));
}
#[test]
fn test_watcher_config_should_ignore() {
let config = WatcherConfig::default();
assert!(config.should_ignore(Path::new("/path/to/.git/config")));
assert!(config.should_ignore(Path::new("/path/node_modules/pkg")));
assert!(!config.should_ignore(Path::new("/path/to/file.txt")));
}
#[test]
fn test_file_event_needs_sync() {
let created = FileEvent::new(FileEventType::Created, PathBuf::from("test.txt"));
assert!(created.needs_sync());
let modified = FileEvent::new(FileEventType::Modified, PathBuf::from("test.txt"));
assert!(modified.needs_sync());
let deleted = FileEvent::new(FileEventType::Deleted, PathBuf::from("test.txt"));
assert!(!deleted.needs_sync());
}
#[test]
fn test_watcher_config_with_custom_debounce() {
let config = WatcherConfig::default().with_debounce(Duration::from_secs(5));
assert_eq!(config.debounce_duration, Duration::from_secs(5));
}
#[test]
fn test_watcher_nonexistent_path() {
let config = WatcherConfig::default();
let result = FileWatcher::new("/nonexistent/path", config);
assert!(result.is_err());
}
}