simple_fs/
watch.rs

1use crate::{Error, Result, SPath};
2use notify::{self, RecommendedWatcher, RecursiveMode};
3use notify_debouncer_full::{DebounceEventHandler, DebounceEventResult, Debouncer, RecommendedCache, new_debouncer};
4use std::path::Path;
5// use std::sync::mpsc::{channel, Receiver, Sender};
6use std::time::Duration;
7
8// -- Re-export some DebouncedEvent
9use flume::{Receiver, Sender};
10pub use notify_debouncer_full::DebouncedEvent;
11use std::collections::HashSet;
12
13const WATCH_DEBOUNCE_MS: u64 = 200;
14
15// region:    --- SimpleEvent
16
17/// A greatly simplified file event struct, containing only one path and one simplified event kind.
18/// Additionally, these will be debounced on top of the debouncer to ensure only one path/kind per debounced event list.
19#[derive(Debug)]
20pub struct SEvent {
21	pub spath: SPath,
22	pub skind: SEventKind,
23}
24
25/// Simplified event kind.
26#[derive(Debug, Clone, Eq, Hash, PartialEq)]
27pub enum SEventKind {
28	Create,
29	Modify,
30	Remove,
31	Other,
32}
33
34impl From<notify::EventKind> for SEventKind {
35	fn from(val: notify::EventKind) -> Self {
36		match val {
37			notify::EventKind::Any => SEventKind::Other,
38			notify::EventKind::Access(_) => SEventKind::Other,
39			notify::EventKind::Create(_) => SEventKind::Create,
40			notify::EventKind::Modify(_) => SEventKind::Modify,
41			notify::EventKind::Remove(_) => SEventKind::Remove,
42			notify::EventKind::Other => SEventKind::Other,
43		}
44	}
45}
46
47/// A simplified watcher struct containing a receiver for file system events and an internal debouncer.
48#[allow(unused)]
49pub struct SWatcher {
50	pub rx: Receiver<Vec<SEvent>>,
51	// Note: Here we keep the debouncer so that it does not get dropped and continues to run.
52	notify_full_debouncer: Debouncer<RecommendedWatcher, RecommendedCache>,
53}
54
55// endregion: --- SimpleEvent
56
57/// A simplified watcher that monitors a path (file or directory) and returns an `SWatcher` object with a
58/// standard mpsc Receiver for a `Vec<SEvent>`.
59/// Each `SEvent` contains one `spath` and one simplified event kind (`SEventKind`).
60/// This will ignore any path that cannot be converted to a string (i.e., it will only trigger events if the path is valid UTF-8)
61pub fn watch(path: impl AsRef<Path>) -> Result<SWatcher> {
62	let (tx, rx) = flume::unbounded();
63
64	let path = path.as_ref();
65	let handler = EventHandler { tx };
66	let mut debouncer =
67		new_debouncer(Duration::from_millis(WATCH_DEBOUNCE_MS), None, handler).map_err(|err| Error::FailToWatch {
68			path: path.to_string_lossy().to_string(),
69			cause: err.to_string(),
70		})?;
71
72	if !path.exists() {
73		return Err(Error::CantWatchPathNotFound(path.to_string_lossy().to_string()));
74	}
75
76	debouncer
77		.watch(path, RecursiveMode::Recursive)
78		.map_err(|err| Error::FailToWatch {
79			path: path.to_string_lossy().to_string(),
80			cause: err.to_string(),
81		})?;
82
83	let swatcher = SWatcher {
84		rx,
85		notify_full_debouncer: debouncer,
86	};
87
88	Ok(swatcher)
89}
90
91/// Event Handler that propagates a simplified Vec<SEvent>
92struct EventHandler {
93	tx: Sender<Vec<SEvent>>,
94}
95
96impl DebounceEventHandler for EventHandler {
97	fn handle_event(&mut self, result: DebounceEventResult) {
98		match result {
99			Ok(events) => {
100				let sevents = build_sevents(events);
101				if !sevents.is_empty() {
102					let _ = self.tx.send(sevents);
103				}
104			}
105			Err(err) => println!("simple-fs - handle_event error {err:?}"), // may want to trace
106		}
107	}
108}
109
110#[derive(Hash, Eq, PartialEq)]
111struct SEventKey {
112	spath_string: String,
113	skind: SEventKind,
114}
115
116fn build_sevents(events: Vec<DebouncedEvent>) -> Vec<SEvent> {
117	let mut sevents_set: HashSet<SEventKey> = HashSet::new();
118
119	let mut sevents = Vec::new();
120
121	for devent in events {
122		let event = devent.event;
123		let skind = SEventKind::from(event.kind);
124
125		for path in event.paths {
126			if let Some(spath) = SPath::from_std_path_buf_ok(path) {
127				let key = SEventKey {
128					spath_string: spath.to_string(),
129					skind: skind.clone(),
130				};
131
132				// If this spath/skind is not in the set, then add it to the sevents list
133				if !sevents_set.contains(&key) {
134					sevents.push(SEvent {
135						spath,
136						skind: skind.clone(),
137					});
138
139					sevents_set.insert(key);
140				}
141			}
142		}
143	}
144
145	sevents
146}