1use clap::ArgMatches;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::fs;
5use std::io;
6#[cfg(target_os = "linux")]
7use std::os::linux::fs::MetadataExt;
8#[cfg(target_os = "windows")]
9use std::os::windows::fs::MetadataExt;
10use std::path::PathBuf;
11use std::process;
12use std::sync;
13use std::sync::Mutex;
14
15pub enum DSError {
22 IO(io::Error),
23 Mutex,
24}
25
26impl fmt::Display for DSError {
27 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28 match &*self {
29 DSError::IO(err) => write!(f, "{}", err),
30 DSError::Mutex => write!(f, "Mutex poisoned"),
31 }
32 }
33}
34
35impl From<io::Error> for DSError {
36 fn from(err: io::Error) -> DSError {
37 DSError::IO(err)
38 }
39}
40
41impl<T> From<sync::PoisonError<T>> for DSError {
42 fn from(_: sync::PoisonError<T>) -> DSError {
43 DSError::Mutex
44 }
45}
46
47pub struct VerboseErrors {
52 pub verbose: bool,
53 once: bool,
54}
55
56impl VerboseErrors {
57 pub fn new() -> VerboseErrors {
58 VerboseErrors {
59 verbose: false,
60 once: true,
61 }
62 }
63
64 pub fn display(&mut self, path: &PathBuf, err: io::Error) {
65 if self.verbose {
66 eprintln!("{} {}", path.to_string_lossy().to_string(), err);
67 } else {
68 if self.once {
69 eprintln!("Use -v to see skipped files");
70 self.once = false;
71 }
72 }
73 }
74}
75
76pub struct FilesystemDevice {
77 pub enabled: bool,
78 pub device: u64,
79}
80
81impl FilesystemDevice {
82 pub fn new() -> FilesystemDevice {
83 FilesystemDevice {
84 enabled: false,
85 device: 0,
86 }
87 }
88
89 #[cfg(target_os = "windows")]
90 pub fn get(&mut self, _path: &PathBuf) -> u64 {
91 0
92 }
93
94 #[cfg(not(target_os = "windows"))]
95 pub fn get(&mut self, path: &PathBuf) -> u64 {
96 if self.enabled {
97 match path.metadata() {
98 Err(_) => 0,
99 Ok(metadata) => metadata.st_dev(),
100 }
101 } else {
102 0
103 }
104 }
105}
106
107pub fn traverse(anchors: &Vec<String>, matches: &ArgMatches) -> BTreeMap<String, u64> {
112 let mut mds = Mutex::new(BTreeMap::new());
113 let mut ve = VerboseErrors::new();
114 let mut fd = FilesystemDevice::new();
115
116 ve.verbose = matches.occurrences_of("verbose") > 0;
117 fd.enabled = matches.occurrences_of("one-filesystem") > 0;
118
119 for dir in anchors {
120 match visit_dirs(PathBuf::from(dir), &mut mds, &mut ve, &mut fd) {
121 Err(err) => {
122 eprintln!("Error: {}", err);
123 process::exit(1);
124 }
125 _ => (),
126 }
127 }
128
129 let disk_space = mds.lock().ok().unwrap().clone();
130 disk_space
131}
132
133pub fn visit_dirs(
138 dir: PathBuf,
139 mds: &mut Mutex<BTreeMap<String, u64>>,
140 ve: &mut VerboseErrors,
141 fd: &mut FilesystemDevice,
142) -> Result<(), DSError> {
143 if dir.is_dir() {
144 let anchor = dir.to_owned();
145 let anchor_device = fd.get(&dir);
146 let contents = match fs::read_dir(&dir) {
147 Ok(contents) => contents,
148 Err(err) => {
149 ve.display(&dir, err);
150 return Ok(());
151 }
152 };
153 for entry in contents {
154 let entry = entry.unwrap();
155 let path = entry.path();
156
157 if symlink_or_error(&path, ve) {
158 continue;
159 }
160
161 if path.is_dir() {
162 if anchor_device != fd.get(&path) {
163 continue;
164 }
165 visit_dirs(path.to_owned(), mds, ve, fd)?;
166 } else {
167 increment(anchor.to_owned(), &mds, path, ve)?;
168 }
169 }
170 }
171 Ok(())
172}
173
174fn symlink_or_error(path: &PathBuf, ve: &mut VerboseErrors) -> bool {
179 match fs::symlink_metadata(&path) {
180 Ok(metadata) => {
181 if metadata.file_type().is_symlink() {
182 return true;
183 }
184 }
185 Err(err) => {
186 ve.display(path, err);
187 return true;
188 }
189 }
190 false
191}
192
193fn increment(
198 anchor: PathBuf,
199 mds: &Mutex<BTreeMap<String, u64>>,
200 path: PathBuf,
201 ve: &mut VerboseErrors,
202) -> Result<(), DSError> {
203 let filesize = match path.metadata() {
204 #[cfg(target_os = "linux")]
205 Ok(metadata) => metadata.st_size(),
206 #[cfg(target_os = "windows")]
207 Ok(metadata) => metadata.file_size(),
208 Err(err) => {
209 ve.display(&path, err);
210 0
211 }
212 };
213 for ancestor in path.ancestors() {
214 let ancestor_path = ancestor.to_string_lossy().to_string();
215 *mds.lock()?.entry(ancestor_path).or_insert(0) += filesize;
216 if anchor == ancestor {
217 break;
218 }
219 }
220 Ok(())
221}
222
223#[cfg(test)]
224#[allow(unused_must_use)]
225mod tests {
226 use super::*;
227 use std::io::{Error, ErrorKind};
228 use std::sync::PoisonError;
229
230 #[test]
231 fn display() {
232 let mut ve = VerboseErrors::new();
233 ve.verbose = false;
234 let err = Error::new(ErrorKind::Other, "example");
235 assert_eq!(ve.display(&PathBuf::from("/some/path"), err), ());
236 }
237
238 #[test]
239 fn display_verbose() {
240 let mut ve = VerboseErrors::new();
241 ve.verbose = true;
242 let err = Error::new(ErrorKind::Other, "example");
243 assert_eq!(ve.display(&PathBuf::from("/some/path"), err), ());
244 }
245
246 #[test]
247 fn increment_err() {
248 let anchor = PathBuf::from("/tmp");
249 let mds = Mutex::new(BTreeMap::new());
250 let path = PathBuf::from("/tmp/does_not_exist");
251 let mut ve = VerboseErrors::new();
252
253 mds.lock()
254 .unwrap()
255 .insert("/tmp/does_not_exist".to_string(), 0 as u64);
256 let result = increment(anchor, &mds, path, &mut ve).ok();
257 assert_eq!(result, Some(()));
258 assert_eq!(mds.lock().unwrap().get("/tmp/does_not_exist").unwrap(), &0);
259 }
260
261 #[test]
262 fn symlink_err() {
263 let path = PathBuf::from("/tmp/does_not_exist");
264 let mut ve = VerboseErrors::new();
265 assert_eq!(symlink_or_error(&path, &mut ve), true);
266 }
267
268 #[test]
269 fn fmt_dserror() {
270 let result = format!("{}", DSError::Mutex);
271 assert_eq!(result, "Mutex poisoned");
272 }
273
274 #[test]
275 fn cast_ioerror() {
276 fn nothing() -> DSError {
277 let err = Error::new(ErrorKind::Other, "example");
278 From::from(err)
279 }
280
281 let result = format!("{}", nothing());
282 assert_eq!(result, "example");
283 }
284
285 #[test]
286 fn cast_mutex_error() {
287 fn nothing() -> DSError {
288 let err = PoisonError::new(Mutex::new(1));
289 From::from(err)
290 }
291
292 let result = format!("{}", nothing());
293 assert_eq!(result, "Mutex poisoned");
294 }
295
296 #[cfg(target_os = "linux")]
297 #[test]
298 fn filesystem_device_disabled() {
299 let mut fd = FilesystemDevice::new();
300 let result = fd.get(&PathBuf::from("/tmp"));
301 assert_eq!(result, 0);
302 }
303
304 #[cfg(target_os = "linux")]
305 #[test]
306 fn filesystem_device_enabled() {
307 let mut fd = FilesystemDevice::new();
308 fd.enabled = true;
309 let result = fd.get(&PathBuf::from("/tmp"));
310 assert_ne!(result, 0);
311 }
312
313 #[cfg(target_os = "windows")]
314 #[test]
315 fn filesystem_device_enabled() {
316 let mut fd = FilesystemDevice::new();
317 fd.enabled = true;
318 let result = fd.get(&PathBuf::from("/Users"));
319 assert_eq!(result, 0);
320 }
321
322 #[cfg(target_os = "linux")]
323 #[test]
324 fn filesystem_device_error() {
325 let mut fd = FilesystemDevice::new();
326 fd.enabled = true;
327 let result = fd.get(&PathBuf::from("/doesnotexist"));
328 assert_eq!(result, 0);
329 }
330
331}
332