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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
use std::path::PathBuf;
use std::time::SystemTime;

/// The base watchable trait.
pub trait Watchable {
    /// The path associated with the watchable object
    fn path(&self) -> &PathBuf;
}

/// A standalone implementation of the watchable trait.
#[derive(Debug, Clone)]
pub struct BasicTarget {
    /// The path we want to watch
    path: PathBuf,
}

impl BasicTarget {
    pub fn new<T: Into<PathBuf>>(path: T) -> Self {
        Self { path: path.into() }
    }
}

impl Watchable for BasicTarget {
    fn path(&self) -> &PathBuf {
        &self.path
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
/// State transitions that a watchable may undergo.
pub enum Transition {
    Created,
    Modified,
    Deleted,
    None,
}

#[derive(Debug, Eq, PartialEq)]
/// The current state of the watchable.
pub enum WatchState {
    DoesNotExist,
    Exists(Option<SystemTime>),
}

#[derive(Debug, Default)]
/// A watcher instance.
///
/// An instance of Watcher keeps track of a vector of watchables and their corresponding states.
pub struct Watcher<W: Watchable> {
    targets: Vec<W>,
    states: Vec<WatchState>,
}

fn compute_state<W: Watchable>(target: &W) -> WatchState {
    // Does the specified path exist
    let file_exists = target.path().exists();

    // Compute the last modification date of this file, if possible
    let mut last_modified_date = None;

    if file_exists {
        // Determine the last modification time of this file
        let metadata = std::fs::metadata(target.path());

        if let Ok(metadata) = metadata {
            if let Ok(modified) = metadata.modified() {
                last_modified_date = Some(modified);
            }
        }
    }

    if file_exists {
        WatchState::Exists(last_modified_date)
    } else {
        WatchState::DoesNotExist
    }
}

impl<W: Watchable> Watcher<W> {
    /// Create a new watcher instance.
    ///
    /// # Examples
    ///
    /// ```
    /// use fwatch::{BasicTarget, Watcher};
    ///
    /// fn main() {
    ///     let mut watcher: Watcher<BasicTarget> = Watcher::new();
    /// }
    /// ```
    pub fn new() -> Self {
        Watcher {
            targets: Vec::new(),
            states: Vec::new(),
        }
    }

    /// Adds a target to the watcher.
    ///
    /// # Examples
    ///
    /// ```
    /// use fwatch::{BasicTarget, Watcher};
    ///
    /// fn main() {
    ///     let mut watcher : Watcher<BasicTarget> = Watcher::new();
    ///
    ///     // Watch the "foo.txt" path
    ///     watcher.add_target(BasicTarget::new("foo.txt"));
    /// }
    /// ```
    pub fn add_target(&mut self, target: W) {
        self.states.push(compute_state(&target));
        self.targets.push(target);
    }

    /// Remove a target from the watcher.
    ///
    /// This function will panic if index is greater than the size of self.states() / self.targets().
    ///
    /// # Examples
    ///
    /// ```
    /// use fwatch::{BasicTarget, Watcher};
    ///
    /// fn main() {
    ///     let mut watcher : Watcher<BasicTarget> = Watcher::new();
    ///
    ///     // Inserts "foo.txt" at index 0
    ///     watcher.add_target(BasicTarget::new("foo.txt"));
    ///
    ///     // Remove "foo.txt" from the watcher
    ///     assert!(watcher.remove_target(0));
    /// }
    /// ```
    pub fn remove_target(&mut self, index: usize) -> bool {
        if index > self.states.len() {
            false
        } else {
            self.states.remove(index);
            self.targets.remove(index);

            true
        }
    }

    /// Attempt to get the state corresponding to the given target index.
    ///
    /// Note that this doesn't update the current state.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use fwatch::{BasicTarget, Watcher, WatchState};
    ///
    /// fn main() {
    ///     let mut watcher : Watcher<BasicTarget> = Watcher::new();
    ///
    ///     // Watch a file that doesn't exist
    ///     watcher.add_target(BasicTarget::new("does_not_exist.txt"));
    ///     assert_eq!(watcher.get_state(0).unwrap(), &WatchState::DoesNotExist);
    ///
    ///     // Watch a file that does exist
    ///     watcher.add_target(BasicTarget::new("exists.txt"));
    ///     assert_ne!(watcher.get_state(1).unwrap(), &WatchState::DoesNotExist);
    /// }
    /// ```
    pub fn get_state(&self, index: usize) -> Option<&WatchState> {
        self.states.get(index)
    }

    /// Attempt to get the path corresponding to the given target index.
    ///
    /// # Examples
    ///
    /// ```
    /// use fwatch::{BasicTarget, Watcher, WatchState};
    ///
    /// fn main() {
    ///     let mut watcher : Watcher<BasicTarget> = Watcher::new();
    ///     watcher.add_target(BasicTarget::new("foo.txt"));
    ///
    ///     let path = watcher.get_path(0).unwrap();
    ///     assert_eq!(path.to_str().unwrap(), "foo.txt");
    /// }
    /// ```
    pub fn get_path(&self, index: usize) -> Option<&PathBuf> {
        self.targets.get(index).and_then(|v| Some(v.path()))
    }

    /// Observe any state transitions in our targets.
    ///
    /// Returns a vector containing the observed state transition for each target.
    ///
    /// # Examples
    ///
    /// ```
    /// use fwatch::{BasicTarget, Watcher, Transition};
    ///
    /// fn main() {
    ///     let mut watcher : Watcher<BasicTarget> = Watcher::new();
    ///
    ///     // Watch a file that doesn't exist
    ///     watcher.add_target(BasicTarget::new("does_not_exist.txt"));
    ///
    ///     let results = watcher.watch();
    ///
    ///     for (index, transition) in watcher.watch().into_iter().enumerate() {
    ///         // Get a reference to the path and state of the current target
    ///         let path = watcher.get_path(index).unwrap();
    ///         let state = watcher.get_state(index).unwrap();
    ///
    ///         match transition {
    ///             Transition::Created => { /* The watched file has been created */ },
    ///             Transition::Modified => { /* The watched file has been modified */ },
    ///             Transition::Deleted => { /* The watched file has been deleted */ },
    ///             Transition::None => { /* None of the above transitions were observed */ },
    ///         }
    ///     }
    /// }
    /// ```
    pub fn watch(&mut self) -> Vec<Transition> {
        let mut result = Vec::new();

        for (index, target) in self.targets.iter().enumerate() {
            let previous_state = self.states.get(index).unwrap();
            let current_state = compute_state(target);
            let mut transition = Transition::None;

            // Check for state transitions
            match (previous_state, &current_state) {
                // The file was created
                (WatchState::DoesNotExist, WatchState::Exists(_)) => {
                    transition = Transition::Created;
                }
                // The file was deleted
                (WatchState::Exists(_), WatchState::DoesNotExist) => {
                    transition = Transition::Deleted;
                }
                // The file was modified
                (WatchState::Exists(Some(t1)), WatchState::Exists(Some(t2))) if t1 != t2 => {
                    transition = Transition::Modified;
                }
                _ => {}
            };

            // now update our state vector
            *self.states.get_mut(index).unwrap() = current_state;

            result.push(transition);
        }

        result
    }
}

#[cfg(test)]
mod tests {
    use crate::{BasicTarget, Transition, Watcher};
    use std::io::{Error, Write};
    use std::thread::sleep;
    use std::time::Duration;
    use tempfile::NamedTempFile;

    #[test]
    /// Creates a temporary file and tests the modification + deletion transitions
    fn transitions() -> Result<(), Error> {
        let mut watcher: Watcher<BasicTarget> = Watcher::new();

        // Open a named temporary file & add it to our watcher
        let tmp = NamedTempFile::new()?;
        watcher.add_target(BasicTarget::new(tmp.path()));

        // We're going to modify our temporary file - to ensure the modification time
        // ono our temporary file changes, we wait a bit over a second before modifying
        sleep(Duration::from_millis(1500));

        {
            let mut handle = tmp.reopen()?;
            write!(handle, "test")?;
        }

        // The watcher should notice the modification transition
        assert_eq!(watcher.watch(), vec![Transition::Modified]);

        // Delete the temporary file
        tmp.close()?;

        // The watcher should observe the deletion transition
        assert_eq!(watcher.watch(), vec![Transition::Deleted]);

        Ok(())
    }
}