ds/
ds.rs

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
15/// Current implementation
16/// Expand upon the basic solution from ds4.rs.  Include proper error
17/// handling and replace unwrap() with ? where possible.  Increase
18/// functionality with additional command line options and windows
19/// support.
20
21pub 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
47/// VerboseErrors
48///
49/// Two flags to track whether files with errors should be printed or
50/// an informational message to the user.
51pub 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
107/// Traverse
108///
109/// Creates a Mutex of a BTreeMap and a VerboseErrors.  Supports scanning
110/// multiple directories.
111pub 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
133/// Visit_Dirs
134///
135/// Recursively searches a directory and returns Result<>. Ignores
136/// directories with errors and symlinks.
137pub 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
174/// Symlink_or_Error
175///
176/// Check if a path is a symlink.  Returns true if path is a symlink
177/// or if the metadata results in an error.
178fn 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
193/// Increment
194///
195/// Finds filesize for Linux and Windows.  Effectively skips files with
196/// errors.  Increment the size of the path and all ancestors.
197fn 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