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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
use inotify::{EventMask, Inotify, WatchMask};
use smol::{channel::Sender, io};
use std::{
    ffi::OsStr,
    path::{Path, PathBuf},
};

/// The error that a channel has been closed
pub const SENDER_CHANNEL_ERROR: &str = "SENDER_CHANNEL_CLOSED";

/// The sender type for a channel as a type for reusability
pub type FsSender = Sender<WatcherOutcome>;

/// Create a watcher for a certain path that can be a file or directory
///
/// #### Structure
/// ```rust
/// use dir_meta::FsSender;
/// use std::path::PathBuf;
///
/// #[derive(Debug)]
/// pub struct FsWatcher {
///     path: Option<PathBuf>,
///     sender: FsSender,
/// }
/// ```
///
/// #### Example
/// ```rust
/// use dir_meta::{inotify::WatchMask, smol::channel, FsWatcher, WatcherOutcome};
///
/// smol::block_on(async {
///     let (sender, receiver) = channel::unbounded::<WatcherOutcome>();
///
///     let watch_options =
///         WatchMask::MODIFY | WatchMask::CREATE | WatchMask::DELETE | WatchMask::DELETE_SELF;
///
///     smol::spawn(FsWatcher::new(sender).path("Foo").watch(watch_options)).detach();
///
///     while let Ok(data) = receiver.recv().await {
///         dbg!(data);
///     }
/// });
/// ```
#[derive(Debug)]
pub struct FsWatcher {
    path: Option<PathBuf>, //Option is used here to make it easier to return ErrorKind::NotFound in io::Result when calling watcher
    sender: FsSender,
}

impl FsWatcher {
    /// Create a new [FsWatcher] by passing an async-channel::channel::Sender with type specified by [FsSender]
    pub fn new(sender: FsSender) -> Self {
        Self {
            sender,
            path: Option::default(),
        }
    }

    /// Add the path to listen to
    pub fn path(mut self, path: impl AsRef<Path>) -> Self {
        self.path.replace(path.as_ref().to_path_buf());

        self
    }

    /// Watch the path using the parameters from `inotify::WatchMask`
    /// which can be concatenated `WatchMask::MODIFY | WatchMask::CREATE | WatchMask::DELETE`
    pub async fn watch(self, watch_for: WatchMask) -> io::Result<()> {
        if let Some(path) = self.path {
            let mut inotify = Inotify::init()?;

            inotify.watches().add(&path, watch_for)?;

            //TODO add logging here "Watching current directory for activity..."

            let mut buffer = [0u8; 4096];

            loop {
                let events = inotify.read_events_blocking(&mut buffer)?;

                for event in events {
                    let outcome: WatcherOutcome = event.into();

                    if self.sender.clone().send(outcome).await.is_err() {
                        return Err(io::Error::new(io::ErrorKind::Other, SENDER_CHANNEL_ERROR));
                    }
                }
            }
        } else {
            Err(io::Error::new(
                io::ErrorKind::NotFound,
                "The path was not found, maybe you didn't specify it",
            ))
        }
    }
}

/// Events triggered from watching a directory or file
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub enum WatcherEvents {
    /// File was accessed
    ///
    /// When watching a directory, this event is only triggered for objects
    /// inside the directory, not the directory itself.
    Access,
    /// Metadata (permissions, timestamps, ...) changed
    ///
    /// When watching a directory, this event can be triggered for the
    /// directory itself, as well as objects inside the directory.
    Attrib,
    /// File opened for writing was closed
    ///
    /// When watching a directory, this event is only triggered for objects
    /// inside the directory, not the directory itself.
    CloseWrite,
    /// File or directory not opened for writing was closed
    ///
    /// When watching a directory, this event can be triggered for the
    /// directory itself, as well as objects inside the directory.
    CloseNoWrite,
    /// File/directory created in watched directory
    ///
    /// When watching a directory, this event is only triggered for objects
    /// inside the directory, not the directory itself.
    Create,
    /// File/directory deleted from watched directory
    ///
    /// When watching a directory, this event is only triggered for objects
    /// inside the directory, not the directory itself.
    Delete,
    /// Watched file/directory was deleted
    DeleteSelf,
    /// File was modified
    ///
    /// When watching a directory, this event is only triggered for objects
    /// inside the directory, not the directory itself.
    Modify,
    /// Watched file/directory was moved
    MoveSelf,
    /// File was renamed/moved; watched directory contained old name
    ///
    /// When watching a directory, this event is only triggered for objects
    /// inside the directory, not the directory itself.
    MovedFrom,
    /// File was renamed/moved; watched directory contains new name
    ///
    /// When watching a directory, this event is only triggered for objects
    /// inside the directory, not the directory itself.
    MovedTo,
    /// File or directory was opened
    ///
    /// When watching a directory, this event can be triggered for the
    /// directory itself, as well as objects inside the directory.
    Open,
    /// Watch was removed
    ///
    /// This event will be generated, if the watch was removed explicitly
    /// (via [`Watches::remove`]), or automatically (because the file was
    /// deleted or the file system was unmounted).
    Ignored,
    /// Event related to a directory
    ///
    /// The subject of the event is a directory.
    IsDir,
    /// Event queue overflowed
    ///
    /// The event queue has overflowed and events have presumably been lost.
    QueueOverflow,
    /// File system containing watched object was unmounted.
    /// File system was unmounted
    ///
    /// The file system that contained the watched object has been
    /// unmounted. An event with [`WatchMask::IGNORED`] will subsequently be
    /// generated for the same watch descriptor.
    Unmount,
    /// Current event is unsupported
    Unsupported,
}

impl From<EventMask> for WatcherEvents {
    fn from(value: EventMask) -> Self {
        match value {
            EventMask::ACCESS => Self::Access,
            EventMask::ATTRIB => Self::Attrib,
            EventMask::CLOSE_WRITE => Self::CloseWrite,
            EventMask::CLOSE_NOWRITE => Self::CloseNoWrite,
            EventMask::CREATE => Self::Create,
            EventMask::DELETE => Self::Delete,
            EventMask::DELETE_SELF => Self::DeleteSelf,
            EventMask::MODIFY => Self::Modify,
            EventMask::MOVE_SELF => Self::MoveSelf,
            EventMask::MOVED_FROM => Self::MovedFrom,
            EventMask::MOVED_TO => Self::MovedTo,
            EventMask::OPEN => Self::Open,
            EventMask::IGNORED => Self::Ignored,
            EventMask::ISDIR => Self::IsDir,
            EventMask::Q_OVERFLOW => Self::QueueOverflow,
            EventMask::UNMOUNT => Self::Unmount,
            _ => Self::Unsupported,
        }
    }
}

/// The outcome of a watched file or directory
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct WatcherOutcome {
    /// Identifies the watch this event originates from
    /// This WatchDescriptor is equal to the one that Watches::add returned when interest for this event was registered. The WatchDescriptor can be used to remove the watch using Watches::remove,
    /// thereby preventing future events of this type from being created.
    pub descriptor: i32,
    /// Indicates what kind of event this is
    pub mask: WatcherEvents,
    /// Connects related events to each other
    /// When a file is renamed, this results two events: MOVED_FROM and MOVED_TO. The cookie field will be the same for both of them, thereby making is possible to connect the event pair.
    pub cookie: u32,
    /// The name of the file the event originates from
    /// This field is set only if the subject of the event is a file or directory in a watched directory.
    /// If the event concerns a file or directory that is watched directly, name will be None.
    pub name: Option<String>,
}

impl From<inotify::Event<&OsStr>> for WatcherOutcome {
    fn from(event: inotify::Event<&OsStr>) -> Self {
        let name = event
            .name
            .map(|inner_name| inner_name.to_string_lossy().to_string());

        Self {
            descriptor: event.wd.get_watch_descriptor_id(),
            mask: event.mask.into(),
            cookie: event.cookie,
            name,
        }
    }
}