use colored::*;
use crossbeam_channel::bounded;
use dashmap::DashMap;
pub use rayon::prelude::*; use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use walkdir::{DirEntry, WalkDir};
pub mod cache;
pub use cache::Cache;
pub fn is_file(entry: &DirEntry) -> bool {
entry.file_type().is_file()
}
pub fn compute_dir_sizes(base: &Path) -> HashMap<PathBuf, u64> {
let (sizes, _, _) = compute_dir_sizes_with_progress(base, true);
sizes
}
pub fn compute_dir_sizes_with_cache(
base: &Path,
quiet: bool,
use_cache: bool,
max_cache_age_hours: u64,
) -> (HashMap<PathBuf, u64>, usize, Duration) {
if use_cache {
if let Ok(cache) = Cache::new() {
if let Ok(Some(cached_entry)) = cache.retrieve(base, max_cache_age_hours * 3600) {
if !quiet {
eprintln!(
"🚀 {} {}",
"Using cached results for".bright_green().bold(),
base.display().to_string().bright_white()
);
}
return (
cached_entry.sizes,
cached_entry.total_files,
Duration::from_secs(0),
);
}
if let Some(parent) = base.parent() {
if let Ok(Some(parent_cache)) = cache.retrieve(parent, max_cache_age_hours * 3600) {
if let Some((filtered_sizes, file_count)) =
cache.can_use_for_subdir(&parent_cache, base)
{
if !quiet {
eprintln!(
"🚀 {} {}",
"Using parent cache for".bright_green().bold(),
base.display().to_string().bright_white()
);
}
return (filtered_sizes, file_count, Duration::from_secs(0));
}
}
}
let (sizes, total_files, duration) =
compute_dir_sizes_with_progress_internal(base, quiet);
if let Err(e) = cache.store(base, &sizes, total_files) {
if !quiet {
eprintln!("⚠️ Warning: Failed to store cache: {e}");
}
} else if !quiet {
eprintln!(
"💾 {} {}",
"Cached results for".bright_blue(),
base.display().to_string().bright_white()
);
}
(sizes, total_files, duration)
} else {
compute_dir_sizes_with_progress_internal(base, quiet)
}
} else {
compute_dir_sizes_with_progress_internal(base, quiet)
}
}
pub fn compute_dir_sizes_with_progress(
base: &Path,
quiet: bool,
) -> (HashMap<PathBuf, u64>, usize, Duration) {
compute_dir_sizes_with_progress_internal(base, quiet)
}
fn compute_dir_sizes_with_progress_internal(
base: &Path,
_quiet: bool,
) -> (HashMap<PathBuf, u64>, usize, Duration) {
let start_time = Instant::now();
let estimated_dirs = 1000; let sizes = Arc::new(DashMap::with_capacity(estimated_dirs));
#[cfg(all(unix, not(test)))]
let seen_inodes = Arc::new(DashMap::<(u64, u64), bool>::with_capacity(
estimated_dirs / 10,
)); let file_count = Arc::new(AtomicUsize::new(0));
let (tx, rx) = bounded(num_cpus::get() * 500); let base_path = base.to_path_buf();
let walker_thread = {
let tx = tx.clone();
let base_path = base_path.clone();
thread::spawn(move || {
let mut count = 0;
for entry in WalkDir::new(&base_path)
.follow_links(false)
.into_iter()
.filter_map(Result::ok)
.filter(is_file)
{
count += 1;
if tx.send(entry).is_err() {
break; }
}
count
})
};
let num_workers = num_cpus::get().min(8); let mut worker_handles = Vec::with_capacity(num_workers);
for _ in 0..num_workers {
let rx = rx.clone();
let sizes = Arc::clone(&sizes);
#[cfg(all(unix, not(test)))]
let seen_inodes = Arc::clone(&seen_inodes);
let file_count = Arc::clone(&file_count);
let base_path = base_path.clone();
let handle = thread::spawn(move || {
while let Ok(entry) = rx.recv() {
if let Ok(metadata) = entry.metadata() {
#[cfg(all(unix, not(test)))]
let mut file_size = metadata.len();
#[cfg(any(not(unix), test))]
let file_size = metadata.len();
file_count.fetch_add(1, Ordering::Relaxed);
#[cfg(all(unix, not(test)))]
{
use std::os::unix::fs::MetadataExt;
let inode = metadata.ino();
let device = metadata.dev();
let key = (device, inode);
if metadata.nlink() > 1 && seen_inodes.insert(key, true).is_some() {
file_size = 0;
}
}
if file_size > 0 {
let mut current = entry.path();
while let Some(parent) = current.parent() {
if !parent.starts_with(&base_path) {
break;
}
match sizes.get_mut(parent) {
Some(mut existing_size) => {
*existing_size += file_size;
}
None => {
sizes.insert(parent.to_path_buf(), file_size);
}
}
current = parent;
}
}
}
}
});
worker_handles.push(handle);
}
drop(tx);
let _total_files = walker_thread.join().unwrap_or(0);
for handle in worker_handles {
let _ = handle.join();
}
let final_sizes: HashMap<PathBuf, u64> = Arc::try_unwrap(sizes)
.unwrap_or_else(|_| panic!("Failed to unwrap Arc"))
.into_iter()
.collect();
let final_file_count = file_count.load(Ordering::Relaxed);
let mut result_sizes = final_sizes;
result_sizes.entry(base.to_path_buf()).or_insert(0);
let duration = start_time.elapsed();
(result_sizes, final_file_count, duration)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_empty_dir() {
let dir = tempdir().unwrap();
let sizes = compute_dir_sizes(dir.path());
assert_eq!(sizes.len(), 1);
assert_eq!(sizes.get(dir.path()), Some(&0));
}
#[test]
fn test_single_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
std::fs::write(file_path, "hello").unwrap();
let sizes = compute_dir_sizes(dir.path());
assert_eq!(sizes.get(dir.path()), Some(&5));
}
#[test]
fn test_nested_dirs() {
let dir = tempdir().unwrap();
let a = dir.path().join("a");
let b = a.join("b");
fs::create_dir_all(&b).unwrap();
let file_path_1 = a.join("foo.txt");
std::fs::write(&file_path_1, "abcd").unwrap();
let file_path_2 = b.join("bar.txt");
std::fs::write(&file_path_2, "xyz").unwrap();
let sizes = compute_dir_sizes(dir.path());
assert_eq!(
sizes.get(dir.path()),
Some(&7),
"Root directory should have 7 bytes total"
);
assert_eq!(
sizes.get(&a),
Some(&7),
"Directory 'a' should have 7 bytes total"
);
assert_eq!(
sizes.get(&b),
Some(&3),
"Directory 'b' should have 3 bytes total"
);
}
#[test]
fn test_cache_functionality() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
std::fs::write(file_path, "hello world").unwrap();
let (sizes1, files1, _) = compute_dir_sizes_with_cache(dir.path(), true, false, 24);
assert_eq!(sizes1.get(dir.path()), Some(&11));
assert_eq!(files1, 1);
let (sizes2, files2, _) = compute_dir_sizes_with_cache(dir.path(), true, true, 24);
assert_eq!(sizes2.get(dir.path()), Some(&11));
assert_eq!(files2, 1);
assert_eq!(sizes1, sizes2);
assert_eq!(files1, files2);
}
#[test]
fn test_cache_store_and_retrieve() {
let dir = tempdir().unwrap();
let cache_dir = tempdir().unwrap();
let cache = Cache::with_dir(cache_dir.path().to_path_buf()).unwrap();
let mut test_sizes = HashMap::new();
test_sizes.insert(dir.path().to_path_buf(), 100);
cache.store(dir.path(), &test_sizes, 5).unwrap();
let retrieved = cache.retrieve(dir.path(), 3600).unwrap();
assert!(retrieved.is_some());
let entry = retrieved.unwrap();
assert_eq!(entry.sizes, test_sizes);
assert_eq!(entry.total_files, 5);
assert_eq!(entry.base_path, dir.path());
}
#[test]
fn test_cache_expiry() {
let dir = tempdir().unwrap();
let cache_dir = tempdir().unwrap();
let cache = Cache::with_dir(cache_dir.path().to_path_buf()).unwrap();
let mut test_sizes = HashMap::new();
test_sizes.insert(dir.path().to_path_buf(), 100);
cache.store(dir.path(), &test_sizes, 5).unwrap();
let retrieved_valid = cache.retrieve(dir.path(), 3600).unwrap();
assert!(retrieved_valid.is_some());
cache.store(dir.path(), &test_sizes, 5).unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
let retrieved = cache.retrieve(dir.path(), 0).unwrap();
assert!(retrieved.is_none());
}
}