Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions notify-debouncer-full/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
## debouncer-full 0.7.1 (unreleased)

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

[#767]: https://github.com/notify-rs/notify/pull/767
[#705]: https://github.com/notify-rs/notify/pull/705

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

Expand Down
199 changes: 197 additions & 2 deletions notify-debouncer-full/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ pub use notify_types::debouncer_full::DebouncedEvent;
use file_id::FileId;
use notify::{
event::{ModifyKind, RemoveKind, RenameMode},
Error, ErrorKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, WatcherKind,
Error, ErrorKind, Event, EventKind, PathOp, RecommendedWatcher, RecursiveMode,
UpdatePathsError, Watcher, WatcherKind,
};

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

/// Add/remove paths to watch in batch.
///
/// For some [`Watcher`] implementations this method provides better performance than multiple
/// calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once.
///
/// # Errors
///
/// Returns [`UpdatePathsError`] if any operation fails. Operations are applied sequentially.
/// When an error occurs, processing stops: operations before `origin` have been applied,
/// `origin` is the operation that failed (if known), and `remaining` are the operations that
/// were not attempted. `remaining` does not include `origin`.
///
/// # Examples
///
/// ```
/// # use notify::{Watcher, RecursiveMode, PathOp};
/// # use notify_debouncer_full::{RecommendedCache, new_debouncer_opt};
/// # use std::path::{Path, PathBuf};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let mut debouncer = new_debouncer_opt::<_, notify::NullWatcher, _>(
/// # std::time::Duration::from_secs(1),
/// # None,
/// # |e| {},
/// # RecommendedCache::new(),
/// # Default::default()
/// # )?;
/// debouncer.update_paths([
/// PathOp::watch_recursive("path/to/file"),
/// PathOp::unwatch("path/to/file2"),
/// ])?;
/// # Ok(())
/// # }
/// ```
pub fn update_paths<Op: Into<PathOp>>(
&mut self,
ops: impl IntoIterator<Item = Op>,
) -> std::result::Result<(), UpdatePathsError> {
let mut paths = Vec::new();
let ops: Vec<_> = ops
.into_iter()
.map(Into::into)
.inspect(|op| {
paths.push((
op.as_path().to_path_buf(),
match op {
PathOp::Watch(_, config) => Some(config.recursive_mode()),
PathOp::Unwatch(_) => None,
},
));
})
.collect();

let res = self.watcher.update_paths(ops);
let updated_len = match res.as_ref() {
Ok(()) => paths.len(),
Err(e) => {
let failed = usize::from(e.origin.is_some());
paths.len().saturating_sub(e.remaining.len() + failed)
}
};
let updated_paths = &paths[..updated_len];
for (path, watch_mode) in updated_paths {
match watch_mode {
Some(recursive_mode) => self.add_root(path, *recursive_mode),
None => self.remove_root(path),
}
}
res
}

pub fn configure(&mut self, option: notify::Config) -> notify::Result<bool> {
self.watcher.configure(option)
}
Expand Down Expand Up @@ -789,7 +860,10 @@ fn sort_events(events: Vec<DebouncedEvent>) -> Vec<DebouncedEvent> {

#[cfg(test)]
mod tests {
use std::{fs, path::Path};
use std::{
fs,
path::{Path, PathBuf},
};

use super::*;

Expand All @@ -799,6 +873,42 @@ mod tests {
use testing::TestCase;
use time::MockTime;

#[derive(Debug)]
struct FailingWatcher {
fail_path: PathBuf,
}

impl Watcher for FailingWatcher {
fn new<F: notify::EventHandler>(
_event_handler: F,
_config: notify::Config,
) -> notify::Result<Self> {
Ok(Self {
fail_path: PathBuf::from("bad"),
})
}

fn watch(&mut self, path: &Path, _recursive_mode: RecursiveMode) -> notify::Result<()> {
if path == self.fail_path {
Err(Error::path_not_found())
} else {
Ok(())
}
}

fn unwatch(&mut self, path: &Path) -> notify::Result<()> {
if path == self.fail_path {
Err(Error::path_not_found())
} else {
Ok(())
}
}

fn kind() -> WatcherKind {
WatcherKind::NullWatcher
}
}

#[rstest]
fn state(
#[values(
Expand Down Expand Up @@ -1014,4 +1124,89 @@ mod tests {
.expect("No event")
.expect("error");
}

#[test]
fn update_paths() -> Result<(), Box<dyn std::error::Error>> {
let dir1 = tempdir()?;
let dir2 = tempdir()?;

// set up the watcher
let (tx, rx) = std::sync::mpsc::channel();
let mut debouncer = new_debouncer(Duration::from_millis(10), None, tx)?;
debouncer.update_paths([
PathOp::watch_recursive(dir1.path()),
PathOp::watch_recursive(dir2.path()),
])?;

// create a new file
let file_path1 = dir1.path().join("file.txt");
let file_path2 = dir2.path().join("file.txt");
fs::write(&file_path1, b"Lorem ipsum1")?;
fs::write(&file_path2, b"Lorem ipsum1")?;

println!(
"waiting for events at {:?} and {:?}",
file_path1, file_path2
);

// wait for up to 10 seconds for the create event, ignore all other events
let deadline = Instant::now() + Duration::from_secs(10);
let mut received = (false, false);
while deadline > Instant::now() {
let events = rx
.recv_timeout(deadline - Instant::now())
.expect("did not receive expected event")
.expect("received an error");

for event in events {
println!("event {event:?}");
if event.event.paths == vec![file_path1.clone()]
|| event.event.paths == vec![file_path1.canonicalize()?]
{
received.0 = true;
}

if event.event.paths == vec![file_path2.clone()]
|| event.event.paths == vec![file_path2.canonicalize()?]
{
received.1 = true;
}

if received == (true, true) {
return Ok(());
}
}
}

panic!("did not receive expected event");
}

#[test]
fn update_paths_error_does_not_add_failed_root() -> Result<(), Box<dyn std::error::Error>> {
let mut debouncer = new_debouncer_opt::<_, FailingWatcher, NoCache>(
Duration::from_millis(20),
Some(Duration::from_millis(5)),
|_| {},
NoCache::new(),
notify::Config::default(),
)?;

let err = debouncer
.update_paths([
PathOp::watch_recursive("ok1"),
PathOp::watch_recursive("bad"),
PathOp::watch_recursive("ok2"),
])
.unwrap_err();
assert!(err.origin.is_some());
assert_eq!(err.remaining.len(), 1);

let roots = debouncer.data.lock().unwrap().roots.clone();
assert_eq!(
roots,
vec![(PathBuf::from("ok1"), RecursiveMode::Recursive)]
);

Ok(())
}
}
8 changes: 6 additions & 2 deletions notify/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## notify 9.0.0 (unreleased)

- FEATURE: remove `Watcher::paths_mut` and introduce `update_paths` [#705]
- FEATURE: impl `EventHandler` for `futures::channel::mpsc::UnboundedSender` and `tokio::sync::mpsc::UnboundedSender` behind the `futures` and `tokio` feature flags [#767]

[#705]: https://github.com/notify-rs/notify/pull/705
[#767]: https://github.com/notify-rs/notify/pull/767

## notify 9.0.0-rc.1 (2026-01-25)

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

[#718]: https://github.com/notify-rs/notify/pull/718
[#720]: https://github.com/notify-rs/notify/pull/720
[#726]: https://github.com/notify-rs/notify/pull/726
[#733]: https://github.com/notify-rs/notify/pull/733
[#736]: https://github.com/notify-rs/notify/pull/736
[#747]: https://github.com/notify-rs/notify/pull/747
[#767]: https://github.com/notify-rs/notify/pull/767

## notify 8.2.0 (2025-08-03)
- FEATURE: notify user if inotify's `max_user_watches` has been reached [#698]
Expand Down
80 changes: 79 additions & 1 deletion notify/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
//! Configuration types

use notify_types::event::EventKindMask;
use std::time::Duration;
use std::{
path::{Path, PathBuf},
time::Duration,
};

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

/// Single watch backend configuration
///
/// This contains some settings that may relate to only one specific backend,
/// such as to correctly configure each backend regardless of what is selected during runtime.
#[derive(Debug)]
pub struct WatchPathConfig {
recursive_mode: RecursiveMode,
}

impl WatchPathConfig {
/// Creates new instance with provided [`RecursiveMode`]
pub fn new(recursive_mode: RecursiveMode) -> Self {
Self { recursive_mode }
}

/// Set [`RecursiveMode`] for the watch
pub fn with_recursive_mode(mut self, recursive_mode: RecursiveMode) -> Self {
self.recursive_mode = recursive_mode;
self
}

/// Returns current setting
pub fn recursive_mode(&self) -> RecursiveMode {
self.recursive_mode
}
}

/// An operation to apply to a watcher
///
/// See [`Watcher::update_paths`] for more information
#[derive(Debug)]
pub enum PathOp {
/// Path should be watched
Watch(PathBuf, WatchPathConfig),

/// Path should be unwatched
Unwatch(PathBuf),
}

impl PathOp {
/// Watch the path with [`RecursiveMode::Recursive`]
pub fn watch_recursive<P: Into<PathBuf>>(path: P) -> Self {
Self::Watch(path.into(), WatchPathConfig::new(RecursiveMode::Recursive))
}

/// Watch the path with [`RecursiveMode::NonRecursive`]
pub fn watch_non_recursive<P: Into<PathBuf>>(path: P) -> Self {
Self::Watch(
path.into(),
WatchPathConfig::new(RecursiveMode::NonRecursive),
)
}

/// Unwatch the path
pub fn unwatch<P: Into<PathBuf>>(path: P) -> Self {
Self::Unwatch(path.into())
}

/// Returns the path associated with this operation.
pub fn as_path(&self) -> &Path {
match self {
PathOp::Watch(p, _) => p,
PathOp::Unwatch(p) => p,
}
}

/// Returns the path associated with this operation.
pub fn into_path(self) -> PathBuf {
match self {
PathOp::Watch(p, _) => p,
PathOp::Unwatch(p) => p,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading