use clap::ArgMatches;
use std::collections::BTreeMap;
use std::fmt;
use std::fs;
use std::io;
#[cfg(target_os = "linux")]
use std::os::linux::fs::MetadataExt;
#[cfg(target_os = "windows")]
use std::os::windows::fs::MetadataExt;
use std::path::PathBuf;
use std::process;
use std::sync;
use std::sync::Mutex;
pub enum DSError {
IO(io::Error),
Mutex,
}
impl fmt::Display for DSError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &*self {
DSError::IO(err) => write!(f, "{}", err),
DSError::Mutex => write!(f, "Mutex poisoned"),
}
}
}
impl From<io::Error> for DSError {
fn from(err: io::Error) -> DSError {
DSError::IO(err)
}
}
impl<T> From<sync::PoisonError<T>> for DSError {
fn from(_: sync::PoisonError<T>) -> DSError {
DSError::Mutex
}
}
pub struct VerboseErrors {
pub verbose: bool,
once: bool,
}
impl VerboseErrors {
pub fn new() -> VerboseErrors {
VerboseErrors {
verbose: false,
once: true,
}
}
pub fn display(&mut self, path: &PathBuf, err: io::Error) {
if self.verbose {
eprintln!("{} {}", path.to_string_lossy().to_string(), err);
} else {
if self.once {
eprintln!("Use -v to see skipped files");
self.once = false;
}
}
}
}
pub struct FilesystemDevice {
pub enabled: bool,
pub device: u64,
}
impl FilesystemDevice {
pub fn new() -> FilesystemDevice {
FilesystemDevice {
enabled: false,
device: 0,
}
}
#[cfg(target_os = "windows")]
pub fn get(&mut self, _path: &PathBuf) -> u64 {
0
}
#[cfg(not(target_os = "windows"))]
pub fn get(&mut self, path: &PathBuf) -> u64 {
if self.enabled {
match path.metadata() {
Err(_) => 0,
Ok(metadata) => metadata.st_dev(),
}
} else {
0
}
}
}
pub fn traverse(anchors: &Vec<String>, matches: &ArgMatches) -> BTreeMap<String, u64> {
let mut mds = Mutex::new(BTreeMap::new());
let mut ve = VerboseErrors::new();
let mut fd = FilesystemDevice::new();
ve.verbose = matches.occurrences_of("verbose") > 0;
fd.enabled = matches.occurrences_of("one-filesystem") > 0;
for dir in anchors {
match visit_dirs(PathBuf::from(dir), &mut mds, &mut ve, &mut fd) {
Err(err) => {
eprintln!("Error: {}", err);
process::exit(1);
}
_ => (),
}
}
let disk_space = mds.lock().ok().unwrap().clone();
disk_space
}
pub fn visit_dirs(
dir: PathBuf,
mds: &mut Mutex<BTreeMap<String, u64>>,
ve: &mut VerboseErrors,
fd: &mut FilesystemDevice,
) -> Result<(), DSError> {
if dir.is_dir() {
let anchor = dir.to_owned();
let anchor_device = fd.get(&dir);
let contents = match fs::read_dir(&dir) {
Ok(contents) => contents,
Err(err) => {
ve.display(&dir, err);
return Ok(());
}
};
for entry in contents {
let entry = entry.unwrap();
let path = entry.path();
if symlink_or_error(&path, ve) {
continue;
}
if path.is_dir() {
if anchor_device != fd.get(&path) {
continue;
}
visit_dirs(path.to_owned(), mds, ve, fd)?;
} else {
increment(anchor.to_owned(), &mds, path, ve)?;
}
}
}
Ok(())
}
fn symlink_or_error(path: &PathBuf, ve: &mut VerboseErrors) -> bool {
match fs::symlink_metadata(&path) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return true;
}
}
Err(err) => {
ve.display(path, err);
return true;
}
}
false
}
fn increment(
anchor: PathBuf,
mds: &Mutex<BTreeMap<String, u64>>,
path: PathBuf,
ve: &mut VerboseErrors,
) -> Result<(), DSError> {
let filesize = match path.metadata() {
#[cfg(target_os = "linux")]
Ok(metadata) => metadata.st_size(),
#[cfg(target_os = "windows")]
Ok(metadata) => metadata.file_size(),
Err(err) => {
ve.display(&path, err);
0
}
};
for ancestor in path.ancestors() {
let ancestor_path = ancestor.to_string_lossy().to_string();
*mds.lock()?.entry(ancestor_path).or_insert(0) += filesize;
if anchor == ancestor {
break;
}
}
Ok(())
}
#[cfg(test)]
#[allow(unused_must_use)]
mod tests {
use super::*;
use std::io::{Error, ErrorKind};
use std::sync::PoisonError;
#[test]
fn display() {
let mut ve = VerboseErrors::new();
ve.verbose = false;
let err = Error::new(ErrorKind::Other, "example");
assert_eq!(ve.display(&PathBuf::from("/some/path"), err), ());
}
#[test]
fn display_verbose() {
let mut ve = VerboseErrors::new();
ve.verbose = true;
let err = Error::new(ErrorKind::Other, "example");
assert_eq!(ve.display(&PathBuf::from("/some/path"), err), ());
}
#[test]
fn increment_err() {
let anchor = PathBuf::from("/tmp");
let mds = Mutex::new(BTreeMap::new());
let path = PathBuf::from("/tmp/does_not_exist");
let mut ve = VerboseErrors::new();
mds.lock()
.unwrap()
.insert("/tmp/does_not_exist".to_string(), 0 as u64);
let result = increment(anchor, &mds, path, &mut ve).ok();
assert_eq!(result, Some(()));
assert_eq!(mds.lock().unwrap().get("/tmp/does_not_exist").unwrap(), &0);
}
#[test]
fn symlink_err() {
let path = PathBuf::from("/tmp/does_not_exist");
let mut ve = VerboseErrors::new();
assert_eq!(symlink_or_error(&path, &mut ve), true);
}
#[test]
fn fmt_dserror() {
let result = format!("{}", DSError::Mutex);
assert_eq!(result, "Mutex poisoned");
}
#[test]
fn cast_ioerror() {
fn nothing() -> DSError {
let err = Error::new(ErrorKind::Other, "example");
From::from(err)
}
let result = format!("{}", nothing());
assert_eq!(result, "example");
}
#[test]
fn cast_mutex_error() {
fn nothing() -> DSError {
let err = PoisonError::new(Mutex::new(1));
From::from(err)
}
let result = format!("{}", nothing());
assert_eq!(result, "Mutex poisoned");
}
#[cfg(target_os = "linux")]
#[test]
fn filesystem_device_disabled() {
let mut fd = FilesystemDevice::new();
let result = fd.get(&PathBuf::from("/tmp"));
assert_eq!(result, 0);
}
#[cfg(target_os = "linux")]
#[test]
fn filesystem_device_enabled() {
let mut fd = FilesystemDevice::new();
fd.enabled = true;
let result = fd.get(&PathBuf::from("/tmp"));
assert_ne!(result, 0);
}
#[cfg(target_os = "windows")]
#[test]
fn filesystem_device_enabled() {
let mut fd = FilesystemDevice::new();
fd.enabled = true;
let result = fd.get(&PathBuf::from("/Users"));
assert_eq!(result, 0);
}
#[cfg(target_os = "linux")]
#[test]
fn filesystem_device_error() {
let mut fd = FilesystemDevice::new();
fd.enabled = true;
let result = fd.get(&PathBuf::from("/doesnotexist"));
assert_eq!(result, 0);
}
}