pub mod cmd;
pub mod config;
pub mod constant;
pub mod utils;
use crate::cmd::Cmd;
use colored::*;
use futures::future;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::Path;
use std::sync::Arc;
use tokio::{fs, sync::Semaphore};
use walkdir::WalkDir;
async fn get_dir_size_async(path: &Path, max_depth: usize, max_files: usize) -> u64 {
use std::collections::VecDeque;
let mut total_size = 0;
let mut file_count = 0;
let mut dirs_to_visit = VecDeque::new();
if path.exists() {
dirs_to_visit.push_back((path.to_path_buf(), 0));
while let Some((current_dir, depth)) = dirs_to_visit.pop_front() {
if depth > max_depth {
eprintln!("{} Warning: Maximum directory depth ({}) exceeded for {}. Size calculation might be incomplete.",
"SKIP".yellow(), max_depth, current_dir.display());
continue;
}
if let Ok(mut entries) = fs::read_dir(¤t_dir).await {
while let Ok(Some(entry)) = entries.next_entry().await {
if file_count > max_files {
eprintln!("{} Warning: Maximum file count ({}) exceeded for {}. Size calculation might be incomplete.",
"SKIP".yellow(), max_files, current_dir.display());
return total_size;
}
if let Ok(metadata) = entry.metadata().await {
if metadata.is_file() {
total_size += metadata.len();
file_count += 1;
} else if metadata.is_dir() {
dirs_to_visit.push_back((entry.path(), depth + 1));
}
}
}
}
}
}
total_size
}
pub fn get_cpu_core_count() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4) }
pub async fn do_clean_all(
dir: &Path,
commands: &Vec<Cmd>,
exclude_dirs: &Vec<String>,
max_concurrent: Option<usize>,
max_directory_depth: usize,
max_files_per_project: usize,
) -> u32 {
let entries: Vec<_> = WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_dir())
.collect();
let cleaning_tasks: Vec<_> = entries
.iter()
.filter_map(|entry| {
let path = entry.path();
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
if dir_name.starts_with('.') || exclude_dirs.contains(&dir_name.to_string()) {
return None;
}
}
let mut tasks_for_dir = vec![];
for cmd in commands.iter() {
if cmd
.related_files
.iter()
.any(|file| path.join(file).exists())
{
tasks_for_dir.push((path.to_path_buf(), cmd.command_type));
}
}
if tasks_for_dir.is_empty() {
None
} else {
Some(tasks_for_dir)
}
})
.flatten()
.collect();
if cleaning_tasks.is_empty() {
println!("{}", "No projects found to clean".yellow());
return 0;
}
let total_tasks = cleaning_tasks.len();
let pb = Arc::new(ProgressBar::new(total_tasks as u64));
pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
)
.expect("Failed to set progress template")
.progress_chars("#>-"),
);
pb.set_message("Scanning projects...");
let max_concurrent_limit = max_concurrent.unwrap_or_else(get_cpu_core_count);
let semaphore = Arc::new(Semaphore::new(max_concurrent_limit));
let size_futures: Vec<_> = cleaning_tasks
.iter()
.map(|(path, _)| {
let semaphore = Arc::clone(&semaphore);
async move {
let _permit = semaphore.acquire().await.unwrap();
get_dir_size_async(path, max_directory_depth, max_files_per_project).await
}
})
.collect();
let sizes_before = future::join_all(size_futures).await;
let total_size_before: u64 = sizes_before.iter().sum();
if total_size_before > 0 {
pb.set_message(format!(
"Total cache size: {}",
format_size(total_size_before)
));
}
let cleaning_futures: Vec<_> = cleaning_tasks
.into_iter()
.zip(sizes_before.into_iter())
.map(|((path, cmd_name), size_before)| {
let pb = Arc::clone(&pb);
let semaphore = Arc::clone(&semaphore);
async move {
let _permit = semaphore.acquire().await.unwrap();
pb.inc(1);
pb.set_message(format!("Cleaning {} ({})", path.display(), cmd_name.as_str()));
let cmd = commands.iter().find(|c| c.command_type == cmd_name).unwrap();
match cmd.run_clean(&path).await {
Ok(_) => {
let size_after = get_dir_size_async(&path, max_directory_depth, max_files_per_project).await;
let cleaned_size = size_before.saturating_sub(size_after);
if cleaned_size > 0 {
pb.println(format!(
"✓ {} {} - {}",
"Cleaned".green(),
path.display(),
format_size(cleaned_size).cyan()
));
} else {
pb.println(format!(
"✓ {} {} - {}",
"Cleaned".green(),
path.display(),
"No files removed".yellow()
));
}
(1, size_before, size_after)
}
Err(e) => {
pb.println(format!(
"✗ {} {} - {} (Error: {})",
"Failed".red(),
path.display(),
cmd_name.as_str(),
e
));
(0, size_before, 0)
}
}
}
})
.collect();
let results = future::join_all(cleaning_futures).await;
pb.finish_with_message("Cleaning complete!");
let total_cleaned: u32 = results.iter().map(|(count, _, _)| count).sum();
let total_size_after: u64 = results.iter().map(|(_, _, after)| after).sum();
let total_freed = total_size_before.saturating_sub(total_size_after);
if total_size_before > 0 {
println!(
"Total space freed: {}",
format_size(total_freed).green().bold()
);
}
total_cleaned
}
fn format_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", bytes, UNITS[unit_index])
} else {
format!("{:.2} {}", size, UNITS[unit_index])
}
}