fs_change_detector/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/swamp
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use message_channel::{Channel, Receiver};
6use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
7use std::path::{Path, PathBuf};
8use std::time::{Duration, Instant};
9use notify::event::ModifyKind;
10use thiserror::Error;
11use tracing::{debug, error};
12use notify::{Event, Result as NotifyResult};
13#[derive(Debug)]
14pub enum ChangeMessage {
15    SomeKindOfChange,
16}
17
18#[derive(Error, Debug)]
19pub enum FileWatcherError {
20    #[error("Filesystem I/O error: {0}")]
21    IoError(String),
22
23    #[error("Watch path not found: '{0}'")]
24    PathNotFound(PathBuf),
25
26    #[error("System watcher limit reached (too many watched files/directories)")]
27    TooManyWatches,
28
29    #[error("Invalid watcher configuration: {0:?}")]
30    InvalidWatcherConfig(Config),
31
32    #[error("Generic watcher error: {0}")]
33    WatcherGenericError(String),
34
35    #[error("Internal channel communication error: {0}")]
36    InternalChannelError(String),
37
38    #[error("Attempted to remove a watch that does not exist for path: '{0}'")]
39    WatchNotFound(PathBuf),
40}
41
42fn map_notify_error_to_file_watcher_error(e: notify::Error, path: &Path) -> FileWatcherError {
43    use notify::ErrorKind;
44    match e.kind {
45        ErrorKind::PathNotFound => FileWatcherError::PathNotFound(path.to_path_buf()),
46        ErrorKind::MaxFilesWatch => FileWatcherError::TooManyWatches,
47        ErrorKind::Generic(msg) => FileWatcherError::WatcherGenericError(msg),
48        ErrorKind::InvalidConfig(config) => FileWatcherError::InvalidWatcherConfig(config),
49        ErrorKind::WatchNotFound => FileWatcherError::WatchNotFound(path.to_path_buf()),
50
51        ErrorKind::Io(io_err) => FileWatcherError::IoError(io_err.to_string()),
52    }
53}
54
55#[derive(Debug)]
56pub struct FileWatcher {
57    pub receiver: Receiver<ChangeMessage>,
58    pub watcher: RecommendedWatcher, // keeps watcher alive
59}
60
61impl FileWatcher {
62    /// # Errors
63    ///
64    pub fn new(watch_path: &Path) -> Result<Self, FileWatcherError> {
65        let (watcher, receiver) = start_watch(watch_path)?;
66        while let Ok(_found) = receiver.recv() {
67        }
68        Ok(Self { receiver, watcher })
69    }
70
71    #[must_use]
72    pub fn has_changed(&self) -> bool {
73        let mut result = false;
74        while let Ok(_found) = self.receiver.recv() {
75            result = true;
76        }
77
78        result
79    }
80}
81
82/// # Errors
83///
84/// # Panics
85///
86///
87pub fn start_watch(
88    watch_path: &Path,
89) -> Result<(RecommendedWatcher, Receiver<ChangeMessage>), FileWatcherError> {
90    let (sender, receiver) = Channel::create();
91
92    let mut last_event = Instant::now().checked_sub(Duration::from_secs(1)).unwrap();
93    let debounce_duration = Duration::from_millis(100);
94
95    let owned_watch_path = watch_path.to_path_buf();
96
97    let mut watcher = notify::recommended_watcher(move |res: NotifyResult<Event> | match res {
98        Ok(event) if matches!(event.kind,
99            EventKind::Modify(ModifyKind::Data(_))
100          | EventKind::Modify(ModifyKind::Any)
101          ) =>
102            {
103                let now = Instant::now();
104                if now.duration_since(last_event) >= debounce_duration {
105                    if let Err(e) = sender.send(ChangeMessage::SomeKindOfChange) {
106                        error!(
107                        error = ?e,
108                        "FileWatcher internal channel send error: receiver likely dropped"
109                    );
110                    }
111                    last_event = now;
112                }
113            }
114        Ok(_) => {
115            // ignore metadata, attrib, open, etc.
116        }
117
118        Err(e) => {
119            error!(
120                error = ?e,
121                path = ?owned_watch_path,
122                "FileWatcher internal background watch error"
123            );
124        }
125    })
126    .map_err(|e| {
127        error!(error = ?e, path = ?watch_path, "Failed to initialize watcher");
128        map_notify_error_to_file_watcher_error(e, watch_path)
129    })?;
130
131    watcher
132        .watch(watch_path, RecursiveMode::Recursive)
133        .map_err(|e| {
134            error!(error = ?e, path = ?watch_path, "Failed to start watching path");
135            map_notify_error_to_file_watcher_error(e, watch_path)
136        })?;
137
138    debug!(path = ?watch_path, "Successfully started file watcher");
139
140    Ok((watcher, receiver))
141}