use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use crate::error::{Error, Result};
use super::error_strategy::AnalysisErrorStrategy;
use super::progress::{AnalysisProgress, AnalysisProgressReporter};
use super::report::DuplicateGroup;
use super::util::{classify_io_error, default_concurrency};
use super::Entry;
pub(crate) struct HashOutcome {
pub(crate) groups: Vec<DuplicateGroup>,
pub(crate) groups_total: usize,
pub(crate) bytes_wasted: u64,
pub(crate) errors: Vec<(PathBuf, Error)>,
pub(crate) errors_total: usize,
}
pub(crate) async fn detect_duplicates(
candidates: Vec<Entry>,
concurrency: Option<usize>,
max_reported_groups: usize,
max_reported_errors: usize,
error_strategy: AnalysisErrorStrategy,
cancel: &CancellationToken,
reporter: &AnalysisProgressReporter,
) -> Result<HashOutcome> {
let mut by_size: HashMap<u64, Vec<Entry>> = HashMap::new();
for entry in candidates {
by_size.entry(entry.size).or_default().push(entry);
}
let to_hash: Vec<Entry> = by_size
.into_values()
.filter(|group| group.len() > 1)
.flatten()
.collect();
let concurrency = concurrency.unwrap_or_else(default_concurrency).max(1);
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = JoinSet::new();
for entry in to_hash {
if cancel.is_cancelled() {
return Err(Error::Cancelled);
}
let semaphore = Arc::clone(&semaphore);
let reporter = reporter.clone();
tasks.spawn(async move {
let permit = semaphore
.acquire_owned()
.await
.expect("semaphore is never closed");
let path = entry.path.clone();
let hash = tokio::task::spawn_blocking(move || hash_file(&path))
.await
.expect("hash task panicked");
drop(permit);
reporter.send(AnalysisProgress::EntryHashed {
path: entry.path.clone(),
});
(entry, hash)
});
}
let mut by_hash: HashMap<(u64, [u8; 32]), Vec<Entry>> = HashMap::new();
let mut errors = Vec::new();
let mut errors_total = 0usize;
while let Some(result) = tasks.join_next().await {
let (entry, hash) = result.expect("hash task panicked");
match hash {
Ok(hash) => {
by_hash.entry((entry.size, hash)).or_default().push(entry);
}
Err(err) => {
if error_strategy == AnalysisErrorStrategy::AbortOnError {
return Err(err);
}
errors_total += 1;
if errors.len() < max_reported_errors {
errors.push((entry.path, err));
}
}
}
}
let mut groups_total = 0usize;
let mut bytes_wasted = 0u64;
let mut groups = Vec::new();
for ((size, hash), entries) in by_hash {
if entries.len() < 2 {
continue;
}
groups_total += 1;
bytes_wasted += size * (entries.len() as u64 - 1);
if groups.len() < max_reported_groups {
groups.push(DuplicateGroup {
hash,
size,
paths: entries.into_iter().map(|e| e.path).collect(),
});
}
}
Ok(HashOutcome {
groups,
groups_total,
bytes_wasted,
errors,
errors_total,
})
}
fn hash_file(path: &Path) -> Result<[u8; 32]> {
let mut hasher = blake3::Hasher::new();
let mut file =
std::fs::File::open(path).map_err(|e| classify_io_error(e, path.to_path_buf()))?;
std::io::copy(&mut file, &mut hasher).map_err(|e| classify_io_error(e, path.to_path_buf()))?;
Ok(*hasher.finalize().as_bytes())
}