nullnet_libconfmon/watcher/
impl.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
use super::{
    types::{FileData, FileInfo, Snapshot},
    utils::{get_mtime, make_error_mapper},
};
use crate::{Error, ErrorKind};
use std::{future::Future, path::PathBuf, time::Duration};

/// A file watcher that monitors changes in a list of files and triggers a callback when changes are detected.
///
/// # Generics
/// - `F`: A closure or function that takes a `Snapshot` and produces a future.
/// - `Fut`: The type of the future returned by the callback.
///
/// # Fields
/// - `files`: A list of `FileInfo` containing metadata about the monitored files.
/// - `callback`: A closure or function to execute when changes are detected.
/// - `poll_interval`: The interval (in milliseconds) at which files are polled for changes.
pub struct Watcher<F, Fut>
where
    F: Fn(Snapshot) -> Fut,
    Fut: Future<Output = ()>,
{
    files: Vec<FileInfo>,
    callback: F,
    poll_interval: u64,
}

impl<F, Fut> Watcher<F, Fut>
where
    F: Fn(Snapshot) -> Fut,
    Fut: Future<Output = ()>,
{
    /// Creates a new `Watcher` instance.
    ///
    /// # Parameters
    /// - `paths`: A list of paths to the files to monitor.
    /// - `poll_interval`: The interval (in milliseconds) at which files are polled for changes.
    /// - `callback`: A closure or function to execute when changes are detected.
    ///
    /// # Returns
    /// - `Ok(Self)`: A new instance of `Watcher` if all files are successfully initialized.
    /// - `Err(Error)`: An error if any file cannot be initialized (e.g., failed to read metadata).
    ///
    /// # Errors
    /// - Returns an error with `ErrorKind::ErrorInitializingWatcher` if a file's metadata cannot be read.
    pub async fn new(paths: Vec<PathBuf>, poll_interval: u64, callback: F) -> Result<Self, Error> {
        let mut files = Vec::new();

        for path in paths {
            let mtime = get_mtime(&path)
                .await
                .map_err(make_error_mapper(ErrorKind::ErrorInitializingWatcher))?;

            files.push(FileInfo { path, mtime });
        }

        Ok(Self {
            files,
            callback,
            poll_interval,
        })
    }

    /// Starts monitoring the files for changes.
    ///
    /// # Behavior
    /// - Periodically checks the modification time of each file.
    /// - If any file has been modified since the last check, triggers the `callback` with a `Snapshot`.
    /// - Continues indefinitely until the task is canceled.
    ///
    /// # Returns
    /// - `Ok(())`: This function only exits if the task is canceled or an error occurs.
    /// - `Err(Error)`: An error if reading a file's metadata or content fails.
    ///
    /// # Errors
    /// - Returns an error with `ErrorKind::ErrorReadingFile` if file metadata or content cannot be read.
    pub async fn watch(&mut self) -> Result<(), Error> {
        loop {
            let mut should_upload = false;
            for file in &mut self.files {
                let current = get_mtime(&file.path).await?;

                if current > file.mtime {
                    file.mtime = current;
                    should_upload = true;
                }
            }

            if should_upload {
                let snapshot = self.snapshot().await?;
                (self.callback)(snapshot).await;
            }

            tokio::time::sleep(Duration::from_millis(self.poll_interval)).await;
        }
    }

    /// Generates a snapshot of the current state of the monitored files.
    ///
    /// # Returns
    /// - `Ok(Snapshot)`: A snapshot containing the data of all monitored files.
    /// - `Err(Error)`: An error if any file cannot be read.
    ///
    /// # Errors
    /// - Returns an error with `ErrorKind::ErrorReadingFile` if a file cannot be read.
    pub async fn snapshot(&self) -> Result<Snapshot, Error> {
        let mut snapshot = Snapshot::new();

        for file in &self.files {
            let content = tokio::fs::read(&file.path)
                .await
                .map_err(make_error_mapper(ErrorKind::ErrorReadingFile))?;

            let filename = file
                .path
                .file_name()
                .unwrap_or(file.path.as_os_str())
                .to_string_lossy()
                .into_owned();

            snapshot.push(FileData { filename, content });
        }

        Ok(snapshot)
    }
}