xvc_walker/
notify.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! A libnotify based file system notification module that considers ignore rules.
//!
//! This module uses [notify] crate to watch file system events.
//! It filters relevant events, and also ignores the events from ignored paths.
//! It defines [PathEvent] as a simple version of [notify::EventKind].
//! It defines [PathEventHandler] that handles events from [notify::EventHandler].
use crate::{
    error::{Error, Result},
    IgnoreRules, MatchResult,
};
pub use notify::{
    Config, Event, EventHandler, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher,
};
use std::fs::Metadata;
use std::path::PathBuf;
use std::time::Duration;
use xvc_logging::watch;

use crossbeam_channel::{bounded, Receiver, Sender};
use log::{debug, warn};

/// An walker-relevant event for changes in a directory.
/// It packs newer [std::fs::Metadata] if there is.
#[derive(Debug, Clone)]
pub enum PathEvent {
    /// Emitted when a new `path` is created with `metadata`.
    Create {
        /// The created path
        path: PathBuf,
        /// The new metadata
        metadata: Metadata,
    },
    /// Emitted after a new write to `path`.
    Update {
        /// Updated path
        path: PathBuf,
        /// New metadata
        metadata: Metadata,
    },
    /// Emitted when [PathBuf] is deleted.
    Delete {
        /// Deleted path
        path: PathBuf,
    },
}

/// A struct that handles [notify::Event]s considering also [IgnoreRules]
struct PathEventHandler {
    sender: Sender<Option<PathEvent>>,
    ignore_rules: IgnoreRules,
}

impl EventHandler for PathEventHandler {
    fn handle_event(&mut self, event: notify::Result<Event>) {
        watch!(event);
        if let Ok(event) = event {
            match event.kind {
                notify::EventKind::Create(_) => self.create_event(event.paths[0].clone()),
                notify::EventKind::Modify(mk) => match mk {
                    notify::event::ModifyKind::Any => todo!(),
                    notify::event::ModifyKind::Data(_) => self.write_event(event.paths[0].clone()),
                    notify::event::ModifyKind::Metadata(_) => {
                        self.write_event(event.paths[0].clone())
                    }
                    notify::event::ModifyKind::Name(rk) => match rk {
                        notify::event::RenameMode::Any => {}
                        notify::event::RenameMode::To => self.create_event(event.paths[0].clone()),
                        notify::event::RenameMode::From => {
                            self.remove_event(event.paths[0].clone())
                        }
                        notify::event::RenameMode::Both => {
                            self.rename_event(event.paths[0].clone(), event.paths[1].clone())
                        }
                        notify::event::RenameMode::Other => {}
                    },
                    notify::event::ModifyKind::Other => {}
                },
                notify::EventKind::Remove(_) => self.remove_event(event.paths[0].clone()),
                notify::EventKind::Any => {}
                notify::EventKind::Access(_) => {}
                notify::EventKind::Other => {}
            }
        } else {
            debug!("{:?}", event);
        }
    }
}

impl PathEventHandler {
    fn write_event(&mut self, path: PathBuf) {
        match self.ignore_rules.check(&path) {
            MatchResult::Whitelist | MatchResult::NoMatch => {
                if let Ok(metadata) = path.metadata() {
                    self.sender
                        .send(Some(PathEvent::Create {
                            path: path.clone(),
                            metadata,
                        }))
                        .unwrap_or_else(|e| {
                            Error::from(e).warn();
                        });
                } else {
                    debug!("Error in metadata for {}", path.to_string_lossy());
                }
            }
            MatchResult::Ignore => {
                debug!("FS Notification Ignored: {}", path.to_string_lossy());
            }
        }
    }

    fn create_event(&mut self, path: PathBuf) {
        match self.ignore_rules.check(&path) {
            MatchResult::Whitelist | MatchResult::NoMatch => {
                if let Ok(metadata) = path.metadata() {
                    self.sender
                        .send(Some(PathEvent::Create {
                            path: path.clone(),
                            metadata,
                        }))
                        .unwrap_or_else(|e| {
                            Error::from(e).warn();
                        });
                } else {
                    debug!("Error in metadata for {}", path.to_string_lossy());
                }
            }
            MatchResult::Ignore => {
                debug!("FS Notification Ignored: {}", path.to_string_lossy());
            }
        }
    }

    fn remove_event(&mut self, path: PathBuf) {
        match self.ignore_rules.check(&path) {
            MatchResult::Whitelist | MatchResult::NoMatch => {
                self.sender
                    .send(Some(PathEvent::Delete { path }))
                    .unwrap_or_else(|e| warn!("{}", e));
            }
            MatchResult::Ignore => {
                debug!("FS Notification Ignored: {}", path.to_string_lossy());
            }
        }
    }

    fn rename_event(&mut self, from: PathBuf, to: PathBuf) {
        self.remove_event(from);
        self.create_event(to);
    }
}

/// Create a [notify::RecommendedWatcher] and a [crossbeam_channel::Receiver] to receive
/// [PathEvent]s. It creates the channel and [PathEventHandler] with its [Sender], then returns the
/// [Receiver] for consumption.
///
/// Paths ignored by `ignore_rules` do not emit any events.
pub fn make_watcher(
    ignore_rules: IgnoreRules,
) -> Result<(RecommendedWatcher, Receiver<Option<PathEvent>>)> {
    let (sender, receiver) = bounded(10000);
    let root = ignore_rules.root.clone();
    let mut watcher = notify::recommended_watcher(PathEventHandler {
        ignore_rules,
        sender,
    })?;

    watcher.watch(&root, RecursiveMode::Recursive)?;
    watch!(watcher);
    Ok((watcher, receiver))
}

/// Create a [notify::PollWatcher] and a [crossbeam_channel::Receiver] to receive
/// [PathEvent]s. It creates the channel and [PathEventHandler] with its [Sender], then returns the
/// [Receiver] for consumption.
///
/// Note that this is used when [`make_watcher`] doesn't work as expected. It uses
/// a polling watcher with 2 second polls to the filesystem. It has lower
/// performance than [`make_watcher`], but it works in all platforms.
pub fn make_polling_watcher(
    ignore_rules: IgnoreRules,
) -> Result<(PollWatcher, Receiver<Option<PathEvent>>)> {
    let (sender, receiver) = bounded(10000);
    let root = ignore_rules.root.clone();
    let mut watcher = notify::poll::PollWatcher::new(
        PathEventHandler {
            ignore_rules,
            sender,
        },
        Config::default().with_poll_interval(Duration::from_secs(2)),
    )?;

    watcher.watch(&root, RecursiveMode::Recursive)?;
    watch!(watcher);
    Ok((watcher, receiver))
}