Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
109 changes: 108 additions & 1 deletion 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,56 @@ 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.
///
/// # 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> {
// timeout,
// tick_rate,
// event_handler,
// RecommendedCache::new(),
// notify::Config::default(),
let ops: Vec<_> = ops.into_iter().map(Into::into).collect();
let res = self.watcher.update_paths(ops.clone());
let updated_paths = match res.as_ref() {
Ok(()) => &ops[..],
Err(e) => &ops[..ops.len() - e.remaining.len()],
};
for op in updated_paths {
match op {
PathOp::Watch(path, config) => self.add_root(path, config.recursive_mode()),
PathOp::Unwatch(path) => self.remove_root(path),
}
}
res
}

pub fn configure(&mut self, option: notify::Config) -> notify::Result<bool> {
self.watcher.configure(option)
}
Expand Down Expand Up @@ -1014,4 +1065,60 @@ 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");
}
}
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
61 changes: 60 additions & 1 deletion notify/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Configuration types

use notify_types::event::EventKindMask;
use std::time::Duration;
use std::{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 +165,65 @@ 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, Clone, PartialEq, Eq, Hash)]
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, Clone, PartialEq, Eq, Hash)]
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())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
66 changes: 57 additions & 9 deletions notify/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Error types

use crate::Config;
use crate::{Config, PathOp};
use std::error::Error as StdError;
use std::fmt::Debug;
use std::path::PathBuf;
use std::result::Result as StdResult;
use std::{self, fmt, io};
Expand Down Expand Up @@ -158,14 +159,61 @@ impl<T> From<std::sync::PoisonError<T>> for Error {
}
}

#[test]
fn display_formatted_errors() {
let expected = "Some error";
/// The error provided by [`crate::Watcher::update_paths`] method
#[derive(Debug)]
pub struct UpdatePathsError {
/// The original error
pub source: Error,

/// The remaining operations that haven't been applied
pub remaining: Vec<PathOp>,
}

impl fmt::Display for UpdatePathsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unable to apply the batch operation: {}", self.source)
}
}

assert_eq!(expected, format!("{}", Error::generic(expected)));
impl StdError for UpdatePathsError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.source)
}
}

assert_eq!(
expected,
format!("{}", Error::io(io::Error::other(expected)))
);
impl From<UpdatePathsError> for Error {
fn from(value: UpdatePathsError) -> Self {
value.source
}
}

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

#[test]
fn display_formatted_errors() {
let expected = "Some error";

assert_eq!(expected, format!("{}", Error::generic(expected)));

assert_eq!(
expected,
format!("{}", Error::io(io::Error::other(expected)))
);
}

#[test]
fn display_update_paths() {
let actual = UpdatePathsError {
source: Error::generic("Some error"),
remaining: Default::default(),
}
.to_string();

assert_eq!(
format!("unable to apply the batch operation: Some error"),
actual
);
}
}
Loading