mod compare;
mod write;
use std::{
collections::BTreeMap,
fs::File,
io::{BufRead, BufReader, Write},
path::Path,
};
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle};
use once_cell::sync::Lazy;
use rayon::{
iter::{IntoParallelRefIterator, ParallelIterator},
ThreadPoolBuilder,
};
use regex::Regex;
use tabwriter::TabWriter;
use walkdir::{DirEntry, WalkDir};
pub use self::{compare::*, write::*};
use crate::{
hash_file,
utilities::{mul_str, relative_name},
Algorithm,
Error,
};
static SPINNER_STRINGS: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
pub fn create_hashes(
path: &Path,
ignored_files: Vec<String>,
algo: Algorithm,
depth: Option<usize>,
follow_symlinks: bool,
jobs: usize,
) -> BTreeMap<String, String> {
let mut walkdir = WalkDir::new(path).follow_links(follow_symlinks);
if let Some(depth) = depth {
walkdir = walkdir.max_depth(depth + 1);
}
let pb_style = ProgressStyle::default_bar()
.template("{prefix:.bold.dim} {spinner} {wide_bar} {pos:>7}/{len:7} ETA: {eta} - {msg}")
.tick_strings(&SPINNER_STRINGS);
let pb = ProgressBar::new_spinner();
pb.set_style(pb_style);
ThreadPoolBuilder::new()
.num_threads(jobs)
.build_global()
.unwrap();
let mut hashes = BTreeMap::new();
pb.enable_steady_tick(80);
pb.set_message("Finding files to hash...");
let mut files: Vec<DirEntry> = walkdir
.into_iter()
.filter_entry(|e: &walkdir::DirEntry| {
let filename = relative_name(path, e.path());
match (ignored_files.contains(&filename), e.file_type().is_file()) {
(true, true) => {
hashes.insert(mul_str("-", algo.hexlen()), filename);
false
}
(true, false) => false,
_ => true,
}
})
.flatten()
.filter(|e| e.file_type().is_file())
.collect();
optimize_file_order(&mut files);
pb.reset();
pb.set_length(files.len() as u64);
pb.set_message("Hashing files...");
let mut result: BTreeMap<String, String> = files
.par_iter()
.progress_with(pb)
.map(|e| {
let value = hash_file(algo, e.path());
let filename = relative_name(path, e.path());
(filename, value)
})
.collect();
hashes.append(&mut result);
hashes
}
#[cfg(target_os = "linux")]
fn optimize_file_order(dirs: &mut [DirEntry]) {
use walkdir::DirEntryExt;
dirs.sort_by(|a, b| {
let a_inode = a.ino();
let b_inode = b.ino();
a_inode.cmp(&b_inode)
});
}
#[cfg(not(target_os = "linux"))]
fn optimize_file_order(_dirs: &mut [DirEntry]) {}
pub fn write_hashes(out_file: &Path, algo: Algorithm, mut hashes: BTreeMap<String, String>) -> i32 {
let file = File::create(&out_file).unwrap();
let mut out = TabWriter::new(file);
hashes.insert(
out_file.to_string_lossy().to_string(),
mul_str("-", algo.hexlen()),
);
for (fname, hash) in hashes {
writeln!(&mut out, "{} {}", hash, fname).unwrap();
}
out.flush().expect("Failed to flush output file");
0
}
pub fn read_hashes(file: &Path) -> Result<BTreeMap<String, String>, Error> {
let mut hashes = BTreeMap::new();
let in_file = BufReader::new(File::open(&file).unwrap());
for line in in_file.lines().map(Result::unwrap) {
try_contains(&line, &mut hashes)?;
}
Ok(hashes)
}
fn try_contains(line: &str, hashes: &mut BTreeMap<String, String>) -> Result<(), Error> {
if line.is_empty() {
return Err(Error::HashesFileParsingFailure);
}
static LINE_RGX1: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)^([[:xdigit:]-]+)\s{2,}(.+?)$").unwrap());
static LINE_RGX2: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)^(.+?)\t{0,}\s{1,}([[:xdigit:]-]+)$").unwrap());
if let Some(captures) = LINE_RGX1.captures(line) {
hashes.insert(captures[2].to_string(), captures[1].to_uppercase());
return Ok(());
}
if let Some(captures) = LINE_RGX2.captures(line) {
hashes.insert(captures[1].to_string(), captures[2].to_uppercase());
return Ok(());
}
Err(Error::HashesFileParsingFailure)
}