fine_ill_do_it_myself/
lib.rs1#![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#[derive(Debug)]
28pub struct DirSizeTracker<WatcherImpl = RecommendedWatcher> {
29 state: Arc<Mutex<State>>,
30 _watcher: Arc<WatcherImpl>,
31}
32
33impl<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 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 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 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 } 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192#[non_exhaustive]
193pub enum Mode {
194 Metadata,
196}
197
198fn 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}