xvc_walker/
notify.rs

1//! A libnotify based file system notification module that considers ignore rules.
2//!
3//! This module uses [notify] crate to watch file system events.
4//! It filters relevant events, and also ignores the events from ignored paths.
5//! It defines [PathEvent] as a simple version of [notify::EventKind].
6//! It defines [PathEventHandler] that handles events from [notify::EventHandler].
7use crate::{
8    error::{Error, Result},
9    IgnoreRules, MatchResult,
10};
11pub use notify::{
12    Config, Event, EventHandler, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher,
13};
14use std::fs::Metadata;
15use std::path::PathBuf;
16use std::time::Duration;
17use xvc_logging::watch;
18
19use crossbeam_channel::{bounded, Receiver, Sender};
20use log::{debug, warn};
21
22/// An walker-relevant event for changes in a directory.
23/// It packs newer [std::fs::Metadata] if there is.
24#[derive(Debug, Clone)]
25pub enum PathEvent {
26    /// Emitted when a new `path` is created with `metadata`.
27    Create {
28        /// The created path
29        path: PathBuf,
30        /// The new metadata
31        metadata: Metadata,
32    },
33    /// Emitted after a new write to `path`.
34    Update {
35        /// Updated path
36        path: PathBuf,
37        /// New metadata
38        metadata: Metadata,
39    },
40    /// Emitted when [PathBuf] is deleted.
41    Delete {
42        /// Deleted path
43        path: PathBuf,
44    },
45}
46
47/// A struct that handles [notify::Event]s considering also [IgnoreRules]
48struct PathEventHandler {
49    sender: Sender<Option<PathEvent>>,
50    ignore_rules: IgnoreRules,
51}
52
53impl EventHandler for PathEventHandler {
54    fn handle_event(&mut self, event: notify::Result<Event>) {
55        watch!(event);
56        if let Ok(event) = event {
57            match event.kind {
58                notify::EventKind::Create(_) => self.create_event(event.paths[0].clone()),
59                notify::EventKind::Modify(mk) => match mk {
60                    notify::event::ModifyKind::Any => todo!(),
61                    notify::event::ModifyKind::Data(_) => self.write_event(event.paths[0].clone()),
62                    notify::event::ModifyKind::Metadata(_) => {
63                        self.write_event(event.paths[0].clone())
64                    }
65                    notify::event::ModifyKind::Name(rk) => match rk {
66                        notify::event::RenameMode::Any => {}
67                        notify::event::RenameMode::To => self.create_event(event.paths[0].clone()),
68                        notify::event::RenameMode::From => {
69                            self.remove_event(event.paths[0].clone())
70                        }
71                        notify::event::RenameMode::Both => {
72                            self.rename_event(event.paths[0].clone(), event.paths[1].clone())
73                        }
74                        notify::event::RenameMode::Other => {}
75                    },
76                    notify::event::ModifyKind::Other => {}
77                },
78                notify::EventKind::Remove(_) => self.remove_event(event.paths[0].clone()),
79                notify::EventKind::Any => {}
80                notify::EventKind::Access(_) => {}
81                notify::EventKind::Other => {}
82            }
83        } else {
84            debug!("{:?}", event);
85        }
86    }
87}
88
89impl PathEventHandler {
90    fn write_event(&mut self, path: PathBuf) {
91        match self.ignore_rules.check(&path) {
92            MatchResult::Whitelist | MatchResult::NoMatch => {
93                if let Ok(metadata) = path.metadata() {
94                    self.sender
95                        .send(Some(PathEvent::Create {
96                            path: path.clone(),
97                            metadata,
98                        }))
99                        .unwrap_or_else(|e| {
100                            Error::from(e).warn();
101                        });
102                } else {
103                    debug!("Error in metadata for {}", path.to_string_lossy());
104                }
105            }
106            MatchResult::Ignore => {
107                debug!("FS Notification Ignored: {}", path.to_string_lossy());
108            }
109        }
110    }
111
112    fn create_event(&mut self, path: PathBuf) {
113        match self.ignore_rules.check(&path) {
114            MatchResult::Whitelist | MatchResult::NoMatch => {
115                if let Ok(metadata) = path.metadata() {
116                    self.sender
117                        .send(Some(PathEvent::Create {
118                            path: path.clone(),
119                            metadata,
120                        }))
121                        .unwrap_or_else(|e| {
122                            Error::from(e).warn();
123                        });
124                } else {
125                    debug!("Error in metadata for {}", path.to_string_lossy());
126                }
127            }
128            MatchResult::Ignore => {
129                debug!("FS Notification Ignored: {}", path.to_string_lossy());
130            }
131        }
132    }
133
134    fn remove_event(&mut self, path: PathBuf) {
135        match self.ignore_rules.check(&path) {
136            MatchResult::Whitelist | MatchResult::NoMatch => {
137                self.sender
138                    .send(Some(PathEvent::Delete { path }))
139                    .unwrap_or_else(|e| warn!("{}", e));
140            }
141            MatchResult::Ignore => {
142                debug!("FS Notification Ignored: {}", path.to_string_lossy());
143            }
144        }
145    }
146
147    fn rename_event(&mut self, from: PathBuf, to: PathBuf) {
148        self.remove_event(from);
149        self.create_event(to);
150    }
151}
152
153/// Create a [notify::RecommendedWatcher] and a [crossbeam_channel::Receiver] to receive
154/// [PathEvent]s. It creates the channel and [PathEventHandler] with its [Sender], then returns the
155/// [Receiver] for consumption.
156///
157/// Paths ignored by `ignore_rules` do not emit any events.
158pub fn make_watcher(
159    ignore_rules: IgnoreRules,
160) -> Result<(RecommendedWatcher, Receiver<Option<PathEvent>>)> {
161    let (sender, receiver) = bounded(10000);
162    let root = ignore_rules.root.clone();
163    let mut watcher = notify::recommended_watcher(PathEventHandler {
164        ignore_rules,
165        sender,
166    })?;
167
168    watcher.watch(&root, RecursiveMode::Recursive)?;
169    watch!(watcher);
170    Ok((watcher, receiver))
171}
172
173/// Create a [notify::PollWatcher] and a [crossbeam_channel::Receiver] to receive
174/// [PathEvent]s. It creates the channel and [PathEventHandler] with its [Sender], then returns the
175/// [Receiver] for consumption.
176///
177/// Note that this is used when [`make_watcher`] doesn't work as expected. It uses
178/// a polling watcher with 2 second polls to the filesystem. It has lower
179/// performance than [`make_watcher`], but it works in all platforms.
180pub fn make_polling_watcher(
181    ignore_rules: IgnoreRules,
182) -> Result<(PollWatcher, Receiver<Option<PathEvent>>)> {
183    let (sender, receiver) = bounded(10000);
184    let root = ignore_rules.root.clone();
185    let mut watcher = notify::poll::PollWatcher::new(
186        PathEventHandler {
187            ignore_rules,
188            sender,
189        },
190        Config::default().with_poll_interval(Duration::from_secs(2)),
191    )?;
192
193    watcher.watch(&root, RecursiveMode::Recursive)?;
194    watch!(watcher);
195    Ok((watcher, receiver))
196}