use crate::{
hasher::{Algorithm, Hasher},
utils,
};
use anyhow::Result;
use indicatif::{ProgressBar, ProgressStyle};
use std::{
fs::File,
io::{BufReader, Read},
path::{Path, PathBuf},
sync::mpsc,
thread,
time::Duration,
};
use walkdir::WalkDir;
struct HashSpinner {
spinner: ProgressBar,
processed_bytes: usize,
}
impl HashSpinner {
fn new() -> Self {
let spinner = ProgressBar::new_spinner().with_message(HashSpinner::processed_bytes_msg(0));
spinner.set_style(
ProgressStyle::default_spinner()
.tick_strings(&utils::BOUNCING_BAR)
.template("{spinner:.white} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_spinner()),
);
spinner.enable_steady_tick(Duration::from_millis(100));
HashSpinner {
spinner,
processed_bytes: 0,
}
}
fn new_with_msg(msg: &str) -> Self {
let spinner = ProgressBar::new_spinner().with_message(format!("|{msg}|"));
spinner.set_style(
ProgressStyle::default_spinner()
.tick_strings(&utils::BOUNCING_BAR)
.template("{spinner:.white} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_spinner()),
);
spinner.enable_steady_tick(Duration::from_millis(100));
HashSpinner {
spinner,
processed_bytes: 0,
}
}
fn processed_bytes_msg(bytes: usize) -> String {
format!(
"|Processed: {}| Calculate hash sum... this may take a while",
utils::convert_bytes_to_human_readable(bytes)
)
}
fn finish_and_clear(self) {
self.spinner.finish_and_clear();
}
fn update(&mut self, bytes: usize) {
self.processed_bytes += bytes;
self.spinner
.set_message(HashSpinner::processed_bytes_msg(self.processed_bytes));
}
}
pub fn get_buffer_hash(buffer: &[u8], algorithm: Algorithm) -> String {
log::info!(
"Try to calculate {} hash for a given byte buffer of size: {}",
algorithm,
utils::convert_bytes_to_human_readable(buffer.len())
);
Hasher::new(algorithm).digest_hex_lower(buffer)
}
pub fn get_hash_for_object(
p: PathBuf,
algorithm: Algorithm,
include_names: bool,
) -> Result<String> {
log::info!(
"Try to calculate {} hash for {}: '{}'",
algorithm,
if p.is_dir() { "directory" } else { "file" },
utils::absolute_path_as_string(&p)
);
let (sender, receiver) = mpsc::channel();
let handle = thread::Builder::new()
.name("Hash-Worker-Thread".to_string())
.spawn(move || {
let result = if p.is_dir() {
hash_directory(p, algorithm, include_names)
} else {
hash_file(p, algorithm, include_names)
};
sender
.send(result)
.expect("Failed to send hash sum to main thread");
})
.map_err(|e| {
log::error!("Failed to spawn Hash-Worker-Thread - Details: {e:?}");
anyhow::anyhow!("Failed to spawn Hash-Worker-Thread.")
})?;
let result = receiver.recv().map_err(|e| {
log::error!("Failed to receive hash sum from Hash-Worker-Thread - Details: {e:?}");
anyhow::anyhow!("Failed to receive hash sum from Hash-Worker-Thread.")
});
handle.join().map_err(|e| {
log::error!("Failed to join Hash-Worker-Thread - Details: {e:?}");
anyhow::anyhow!("Failed to join Hash-Worker-Thread.")
})?;
result?
}
fn hash_file<P: AsRef<Path>>(file: P, algorithm: Algorithm, include_names: bool) -> Result<String> {
let file_path = file.as_ref();
let file = File::open(file_path).map_err(|io_err| {
let msg = format!(
"Failed to open file: {}",
utils::absolute_path_as_string(file_path),
);
log::error!("{msg} - Details: {io_err:?}");
anyhow::anyhow!(msg)
})?;
let mut reader = BufReader::with_capacity(utils::CAPACITY, file);
let mut hasher = Hasher::new(algorithm);
let mut spinner = HashSpinner::new();
if include_names {
if let Some(file_name) = file_path.file_name() {
hasher.update(file_name.to_string_lossy().as_bytes());
}
}
let mut buf = [0u8; utils::CAPACITY];
let result = loop {
match reader.read(&mut buf) {
Ok(n) => {
if n == 0 {
break Ok(());
}
hasher.update(&buf[..n]);
spinner.update(n);
}
Err(io_err) => {
let msg = format!(
"Failed to read from file: {}",
utils::absolute_path_as_string(file_path),
);
log::error!("{msg} - Details: {io_err:?}");
break Err(anyhow::anyhow!(msg));
}
}
};
spinner.finish_and_clear();
result?;
Ok(hex::encode(hasher.finalize()))
}
fn hash_directory<P: AsRef<Path>>(
dir: P,
algorithm: Algorithm,
include_names: bool,
) -> Result<String> {
let root = dir.as_ref();
let mut spinner = HashSpinner::new_with_msg("Read directory recursively");
let entries: Vec<_> = WalkDir::new(root)
.sort_by_key(|e| e.path().to_path_buf()) .max_depth(usize::MAX)
.into_iter()
.filter_map(Result::ok)
.filter(|entry| entry.path() != root) .collect();
let mut hasher = Hasher::new(algorithm);
if include_names {
if let Some(root_name) = root.file_name() {
hasher.update(root_name.to_string_lossy().as_bytes());
}
}
let mut result: Result<()> = Result::Ok(());
let mut buf = [0u8; utils::CAPACITY];
for entry in entries {
let path = entry.path();
if include_names {
let relative_path = match path.strip_prefix(root) {
Ok(relative_path) => relative_path,
Err(err) => {
let msg = format!(
"Failed to strip prefix from path: {}",
utils::absolute_path_as_string(path),
);
log::error!("{msg} - Details: {err:?}");
result = Err(anyhow::anyhow!(msg));
break;
}
};
hasher.update(relative_path.to_string_lossy().as_bytes());
}
if path.is_file() {
match File::open(path) {
Ok(file) => {
let mut reader = BufReader::with_capacity(utils::CAPACITY, file);
let r = loop {
match reader.read(&mut buf) {
Ok(n) => {
if n == 0 {
break Ok(());
}
hasher.update(&buf[..n]);
spinner.update(n);
}
Err(io_err) => {
let msg = format!(
"Failed to read from file: {}",
utils::absolute_path_as_string(path),
);
log::error!("{msg} - Details: {io_err:?}");
break Err(anyhow::anyhow!(msg));
}
}
};
if let Err(io_err) = r {
result = Err(io_err);
break;
}
}
Err(io_err) => {
let msg = format!(
"Failed to open file: {}",
utils::absolute_path_as_string(path),
);
log::error!("{msg} - Details: {io_err:?}");
result = Err(anyhow::anyhow!(msg));
break;
}
}
}
}
spinner.finish_and_clear();
result?;
Ok(hex::encode(hasher.finalize()))
}