use blake3::Hash;
use filetime::FileTime;
use std::{
collections::{
hash_map::Entry::{Occupied, Vacant},
HashMap, VecDeque,
},
fs::File,
io::{self, BufRead, BufReader},
path::{Path, PathBuf},
sync::mpsc::{self, Receiver, SyncSender},
};
use walkdir::WalkDir;
mod db;
pub use db::{Entry, HashDb};
use crate::db::retry_on_busy;
#[derive(Debug)]
pub struct Params {
lower_limit: u64,
roots: Vec<PathBuf>,
db: PathBuf,
}
impl Params {
pub fn new(lower_limit: u64, roots: Vec<PathBuf>, db: PathBuf) -> Self {
Self { lower_limit, roots, db }
}
pub fn roots(&self) -> &[PathBuf] {
&self.roots
}
pub fn db_path(&self) -> &Path {
&self.db
}
}
pub type Duplicates = HashMap<(u64, Hash), Vec<PathBuf>>;
pub struct Stats {
pub duplicates: Duplicates,
pub total_files_processed: usize,
pub total_bytes_processed: u64,
}
pub fn find(params: &Params) -> io::Result<Stats> {
let num_threads = num_cpus::get().min(16);
let fds = rlimit::getrlimit(rlimit::Resource::NOFILE)?.0 as usize - 4 * num_threads;
let (tx, rx) = mpsc::sync_channel(fds);
let mut threads = VecDeque::with_capacity(num_threads);
for _ in 0..num_threads {
let (thread_tx, thread_rx) = mpsc::sync_channel(fds / num_threads);
let db = params.db.clone();
let tx = tx.clone();
let handle = std::thread::spawn(move || hasher_task(db, thread_rx, tx));
threads.push_back((handle, thread_tx));
}
let collector = std::thread::spawn(|| collect(rx));
drop(tx);
let mut total_files_processed = 0;
let mut total_bytes_processed = 0;
let mut next_worker = 0;
for root in ¶ms.roots {
for entry in WalkDir::new(root) {
let mut path = match entry {
Ok(p) => p.into_path(),
Err(e) => {
eprintln!("io error occured: {}", e);
continue;
}
};
if path.is_dir() || path.is_symlink() {
continue;
}
let md = match path.metadata() {
Ok(md) => md,
Err(e) => {
eprintln!("io error when reading metadata {}: {}", path.display(), e);
continue;
}
};
let size = md.len();
if size < params.lower_limit {
continue;
}
let mtime = FileTime::from_last_modification_time(&md);
let mut file = match File::open(&path) {
Ok(f) => f,
Err(e) => {
eprintln!("failed to open {}: {}", path.display(), e);
continue;
}
};
total_files_processed += 1;
total_bytes_processed += size;
'outer: loop {
for _ in 0..threads.len() {
let tx = &threads[next_worker].1;
next_worker = (next_worker + 1) % num_threads;
match tx.try_send((path, file, mtime)) {
Ok(()) => break 'outer,
Err(mpsc::TrySendError::Full((p, f, _)))
| Err(mpsc::TrySendError::Disconnected((p, f, _))) => {
path = p;
file = f;
}
}
}
std::thread::yield_now();
}
}
}
for (t, rx) in threads {
drop(rx);
t.join().expect("failed to join with thread").expect("db operation failed");
}
Ok(Stats {
duplicates: collector.join().expect("failed to join with collector"),
total_files_processed,
total_bytes_processed,
})
}
fn hash_file(file: File) -> io::Result<(u64, Hash)> {
let mut reader = BufReader::with_capacity(16 * 1024 * 1024, file);
let mut hasher = blake3::Hasher::new();
let mut size = 0;
loop {
let data = reader.fill_buf()?;
let len = data.len();
if len == 0 {
return Ok((size, hasher.finalize()));
} else {
hasher.update(data);
reader.consume(len);
size += len as u64;
}
}
}
fn hasher_task(
db: PathBuf,
tasks: Receiver<(PathBuf, File, FileTime)>,
tx: SyncSender<(PathBuf, io::Result<(u64, Hash)>)>,
) -> rusqlite::Result<()> {
let db = db::HashDb::try_new(db)?;
while let Ok((path, file, mtime)) = tasks.recv() {
let res = match retry_on_busy(|| db.select(&path))? {
Some(entry) if entry.mtime == mtime.unix_seconds() => Ok((entry.size, entry.hash)),
_ => {
let res = hash_file(file);
if let Ok((size, hash)) = res {
let entry = db::Entry { path: &path, mtime: mtime.unix_seconds(), size, hash };
retry_on_busy(|| db.insert(&entry))?;
res
} else {
res
}
}
};
if tx.send((path, res)).is_err() {
eprintln!("failed to send hash, quiting...");
break;
}
}
Ok(())
}
fn collect(rx: Receiver<(PathBuf, io::Result<(u64, Hash)>)>) -> Duplicates {
let mut entries: Duplicates = HashMap::new();
while let Ok((path, res)) = rx.recv() {
let (size, hash) = match res {
Ok(h) => h,
Err(e) => {
eprintln!("failed to read from {}: {}", path.display(), e);
continue;
}
};
match entries.entry((size, hash)) {
Occupied(mut v) => v.get_mut().push(path),
Vacant(v) => {
v.insert(vec![path]);
}
}
}
entries
}