Skip to content

Commit 5f91c42

Browse files
riberkJohnTitor
andauthored
feat: rework PathsMut to be more consistent (#705)
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
1 parent a90cb87 commit 5f91c42

File tree

7 files changed

+575
-105
lines changed

7 files changed

+575
-105
lines changed

notify-debouncer-full/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
## debouncer-full 0.7.1 (unreleased)
33

44
- FEATURE: impl `EventHandler` for `futures::channel::mpsc::UnboundedSender` and `tokio::sync::mpsc::UnboundedSender` behind the `futures` and `tokio` feature flags [#767]
5+
- FEATURE: add support of a watcher's method `update_paths` [#705]
56

67
[#767]: https://github.com/notify-rs/notify/pull/767
8+
[#705]: https://github.com/notify-rs/notify/pull/705
79

810
## debouncer-full 0.7.0 (2026-01-23)
911

notify-debouncer-full/src/lib.rs

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ pub use notify_types::debouncer_full::DebouncedEvent;
9595
use file_id::FileId;
9696
use notify::{
9797
event::{ModifyKind, RemoveKind, RenameMode},
98-
Error, ErrorKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, WatcherKind,
98+
Error, ErrorKind, Event, EventKind, PathOp, RecommendedWatcher, RecursiveMode,
99+
UpdatePathsError, Watcher, WatcherKind,
99100
};
100101

101102
/// The set of requirements for watcher debounce event handling functions.
@@ -627,6 +628,76 @@ impl<T: Watcher, C: FileIdCache> Debouncer<T, C> {
627628
Ok(())
628629
}
629630

631+
/// Add/remove paths to watch in batch.
632+
///
633+
/// For some [`Watcher`] implementations this method provides better performance than multiple
634+
/// calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once.
635+
///
636+
/// # Errors
637+
///
638+
/// Returns [`UpdatePathsError`] if any operation fails. Operations are applied sequentially.
639+
/// When an error occurs, processing stops: operations before `origin` have been applied,
640+
/// `origin` is the operation that failed (if known), and `remaining` are the operations that
641+
/// were not attempted. `remaining` does not include `origin`.
642+
///
643+
/// # Examples
644+
///
645+
/// ```
646+
/// # use notify::{Watcher, RecursiveMode, PathOp};
647+
/// # use notify_debouncer_full::{RecommendedCache, new_debouncer_opt};
648+
/// # use std::path::{Path, PathBuf};
649+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
650+
/// # let mut debouncer = new_debouncer_opt::<_, notify::NullWatcher, _>(
651+
/// # std::time::Duration::from_secs(1),
652+
/// # None,
653+
/// # |e| {},
654+
/// # RecommendedCache::new(),
655+
/// # Default::default()
656+
/// # )?;
657+
/// debouncer.update_paths([
658+
/// PathOp::watch_recursive("path/to/file"),
659+
/// PathOp::unwatch("path/to/file2"),
660+
/// ])?;
661+
/// # Ok(())
662+
/// # }
663+
/// ```
664+
pub fn update_paths<Op: Into<PathOp>>(
665+
&mut self,
666+
ops: impl IntoIterator<Item = Op>,
667+
) -> std::result::Result<(), UpdatePathsError> {
668+
let mut paths = Vec::new();
669+
let ops: Vec<_> = ops
670+
.into_iter()
671+
.map(Into::into)
672+
.inspect(|op| {
673+
paths.push((
674+
op.as_path().to_path_buf(),
675+
match op {
676+
PathOp::Watch(_, config) => Some(config.recursive_mode()),
677+
PathOp::Unwatch(_) => None,
678+
},
679+
));
680+
})
681+
.collect();
682+
683+
let res = self.watcher.update_paths(ops);
684+
let updated_len = match res.as_ref() {
685+
Ok(()) => paths.len(),
686+
Err(e) => {
687+
let failed = usize::from(e.origin.is_some());
688+
paths.len().saturating_sub(e.remaining.len() + failed)
689+
}
690+
};
691+
let updated_paths = &paths[..updated_len];
692+
for (path, watch_mode) in updated_paths {
693+
match watch_mode {
694+
Some(recursive_mode) => self.add_root(path, *recursive_mode),
695+
None => self.remove_root(path),
696+
}
697+
}
698+
res
699+
}
700+
630701
pub fn configure(&mut self, option: notify::Config) -> notify::Result<bool> {
631702
self.watcher.configure(option)
632703
}
@@ -789,7 +860,10 @@ fn sort_events(events: Vec<DebouncedEvent>) -> Vec<DebouncedEvent> {
789860

790861
#[cfg(test)]
791862
mod tests {
792-
use std::{fs, path::Path};
863+
use std::{
864+
fs,
865+
path::{Path, PathBuf},
866+
};
793867

794868
use super::*;
795869

@@ -799,6 +873,42 @@ mod tests {
799873
use testing::TestCase;
800874
use time::MockTime;
801875

876+
#[derive(Debug)]
877+
struct FailingWatcher {
878+
fail_path: PathBuf,
879+
}
880+
881+
impl Watcher for FailingWatcher {
882+
fn new<F: notify::EventHandler>(
883+
_event_handler: F,
884+
_config: notify::Config,
885+
) -> notify::Result<Self> {
886+
Ok(Self {
887+
fail_path: PathBuf::from("bad"),
888+
})
889+
}
890+
891+
fn watch(&mut self, path: &Path, _recursive_mode: RecursiveMode) -> notify::Result<()> {
892+
if path == self.fail_path {
893+
Err(Error::path_not_found())
894+
} else {
895+
Ok(())
896+
}
897+
}
898+
899+
fn unwatch(&mut self, path: &Path) -> notify::Result<()> {
900+
if path == self.fail_path {
901+
Err(Error::path_not_found())
902+
} else {
903+
Ok(())
904+
}
905+
}
906+
907+
fn kind() -> WatcherKind {
908+
WatcherKind::NullWatcher
909+
}
910+
}
911+
802912
#[rstest]
803913
fn state(
804914
#[values(
@@ -1014,4 +1124,89 @@ mod tests {
10141124
.expect("No event")
10151125
.expect("error");
10161126
}
1127+
1128+
#[test]
1129+
fn update_paths() -> Result<(), Box<dyn std::error::Error>> {
1130+
let dir1 = tempdir()?;
1131+
let dir2 = tempdir()?;
1132+
1133+
// set up the watcher
1134+
let (tx, rx) = std::sync::mpsc::channel();
1135+
let mut debouncer = new_debouncer(Duration::from_millis(10), None, tx)?;
1136+
debouncer.update_paths([
1137+
PathOp::watch_recursive(dir1.path()),
1138+
PathOp::watch_recursive(dir2.path()),
1139+
])?;
1140+
1141+
// create a new file
1142+
let file_path1 = dir1.path().join("file.txt");
1143+
let file_path2 = dir2.path().join("file.txt");
1144+
fs::write(&file_path1, b"Lorem ipsum1")?;
1145+
fs::write(&file_path2, b"Lorem ipsum1")?;
1146+
1147+
println!(
1148+
"waiting for events at {:?} and {:?}",
1149+
file_path1, file_path2
1150+
);
1151+
1152+
// wait for up to 10 seconds for the create event, ignore all other events
1153+
let deadline = Instant::now() + Duration::from_secs(10);
1154+
let mut received = (false, false);
1155+
while deadline > Instant::now() {
1156+
let events = rx
1157+
.recv_timeout(deadline - Instant::now())
1158+
.expect("did not receive expected event")
1159+
.expect("received an error");
1160+
1161+
for event in events {
1162+
println!("event {event:?}");
1163+
if event.event.paths == vec![file_path1.clone()]
1164+
|| event.event.paths == vec![file_path1.canonicalize()?]
1165+
{
1166+
received.0 = true;
1167+
}
1168+
1169+
if event.event.paths == vec![file_path2.clone()]
1170+
|| event.event.paths == vec![file_path2.canonicalize()?]
1171+
{
1172+
received.1 = true;
1173+
}
1174+
1175+
if received == (true, true) {
1176+
return Ok(());
1177+
}
1178+
}
1179+
}
1180+
1181+
panic!("did not receive expected event");
1182+
}
1183+
1184+
#[test]
1185+
fn update_paths_error_does_not_add_failed_root() -> Result<(), Box<dyn std::error::Error>> {
1186+
let mut debouncer = new_debouncer_opt::<_, FailingWatcher, NoCache>(
1187+
Duration::from_millis(20),
1188+
Some(Duration::from_millis(5)),
1189+
|_| {},
1190+
NoCache::new(),
1191+
notify::Config::default(),
1192+
)?;
1193+
1194+
let err = debouncer
1195+
.update_paths([
1196+
PathOp::watch_recursive("ok1"),
1197+
PathOp::watch_recursive("bad"),
1198+
PathOp::watch_recursive("ok2"),
1199+
])
1200+
.unwrap_err();
1201+
assert!(err.origin.is_some());
1202+
assert_eq!(err.remaining.len(), 1);
1203+
1204+
let roots = debouncer.data.lock().unwrap().roots.clone();
1205+
assert_eq!(
1206+
roots,
1207+
vec![(PathBuf::from("ok1"), RecursiveMode::Recursive)]
1208+
);
1209+
1210+
Ok(())
1211+
}
10171212
}

notify/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## notify 9.0.0 (unreleased)
44

5+
- FEATURE: remove `Watcher::paths_mut` and introduce `update_paths` [#705]
6+
- FEATURE: impl `EventHandler` for `futures::channel::mpsc::UnboundedSender` and `tokio::sync::mpsc::UnboundedSender` behind the `futures` and `tokio` feature flags [#767]
7+
8+
[#705]: https://github.com/notify-rs/notify/pull/705
9+
[#767]: https://github.com/notify-rs/notify/pull/767
10+
511
## notify 9.0.0-rc.1 (2026-01-25)
612

713
> [!IMPORTANT]
@@ -15,15 +21,13 @@
1521
- FIX: Fix the bug that `INotifyWatcher` keeps watching deleted paths [#720]
1622
- FIX: Fixed ordering where `FsEventWatcher` emitted `Remove` events non-terminally [#747]
1723
- FIX: [macOS] throw `FsEventWatcher` stream start error properly [#733]
18-
- FEATURE: impl `EventHandler` for `futures::channel::mpsc::UnboundedSender` and `tokio::sync::mpsc::UnboundedSender` behind the `futures` and `tokio` feature flags [#767]
1924

2025
[#718]: https://github.com/notify-rs/notify/pull/718
2126
[#720]: https://github.com/notify-rs/notify/pull/720
2227
[#726]: https://github.com/notify-rs/notify/pull/726
2328
[#733]: https://github.com/notify-rs/notify/pull/733
2429
[#736]: https://github.com/notify-rs/notify/pull/736
2530
[#747]: https://github.com/notify-rs/notify/pull/747
26-
[#767]: https://github.com/notify-rs/notify/pull/767
2731

2832
## notify 8.2.0 (2025-08-03)
2933
- FEATURE: notify user if inotify's `max_user_watches` has been reached [#698]

notify/src/config.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
//! Configuration types
22
33
use notify_types::event::EventKindMask;
4-
use std::time::Duration;
4+
use std::{
5+
path::{Path, PathBuf},
6+
time::Duration,
7+
};
58

69
/// Indicates whether only the provided directory or its sub-directories as well should be watched
710
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
@@ -165,6 +168,81 @@ impl Default for Config {
165168
}
166169
}
167170

171+
/// Single watch backend configuration
172+
///
173+
/// This contains some settings that may relate to only one specific backend,
174+
/// such as to correctly configure each backend regardless of what is selected during runtime.
175+
#[derive(Debug)]
176+
pub struct WatchPathConfig {
177+
recursive_mode: RecursiveMode,
178+
}
179+
180+
impl WatchPathConfig {
181+
/// Creates new instance with provided [`RecursiveMode`]
182+
pub fn new(recursive_mode: RecursiveMode) -> Self {
183+
Self { recursive_mode }
184+
}
185+
186+
/// Set [`RecursiveMode`] for the watch
187+
pub fn with_recursive_mode(mut self, recursive_mode: RecursiveMode) -> Self {
188+
self.recursive_mode = recursive_mode;
189+
self
190+
}
191+
192+
/// Returns current setting
193+
pub fn recursive_mode(&self) -> RecursiveMode {
194+
self.recursive_mode
195+
}
196+
}
197+
198+
/// An operation to apply to a watcher
199+
///
200+
/// See [`Watcher::update_paths`] for more information
201+
#[derive(Debug)]
202+
pub enum PathOp {
203+
/// Path should be watched
204+
Watch(PathBuf, WatchPathConfig),
205+
206+
/// Path should be unwatched
207+
Unwatch(PathBuf),
208+
}
209+
210+
impl PathOp {
211+
/// Watch the path with [`RecursiveMode::Recursive`]
212+
pub fn watch_recursive<P: Into<PathBuf>>(path: P) -> Self {
213+
Self::Watch(path.into(), WatchPathConfig::new(RecursiveMode::Recursive))
214+
}
215+
216+
/// Watch the path with [`RecursiveMode::NonRecursive`]
217+
pub fn watch_non_recursive<P: Into<PathBuf>>(path: P) -> Self {
218+
Self::Watch(
219+
path.into(),
220+
WatchPathConfig::new(RecursiveMode::NonRecursive),
221+
)
222+
}
223+
224+
/// Unwatch the path
225+
pub fn unwatch<P: Into<PathBuf>>(path: P) -> Self {
226+
Self::Unwatch(path.into())
227+
}
228+
229+
/// Returns the path associated with this operation.
230+
pub fn as_path(&self) -> &Path {
231+
match self {
232+
PathOp::Watch(p, _) => p,
233+
PathOp::Unwatch(p) => p,
234+
}
235+
}
236+
237+
/// Returns the path associated with this operation.
238+
pub fn into_path(self) -> PathBuf {
239+
match self {
240+
PathOp::Watch(p, _) => p,
241+
PathOp::Unwatch(p) => p,
242+
}
243+
}
244+
}
245+
168246
#[cfg(test)]
169247
mod tests {
170248
use super::*;

0 commit comments

Comments
 (0)