srusty-files 0.2.0

A high-performance, cross-platform file search engine library with REST API
Documentation
use crate::core::config::SearchConfig;
use crate::core::error::Result;
use crate::filters::ExclusionFilter;
use crate::storage::Database;
use crate::watcher::debouncer::{EventDebouncer, FileEventType};
use crate::watcher::synchronizer::{FileEvent, IndexSynchronizer};
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::mpsc;

pub struct FileSystemMonitor {
    exclusion_filter: Arc<ExclusionFilter>,
    synchronizer: Arc<IndexSynchronizer>,
    debouncer: Arc<EventDebouncer>,
    is_running: Arc<AtomicBool>,
    watcher: Option<RecommendedWatcher>,
}

impl FileSystemMonitor {
    pub fn new(
        database: Arc<Database>,
        config: Arc<SearchConfig>,
        exclusion_filter: Arc<ExclusionFilter>,
    ) -> Self {
        let synchronizer = Arc::new(IndexSynchronizer::new(
            database,
            Arc::clone(&config),
            Arc::clone(&exclusion_filter),
        ));

        let debouncer = Arc::new(EventDebouncer::new(config.watch_debounce_ms));

        Self {
            exclusion_filter,
            synchronizer,
            debouncer,
            is_running: Arc::new(AtomicBool::new(false)),
            watcher: None,
        }
    }

    pub fn start<P: AsRef<Path>>(&mut self, root: P) -> Result<()> {
        if self.is_running.load(Ordering::Relaxed) {
            return Ok(());
        }

        let sender = self.synchronizer.get_sender();
        let debouncer = Arc::clone(&self.debouncer);
        let exclusion_filter = Arc::clone(&self.exclusion_filter);

        let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
            if let Ok(event) = res {
                Self::handle_notify_event(event, &sender, &debouncer, &exclusion_filter);
            }
        })?;

        watcher.watch(root.as_ref(), RecursiveMode::Recursive)?;

        self.watcher = Some(watcher);
        self.is_running.store(true, Ordering::Relaxed);

        Ok(())
    }

    pub fn stop(&mut self) -> Result<()> {
        if !self.is_running.load(Ordering::Relaxed) {
            return Ok(());
        }

        self.watcher = None;
        self.is_running.store(false, Ordering::Relaxed);

        Ok(())
    }

    pub fn is_running(&self) -> bool {
        self.is_running.load(Ordering::Relaxed)
    }

    fn handle_notify_event(
        event: Event,
        sender: &mpsc::UnboundedSender<FileEvent>,
        debouncer: &Arc<EventDebouncer>,
        exclusion_filter: &Arc<ExclusionFilter>,
    ) {
        let event_type = match event.kind {
            EventKind::Create(_) => FileEventType::Created,
            EventKind::Modify(_) => FileEventType::Modified,
            EventKind::Remove(_) => FileEventType::Deleted,
            EventKind::Any => FileEventType::Modified,
            _ => return,
        };

        for path in event.paths {
            if exclusion_filter.is_excluded(&path) {
                continue;
            }

            if !debouncer.should_process(path.clone(), event_type) {
                continue;
            }

            let file_event = FileEvent { path, event_type };

            if sender.send(file_event).is_err() {
                log::error!("Failed to send file event to synchronizer");
            }
        }
    }

    pub async fn run_cleanup_task(&self) {
        use tokio::time::{interval, Duration};

        let mut interval = interval(Duration::from_secs(60));
        let debouncer = Arc::clone(&self.debouncer);

        loop {
            interval.tick().await;
            debouncer.cleanup_old_events(Duration::from_secs(300));
        }
    }
}

impl Drop for FileSystemMonitor {
    fn drop(&mut self) {
        let _ = self.stop();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn test_monitor_creation() {
        let db = Arc::new(Database::in_memory(10).unwrap());
        let config = Arc::new(SearchConfig::default());
        let filter = Arc::new(ExclusionFilter::default());

        let monitor = FileSystemMonitor::new(db, config, filter);
        assert!(!monitor.is_running());
    }

    #[test]
    fn test_monitor_start_stop() {
        let temp_dir = TempDir::new().unwrap();

        let db = Arc::new(Database::in_memory(10).unwrap());
        let config = Arc::new(SearchConfig::default());
        let filter = Arc::new(ExclusionFilter::default());

        let mut monitor = FileSystemMonitor::new(db, config, filter);

        assert!(monitor.start(temp_dir.path()).is_ok());
        assert!(monitor.is_running());

        assert!(monitor.stop().is_ok());
        assert!(!monitor.is_running());
    }
}