1use crate::ignore::IgnoreFilter;
12use crate::WatchEvent;
13use notify::{Event, EventKind, RecursiveMode, Watcher};
14use std::path::Path;
15use std::sync::Arc;
16use tokio::sync::mpsc;
17
18pub struct NotifyWatcher {
23 watcher: notify::RecommendedWatcher,
24}
25
26impl std::fmt::Debug for NotifyWatcher {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("NotifyWatcher").finish_non_exhaustive()
29 }
30}
31
32impl NotifyWatcher {
33 pub fn new(
42 ignore: Arc<IgnoreFilter>,
43 ) -> zccache_core::Result<(Self, mpsc::UnboundedReceiver<WatchEvent>)> {
44 let (tx, rx) = mpsc::unbounded_channel();
45
46 let watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
47 match res {
48 Ok(event) => {
49 for watch_event in convert_event(&ignore, &event) {
50 if tx.send(watch_event).is_err() {
51 return;
53 }
54 }
55 }
56 Err(e) => {
57 tracing::warn!("watcher error: {e}");
58 let _ = tx.send(WatchEvent::Error(e.to_string()));
59 }
60 }
61 })
62 .map_err(|e| std::io::Error::other(e.to_string()))?;
63
64 Ok((Self { watcher }, rx))
65 }
66
67 pub fn watch(&mut self, path: &Path) -> zccache_core::Result<()> {
77 self.watcher
78 .watch(path, RecursiveMode::NonRecursive)
79 .map_err(|e| std::io::Error::other(e.to_string()))?;
80 Ok(())
81 }
82
83 pub fn watch_recursive(&mut self, path: &Path) -> zccache_core::Result<()> {
91 self.watcher
92 .watch(path, RecursiveMode::Recursive)
93 .map_err(|e| std::io::Error::other(e.to_string()))?;
94 Ok(())
95 }
96
97 pub fn unwatch(&mut self, path: &Path) -> zccache_core::Result<()> {
103 self.watcher
104 .unwatch(path)
105 .map_err(|e| std::io::Error::other(e.to_string()))?;
106 Ok(())
107 }
108}
109
110fn convert_event(ignore: &IgnoreFilter, event: &Event) -> Vec<WatchEvent> {
112 if event.need_rescan() {
117 return vec![WatchEvent::Overflow];
118 }
119
120 if matches!(
122 event.kind,
123 EventKind::Modify(notify::event::ModifyKind::Name(
124 notify::event::RenameMode::Both
125 ))
126 ) && event.paths.len() >= 2
127 {
128 let from = &event.paths[0];
129 let to = &event.paths[1];
130 let from_ignored = ignore.should_ignore(from);
131 let to_ignored = ignore.should_ignore(to);
132 if from_ignored && to_ignored {
133 return vec![];
134 }
135 if from_ignored {
136 return vec![WatchEvent::Created(to.as_path().into())];
138 }
139 if to_ignored {
140 return vec![WatchEvent::Removed(from.as_path().into())];
142 }
143 return vec![WatchEvent::Renamed {
144 from: from.as_path().into(),
145 to: to.as_path().into(),
146 }];
147 }
148
149 let mut result = Vec::new();
150 for path in &event.paths {
151 if ignore.should_ignore(path) {
152 continue;
153 }
154
155 let watch_event = match event.kind {
156 EventKind::Create(_) => WatchEvent::Created(path.as_path().into()),
157 EventKind::Remove(_) => WatchEvent::Removed(path.as_path().into()),
158 EventKind::Modify(notify::event::ModifyKind::Name(notify::event::RenameMode::From)) => {
159 WatchEvent::Removed(path.as_path().into())
161 }
162 EventKind::Modify(notify::event::ModifyKind::Name(notify::event::RenameMode::To)) => {
163 WatchEvent::Created(path.as_path().into())
165 }
166 EventKind::Modify(_) => WatchEvent::Modified(path.as_path().into()),
167 EventKind::Access(_) => continue,
168 _ => WatchEvent::Modified(path.as_path().into()),
170 };
171
172 result.push(watch_event);
173 }
174
175 result
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn test_filter() -> IgnoreFilter {
183 IgnoreFilter::new(vec![".git".to_string(), "target".to_string()])
184 }
185
186 #[test]
187 fn convert_create_event() {
188 let filter = test_filter();
189 let event = Event {
190 kind: EventKind::Create(notify::event::CreateKind::File),
191 paths: vec![Path::new("src/main.rs").to_owned()],
192 attrs: Default::default(),
193 };
194 let result = convert_event(&filter, &event);
195 assert_eq!(result.len(), 1);
196 assert!(
197 matches!(&result[0], WatchEvent::Created(p) if p.as_path() == Path::new("src/main.rs"))
198 );
199 }
200
201 #[test]
202 fn convert_modify_event() {
203 let filter = test_filter();
204 let event = Event {
205 kind: EventKind::Modify(notify::event::ModifyKind::Data(
206 notify::event::DataChange::Content,
207 )),
208 paths: vec![Path::new("src/lib.rs").to_owned()],
209 attrs: Default::default(),
210 };
211 let result = convert_event(&filter, &event);
212 assert_eq!(result.len(), 1);
213 assert!(
214 matches!(&result[0], WatchEvent::Modified(p) if p.as_path() == Path::new("src/lib.rs"))
215 );
216 }
217
218 #[test]
219 fn convert_remove_event() {
220 let filter = test_filter();
221 let event = Event {
222 kind: EventKind::Remove(notify::event::RemoveKind::File),
223 paths: vec![Path::new("old.c").to_owned()],
224 attrs: Default::default(),
225 };
226 let result = convert_event(&filter, &event);
227 assert_eq!(result.len(), 1);
228 assert!(matches!(&result[0], WatchEvent::Removed(p) if p.as_path() == Path::new("old.c")));
229 }
230
231 #[test]
232 fn convert_rename_both() {
233 let filter = test_filter();
234 let event = Event {
235 kind: EventKind::Modify(notify::event::ModifyKind::Name(
236 notify::event::RenameMode::Both,
237 )),
238 paths: vec![Path::new("old.c").to_owned(), Path::new("new.c").to_owned()],
239 attrs: Default::default(),
240 };
241 let result = convert_event(&filter, &event);
242 assert_eq!(result.len(), 1);
243 assert!(matches!(
244 &result[0],
245 WatchEvent::Renamed { from, to }
246 if from.as_path() == Path::new("old.c") && to.as_path() == Path::new("new.c")
247 ));
248 }
249
250 #[test]
251 fn convert_rename_from_becomes_removed() {
252 let filter = test_filter();
253 let event = Event {
254 kind: EventKind::Modify(notify::event::ModifyKind::Name(
255 notify::event::RenameMode::From,
256 )),
257 paths: vec![Path::new("gone.c").to_owned()],
258 attrs: Default::default(),
259 };
260 let result = convert_event(&filter, &event);
261 assert_eq!(result.len(), 1);
262 assert!(matches!(&result[0], WatchEvent::Removed(p) if p.as_path() == Path::new("gone.c")));
263 }
264
265 #[test]
266 fn convert_rename_to_becomes_created() {
267 let filter = test_filter();
268 let event = Event {
269 kind: EventKind::Modify(notify::event::ModifyKind::Name(
270 notify::event::RenameMode::To,
271 )),
272 paths: vec![Path::new("appeared.c").to_owned()],
273 attrs: Default::default(),
274 };
275 let result = convert_event(&filter, &event);
276 assert_eq!(result.len(), 1);
277 assert!(
278 matches!(&result[0], WatchEvent::Created(p) if p.as_path() == Path::new("appeared.c"))
279 );
280 }
281
282 #[test]
283 fn ignored_paths_filtered_out() {
284 let filter = test_filter();
285 let event = Event {
286 kind: EventKind::Modify(notify::event::ModifyKind::Data(
287 notify::event::DataChange::Content,
288 )),
289 paths: vec![Path::new("project/.git/index").to_owned()],
290 attrs: Default::default(),
291 };
292 let result = convert_event(&filter, &event);
293 assert!(result.is_empty());
294 }
295
296 #[test]
297 fn ignored_rename_both_filtered() {
298 let filter = test_filter();
299 let event = Event {
300 kind: EventKind::Modify(notify::event::ModifyKind::Name(
301 notify::event::RenameMode::Both,
302 )),
303 paths: vec![
304 Path::new("project/.git/old").to_owned(),
305 Path::new("project/.git/new").to_owned(),
306 ],
307 attrs: Default::default(),
308 };
309 let result = convert_event(&filter, &event);
310 assert!(result.is_empty());
311 }
312
313 #[test]
314 fn rename_from_ignored_to_visible_becomes_created() {
315 let filter = test_filter();
317 let event = Event {
318 kind: EventKind::Modify(notify::event::ModifyKind::Name(
319 notify::event::RenameMode::Both,
320 )),
321 paths: vec![
322 Path::new("project/.git/stash").to_owned(),
323 Path::new("src/recovered.c").to_owned(),
324 ],
325 attrs: Default::default(),
326 };
327 let result = convert_event(&filter, &event);
328 assert_eq!(result.len(), 1);
329 assert!(
330 matches!(&result[0], WatchEvent::Created(p) if p.as_path() == Path::new("src/recovered.c"))
331 );
332 }
333
334 #[test]
335 fn rename_from_visible_to_ignored_becomes_removed() {
336 let filter = test_filter();
338 let event = Event {
339 kind: EventKind::Modify(notify::event::ModifyKind::Name(
340 notify::event::RenameMode::Both,
341 )),
342 paths: vec![
343 Path::new("src/main.rs").to_owned(),
344 Path::new("project/.git/stash").to_owned(),
345 ],
346 attrs: Default::default(),
347 };
348 let result = convert_event(&filter, &event);
349 assert_eq!(result.len(), 1);
350 assert!(
351 matches!(&result[0], WatchEvent::Removed(p) if p.as_path() == Path::new("src/main.rs"))
352 );
353 }
354
355 #[test]
356 fn access_events_ignored() {
357 let filter = test_filter();
358 let event = Event {
359 kind: EventKind::Access(notify::event::AccessKind::Read),
360 paths: vec![Path::new("src/main.rs").to_owned()],
361 attrs: Default::default(),
362 };
363 let result = convert_event(&filter, &event);
364 assert!(result.is_empty());
365 }
366
367 #[test]
368 fn rename_both_with_single_path_falls_through() {
369 let filter = test_filter();
371 let event = Event {
372 kind: EventKind::Modify(notify::event::ModifyKind::Name(
373 notify::event::RenameMode::Both,
374 )),
375 paths: vec![Path::new("only_one.c").to_owned()],
376 attrs: Default::default(),
377 };
378 let result = convert_event(&filter, &event);
379 assert_eq!(result.len(), 1);
381 assert!(matches!(&result[0], WatchEvent::Modified(_)));
382 }
383
384 #[test]
385 fn event_with_empty_paths() {
386 let filter = test_filter();
387 let event = Event {
388 kind: EventKind::Modify(notify::event::ModifyKind::Data(
389 notify::event::DataChange::Content,
390 )),
391 paths: vec![],
392 attrs: Default::default(),
393 };
394 let result = convert_event(&filter, &event);
395 assert!(result.is_empty());
396 }
397
398 #[test]
399 fn event_kind_other_becomes_modified() {
400 let filter = test_filter();
401 let event = Event {
402 kind: EventKind::Other,
403 paths: vec![Path::new("mystery.c").to_owned()],
404 attrs: Default::default(),
405 };
406 let result = convert_event(&filter, &event);
407 assert_eq!(result.len(), 1);
408 assert!(matches!(&result[0], WatchEvent::Modified(_)));
409 }
410
411 #[test]
412 fn event_kind_any_becomes_modified() {
413 let filter = test_filter();
414 let event = Event {
415 kind: EventKind::Any,
416 paths: vec![Path::new("any.c").to_owned()],
417 attrs: Default::default(),
418 };
419 let result = convert_event(&filter, &event);
420 assert_eq!(result.len(), 1);
421 assert!(matches!(&result[0], WatchEvent::Modified(_)));
422 }
423
424 #[test]
425 fn remove_directory_event() {
426 let filter = test_filter();
427 let event = Event {
428 kind: EventKind::Remove(notify::event::RemoveKind::Folder),
429 paths: vec![Path::new("src/old_module").to_owned()],
430 attrs: Default::default(),
431 };
432 let result = convert_event(&filter, &event);
433 assert_eq!(result.len(), 1);
434 assert!(matches!(&result[0], WatchEvent::Removed(_)));
435 }
436
437 #[test]
438 fn create_directory_event() {
439 let filter = test_filter();
440 let event = Event {
441 kind: EventKind::Create(notify::event::CreateKind::Folder),
442 paths: vec![Path::new("src/new_module").to_owned()],
443 attrs: Default::default(),
444 };
445 let result = convert_event(&filter, &event);
446 assert_eq!(result.len(), 1);
447 assert!(matches!(&result[0], WatchEvent::Created(_)));
448 }
449
450 #[test]
451 fn metadata_change_becomes_modified() {
452 let filter = test_filter();
453 let event = Event {
454 kind: EventKind::Modify(notify::event::ModifyKind::Metadata(
455 notify::event::MetadataKind::Permissions,
456 )),
457 paths: vec![Path::new("script.sh").to_owned()],
458 attrs: Default::default(),
459 };
460 let result = convert_event(&filter, &event);
461 assert_eq!(result.len(), 1);
462 assert!(matches!(&result[0], WatchEvent::Modified(_)));
463 }
464
465 #[test]
466 fn notify_watcher_can_be_created() {
467 use std::sync::Arc;
468
469 let ignore = Arc::new(IgnoreFilter::default());
470 let result = NotifyWatcher::new(ignore);
471 assert!(result.is_ok());
472
473 let (mut watcher, _rx) = result.unwrap();
474 let dir = tempfile::TempDir::new().unwrap();
476 assert!(watcher.watch(dir.path()).is_ok());
477 assert!(watcher.unwatch(dir.path()).is_ok());
478 }
479
480 #[test]
481 fn notify_watcher_watch_nonexistent_fails() {
482 use std::sync::Arc;
483
484 let ignore = Arc::new(IgnoreFilter::default());
485 let (mut watcher, _rx) = NotifyWatcher::new(ignore).unwrap();
486 let result = watcher.watch(Path::new("/no/such/directory/ever"));
487 assert!(result.is_err());
488 }
489
490 #[test]
491 fn notify_watcher_debug_impl() {
492 use std::sync::Arc;
493 let ignore = Arc::new(IgnoreFilter::default());
494 let (watcher, _rx) = NotifyWatcher::new(ignore).unwrap();
495 let debug = format!("{watcher:?}");
496 assert!(debug.contains("NotifyWatcher"));
497 }
498
499 #[test]
500 fn rescan_flag_produces_overflow() {
501 use notify::event::Flag;
502
503 let filter = test_filter();
504 let event = Event::new(EventKind::Other).set_flag(Flag::Rescan);
507 let result = convert_event(&filter, &event);
508 assert_eq!(result.len(), 1);
509 assert!(matches!(&result[0], WatchEvent::Overflow));
510 }
511
512 #[test]
513 fn rescan_flag_with_paths_still_produces_overflow() {
514 use notify::event::Flag;
515
516 let filter = test_filter();
517 let mut event = Event::new(EventKind::Other).set_flag(Flag::Rescan);
520 event.paths = vec![Path::new("src/main.rs").to_owned()];
521 let result = convert_event(&filter, &event);
522 assert_eq!(result.len(), 1);
523 assert!(matches!(&result[0], WatchEvent::Overflow));
524 }
525
526 #[test]
527 fn mixed_paths_filter_individually() {
528 let filter = test_filter();
529 let event = Event {
530 kind: EventKind::Modify(notify::event::ModifyKind::Data(
531 notify::event::DataChange::Content,
532 )),
533 paths: vec![
534 Path::new("src/main.rs").to_owned(),
535 Path::new("target/debug/binary").to_owned(),
536 Path::new("src/lib.rs").to_owned(),
537 ],
538 attrs: Default::default(),
539 };
540 let result = convert_event(&filter, &event);
541 assert_eq!(result.len(), 2);
542 }
543}