fs_change_detector/
lib.rs1use message_channel::{Channel, Receiver};
6use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
7use std::path::{Path, PathBuf};
8use std::time::{Duration, Instant};
9use notify::event::ModifyKind;
10use thiserror::Error;
11use tracing::{debug, error};
12use notify::{Event, Result as NotifyResult};
13#[derive(Debug)]
14pub enum ChangeMessage {
15 SomeKindOfChange,
16}
17
18#[derive(Error, Debug)]
19pub enum FileWatcherError {
20 #[error("Filesystem I/O error: {0}")]
21 IoError(String),
22
23 #[error("Watch path not found: '{0}'")]
24 PathNotFound(PathBuf),
25
26 #[error("System watcher limit reached (too many watched files/directories)")]
27 TooManyWatches,
28
29 #[error("Invalid watcher configuration: {0:?}")]
30 InvalidWatcherConfig(Config),
31
32 #[error("Generic watcher error: {0}")]
33 WatcherGenericError(String),
34
35 #[error("Internal channel communication error: {0}")]
36 InternalChannelError(String),
37
38 #[error("Attempted to remove a watch that does not exist for path: '{0}'")]
39 WatchNotFound(PathBuf),
40}
41
42fn map_notify_error_to_file_watcher_error(e: notify::Error, path: &Path) -> FileWatcherError {
43 use notify::ErrorKind;
44 match e.kind {
45 ErrorKind::PathNotFound => FileWatcherError::PathNotFound(path.to_path_buf()),
46 ErrorKind::MaxFilesWatch => FileWatcherError::TooManyWatches,
47 ErrorKind::Generic(msg) => FileWatcherError::WatcherGenericError(msg),
48 ErrorKind::InvalidConfig(config) => FileWatcherError::InvalidWatcherConfig(config),
49 ErrorKind::WatchNotFound => FileWatcherError::WatchNotFound(path.to_path_buf()),
50
51 ErrorKind::Io(io_err) => FileWatcherError::IoError(io_err.to_string()),
52 }
53}
54
55#[derive(Debug)]
56pub struct FileWatcher {
57 pub receiver: Receiver<ChangeMessage>,
58 pub watcher: RecommendedWatcher, }
60
61impl FileWatcher {
62 pub fn new(watch_path: &Path) -> Result<Self, FileWatcherError> {
65 let (watcher, receiver) = start_watch(watch_path)?;
66 while let Ok(_found) = receiver.recv() {
67 }
68 Ok(Self { receiver, watcher })
69 }
70
71 #[must_use]
72 pub fn has_changed(&self) -> bool {
73 let mut result = false;
74 while let Ok(_found) = self.receiver.recv() {
75 result = true;
76 }
77
78 result
79 }
80}
81
82pub fn start_watch(
88 watch_path: &Path,
89) -> Result<(RecommendedWatcher, Receiver<ChangeMessage>), FileWatcherError> {
90 let (sender, receiver) = Channel::create();
91
92 let mut last_event = Instant::now().checked_sub(Duration::from_secs(1)).unwrap();
93 let debounce_duration = Duration::from_millis(100);
94
95 let owned_watch_path = watch_path.to_path_buf();
96
97 let mut watcher = notify::recommended_watcher(move |res: NotifyResult<Event> | match res {
98 Ok(event) if matches!(event.kind,
99 EventKind::Modify(ModifyKind::Data(_))
100 | EventKind::Modify(ModifyKind::Any)
101 ) =>
102 {
103 let now = Instant::now();
104 if now.duration_since(last_event) >= debounce_duration {
105 if let Err(e) = sender.send(ChangeMessage::SomeKindOfChange) {
106 error!(
107 error = ?e,
108 "FileWatcher internal channel send error: receiver likely dropped"
109 );
110 }
111 last_event = now;
112 }
113 }
114 Ok(_) => {
115 }
117
118 Err(e) => {
119 error!(
120 error = ?e,
121 path = ?owned_watch_path,
122 "FileWatcher internal background watch error"
123 );
124 }
125 })
126 .map_err(|e| {
127 error!(error = ?e, path = ?watch_path, "Failed to initialize watcher");
128 map_notify_error_to_file_watcher_error(e, watch_path)
129 })?;
130
131 watcher
132 .watch(watch_path, RecursiveMode::Recursive)
133 .map_err(|e| {
134 error!(error = ?e, path = ?watch_path, "Failed to start watching path");
135 map_notify_error_to_file_watcher_error(e, watch_path)
136 })?;
137
138 debug!(path = ?watch_path, "Successfully started file watcher");
139
140 Ok((watcher, receiver))
141}