fs_change_notifier/
lib.rs

1//! FS changes' notifier.
2//!
3//! Simple library to watch file changes inside given directory.
4//!
5//! Usage example:
6//!
7//! ```rust,ignore
8//! use fs_change_notifier::{create_watcher, match_event, RecursiveMode};
9//!
10//! let root = PathBuf::from(".");
11//! let (mut wr, rx) = create_watcher(|e| log::error!("{e:?}")).unwrap();
12//! wr.watch(&root, RecursiveMode::Recursive).unwrap();
13//!
14//! loop {
15//!     tokio::select! {
16//!         _ = your_job => {},
17//!         _ = match_event(&root, rx, &exclude) => {
18//!             // do your logic on fs update
19//!         },
20//!     }
21//! }
22//! ```
23
24#![deny(warnings, missing_docs, clippy::todo, clippy::unimplemented)]
25
26use notify::event::{CreateKind, ModifyKind};
27use notify::{Event, EventKind, Watcher};
28use std::collections::HashSet;
29use std::path::{Path, PathBuf};
30use tokio::sync::mpsc;
31
32pub use notify::RecursiveMode;
33
34/// Creates a watcher and an associated MPSC channel receiver.
35pub fn create_watcher(
36    err_handler: impl Fn(notify::Error) + Send + 'static,
37) -> anyhow::Result<(Box<dyn Watcher + Send>, mpsc::Receiver<Event>)> {
38    let (tx, rx) = mpsc::channel::<Event>(1000);
39    let mut watcher = notify::recommended_watcher(move |ev: notify::Result<Event>| match ev {
40        Ok(ev) => {
41            let _ = tx.blocking_send(ev);
42        }
43        Err(e) => err_handler(e),
44    })?;
45
46    watcher.configure(notify::Config::default().with_follow_symlinks(false))?;
47
48    Ok((Box::new(watcher) as Box<dyn Watcher + Send>, rx))
49}
50
51/// Matches event and returns on included ones.
52///
53/// This function relies on given `root` and `exclude` set to watch changes happened
54/// only inside given directory and not with excluded files.
55pub async fn match_event(root: &Path, mut rx: mpsc::Receiver<Event>, exclude: &HashSet<PathBuf>) {
56    loop {
57        let event = if let Some(ev) = rx.recv().await {
58            ev
59        } else {
60            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
61            continue;
62        };
63
64        let has_non_excluded = event.paths.iter().any(|event_path| {
65            !exclude.iter().any(|exclude_path| {
66                let event = event_path.to_string_lossy();
67                let exclude = exclude_path.to_string_lossy();
68
69                if exclude.contains('*') {
70                    let parts = exclude.split('*').collect::<Vec<_>>();
71                    if parts.len() != 2 {
72                        false
73                    } else if let Ok(relative_event) = event_path.strip_prefix(root).map(|p| p.to_string_lossy()) {
74                        relative_event.starts_with(parts[0]) && relative_event.ends_with(parts[1])
75                    } else {
76                        event.contains(parts[0]) && event.ends_with(parts[1])
77                    }
78                } else {
79                    event.contains(exclude.as_ref())
80                }
81            })
82        });
83
84        if has_non_excluded {
85            match event.kind {
86                EventKind::Create(CreateKind::File) => return,
87                EventKind::Modify(ModifyKind::Name(_)) | EventKind::Modify(ModifyKind::Data(_)) => return,
88                EventKind::Remove(_) => return,
89                _ => {}
90            }
91        }
92    }
93}
94
95/// Matches event and returns changed file.
96///
97/// This function relies on given `root` and `exclude` set to watch changes happened
98/// only inside given directory and not with excluded files.
99pub async fn fetch_changed(root: &Path, mut rx: mpsc::Receiver<Event>, exclude: &HashSet<PathBuf>) -> Vec<PathBuf> {
100    loop {
101        let event = if let Some(ev) = rx.recv().await {
102            ev
103        } else {
104            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
105            continue;
106        };
107
108        let mut included = vec![];
109        event.paths.iter().for_each(|event_path| {
110            exclude.iter().for_each(|exclude_path| {
111                let event = event_path.to_string_lossy();
112                let exclude = exclude_path.to_string_lossy();
113
114                let exclude = if exclude.contains('*') {
115                    let parts = exclude.split('*').collect::<Vec<_>>();
116                    if parts.len() != 2 {
117                        false
118                    } else if let Ok(relative_event) = event_path.strip_prefix(root).map(|p| p.to_string_lossy()) {
119                        relative_event.starts_with(parts[0]) && relative_event.ends_with(parts[1])
120                    } else {
121                        event.contains(parts[0]) && event.ends_with(parts[1])
122                    }
123                } else {
124                    event.contains(exclude.as_ref())
125                };
126
127                if !exclude {
128                    included.push(event_path.to_path_buf());
129                }
130            })
131        });
132
133        if !included.is_empty() {
134            match event.kind {
135                EventKind::Create(CreateKind::File) => return included,
136                EventKind::Modify(ModifyKind::Name(_)) | EventKind::Modify(ModifyKind::Data(_)) => return included,
137                EventKind::Remove(_) => return included,
138                _ => {}
139            }
140        }
141    }
142}