rust_filesearch/fs/
watch.rs

1#[cfg(feature = "watch")]
2use crate::errors::{FsError, Result};
3#[cfg(feature = "watch")]
4use crate::models::WatchEvent;
5#[cfg(feature = "watch")]
6use notify::{Event, EventKind, RecursiveMode, Watcher};
7#[cfg(feature = "watch")]
8use std::path::Path;
9#[cfg(feature = "watch")]
10use std::sync::mpsc::channel;
11#[cfg(feature = "watch")]
12use std::time::Duration;
13
14#[cfg(feature = "watch")]
15pub struct FileWatcher {
16    events: Vec<String>,
17}
18
19#[cfg(feature = "watch")]
20impl FileWatcher {
21    pub fn new(events: Vec<String>) -> Self {
22        Self { events }
23    }
24
25    pub fn watch<F>(&self, path: &Path, mut callback: F) -> Result<()>
26    where
27        F: FnMut(WatchEvent),
28    {
29        let (tx, rx) = channel();
30
31        let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
32            if let Ok(event) = res {
33                let _ = tx.send(event);
34            }
35        })
36        .map_err(|e| FsError::Watch(e.to_string()))?;
37
38        watcher
39            .watch(path, RecursiveMode::Recursive)
40            .map_err(|e| FsError::Watch(e.to_string()))?;
41
42        println!(
43            "Watching {} for changes... (Ctrl+C to stop)",
44            path.display()
45        );
46
47        loop {
48            match rx.recv_timeout(Duration::from_millis(100)) {
49                Ok(event) => {
50                    if let Some(watch_event) = self.process_event(event) {
51                        callback(watch_event);
52                    }
53                }
54                Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
55                Err(_) => break,
56            }
57        }
58
59        Ok(())
60    }
61
62    fn process_event(&self, event: Event) -> Option<WatchEvent> {
63        let event_type = match event.kind {
64            EventKind::Create(_) => "create",
65            EventKind::Modify(_) => "modify",
66            EventKind::Remove(_) => "remove",
67            _ => return None,
68        };
69
70        // Filter by requested event types
71        if !self.events.is_empty() && !self.events.contains(&event_type.to_string()) {
72            return None;
73        }
74
75        // Get the first path from the event
76        let path = event.paths.first()?.clone();
77
78        // Try to get metadata (may fail if file was removed)
79        let (mtime, size) = if let Ok(metadata) = std::fs::metadata(&path) {
80            let mtime = metadata.modified().ok().map(chrono::DateTime::from);
81            let size = Some(metadata.len());
82            (mtime, size)
83        } else {
84            (None, None)
85        };
86
87        Some(WatchEvent {
88            event: event_type.to_string(),
89            path,
90            mtime,
91            size,
92        })
93    }
94}
95
96#[cfg(not(feature = "watch"))]
97pub struct FileWatcher;
98
99#[cfg(not(feature = "watch"))]
100impl FileWatcher {
101    pub fn new(_events: Vec<String>) -> Self {
102        Self
103    }
104}