fine_ill_do_it_myself/
lib.rs

1//! This crate provides the [`DirSizeTracker`] struct, which efficiently keeps track of a directory's total size using filesystem events.
2//!
3//! See the list of known problems in the docs of [`notify`].
4
5#![warn(missing_docs)]
6#![warn(clippy::arithmetic_side_effects)]
7
8mod error_data;
9#[cfg(test)]
10mod tests;
11
12use error_data::ErrorData;
13pub use notify;
14
15use std::{
16    collections::HashMap,
17    path::{Path, PathBuf},
18    sync::{Arc, Mutex},
19};
20
21use jwalk::WalkDir;
22use notify::{Config, Event, EventHandler, RecommendedWatcher, RecursiveMode, Watcher};
23
24/// Keeps track of a directory's total size.
25///
26/// The total size is updated in a separate thread using the specified implementation of [`Watcher`].
27#[derive(Debug)]
28pub struct DirSizeTracker<WatcherImpl = RecommendedWatcher> {
29    state: Arc<Mutex<State>>,
30    _watcher: Arc<WatcherImpl>,
31}
32
33// Implement Clone without requiring WatchingImpl to implement Clone
34impl<WatcherImpl> Clone for DirSizeTracker<WatcherImpl> {
35    fn clone(&self) -> Self {
36        DirSizeTracker {
37            state: Arc::clone(&self.state),
38            _watcher: Arc::clone(&self._watcher),
39        }
40    }
41}
42
43impl<WatcherImpl: Watcher> DirSizeTracker<WatcherImpl> {
44    /// Starts tracking the size of the given directory.
45    ///
46    /// For best performance, this should only be called once.
47    pub fn new(path: PathBuf, mode: Mode) -> Result<Self, Error> {
48        Self::try_new(path, mode).map_err(Error)
49    }
50
51    fn try_new(path: PathBuf, mode: Mode) -> Result<Self, ErrorData> {
52        // The initial scan must be done after the watcher starts so that any events occurring during the scan will be handled.
53        let state = Arc::new(Mutex::new(State {
54            path,
55            mode,
56            need_scan: true,
57            sizes: HashMap::new(),
58            total_size: 0,
59            error: None,
60        }));
61        let mut watcher =
62            WatcherImpl::new(create_event_handler(Arc::clone(&state)), Config::default())?;
63        watcher.watch(&state.lock().unwrap().path, RecursiveMode::Recursive)?;
64        let dir_size_tracker = DirSizeTracker {
65            state,
66            _watcher: Arc::new(watcher),
67        };
68        Ok(dir_size_tracker)
69    }
70
71    /// Returns the current size of the directory.
72    ///
73    /// If an error occured when responding to a filesystem event, then the error is returned, the size tracking starts over, and the next call to this function will return the result of a re-attempt.
74    pub fn get_total_size(&self) -> Result<u64, Error> {
75        let mut state = self.state.lock().unwrap();
76        if state.need_scan {
77            state.need_scan = false;
78            state.error = state.scan().err();
79        }
80        if let Some(error) = state.error.take() {
81            state.need_scan = true;
82            return Err(Error(error));
83        }
84        Ok(state.total_size)
85    }
86}
87
88fn create_event_handler(state: Arc<Mutex<State>>) -> impl EventHandler {
89    move |result| {
90        let mut state = state.lock().unwrap();
91        if state.error.is_none() {
92            state.error = state.handle_event(result).err();
93        }
94    }
95}
96
97#[derive(Debug)]
98struct State {
99    path: PathBuf,
100    mode: Mode,
101    need_scan: bool,
102    sizes: HashMap<PathBuf, u64>,
103    total_size: u64,
104    error: Option<ErrorData>,
105}
106
107impl State {
108    fn scan(&mut self) -> Result<(), ErrorData> {
109        self.total_size = 0;
110        self.sizes = WalkDir::new(&self.path)
111            .skip_hidden(false)
112            .into_iter()
113            .map(|result| {
114                let path = result?.path();
115                let size = get_size(&path, self.mode)?.unwrap_or(0);
116                self.add_size(size)?;
117                Ok((path, size))
118            })
119            .collect::<Result<_, ErrorData>>()?;
120        Ok(())
121    }
122
123    fn handle_event(&mut self, result: Result<Event, notify::Error>) -> Result<(), ErrorData> {
124        let event = result?;
125        if event.need_rescan() || self.need_scan {
126            self.need_scan = false;
127            self.scan()?;
128        } else if event.kind.is_access() {
129            // No modification was made
130        } else {
131            for path in event.paths {
132                let new_size = get_size(&path, self.mode)?;
133                let old_size = if let Some(new_size) = new_size {
134                    self.sizes.insert(path, new_size)
135                } else {
136                    self.sizes.remove(&path)
137                };
138                self.subtract_size(old_size.unwrap_or(0))?;
139                self.add_size(new_size.unwrap_or(0))?;
140            }
141        }
142        Ok(())
143    }
144
145    fn add_size(&mut self, n: u64) -> Result<(), ErrorData> {
146        self.total_size = self
147            .total_size
148            .checked_add(n)
149            .ok_or(ErrorData::IntOverflow)?;
150        Ok(())
151    }
152
153    fn subtract_size(&mut self, n: u64) -> Result<(), ErrorData> {
154        self.total_size = self
155            .total_size
156            .checked_sub(n)
157            .ok_or(ErrorData::IntUnderflow)?;
158        Ok(())
159    }
160}
161
162/// Error that occurs in [`DirSizeTracker::new`] or when responding to filesystem events.
163#[derive(Debug)]
164pub struct Error(ErrorData);
165
166impl std::fmt::Display for Error {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self.0 {
169            ErrorData::Io(ref error) => write!(f, "{error}"),
170            ErrorData::Jwalk(_) => write!(f, "failed to scan directory"),
171            ErrorData::Notify(_) => write!(f, "failed to get filesystem events"),
172            ErrorData::IntOverflow => write!(f, "total size is greater than `u64::MAX`"),
173            ErrorData::IntUnderflow => write!(f, "calculated total size became negative"),
174        }
175    }
176}
177
178impl std::error::Error for Error {
179    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
180        match self.0 {
181            ErrorData::Io(ref error) => std::error::Error::source(error),
182            ErrorData::Jwalk(ref error) => Some(error),
183            ErrorData::Notify(ref error) => Some(error),
184            ErrorData::IntOverflow => None,
185            ErrorData::IntUnderflow => None,
186        }
187    }
188}
189
190/// Specifies the method used for calculating file sizes.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192#[non_exhaustive]
193pub enum Mode {
194    /// Uses [`std::fs::Metadata::len`] for each file and folder.
195    Metadata,
196}
197
198/// Returns the file's size, or `None` if it doesn't exist
199fn get_size(path: &Path, mode: Mode) -> Result<Option<u64>, ErrorData> {
200    match try_get_size(path, mode) {
201        Ok(value) => Ok(Some(value)),
202        Err(ErrorData::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
203        Err(error) => Err(error),
204    }
205}
206
207fn try_get_size(path: &Path, mode: Mode) -> Result<u64, ErrorData> {
208    let metadata = std::fs::symlink_metadata(path)?;
209    let size = match mode {
210        Mode::Metadata => metadata.len(),
211    };
212    Ok(size)
213}