#![feature(path_try_exists)]
use chrono::{DateTime, Local};
use chrono_humanize::HumanTime;
use colored::Colorize;
use dialoguer::theme::SimpleTheme;
use dialoguer::{Confirm, Input, MultiSelect};
use fs_extra::dir::get_size;
use home::home_dir;
use spinners::{Spinner, Spinners};
use std::fs;
use std::fs::remove_dir_all;
use std::path::Path;
use std::process::exit;
use walkdir::WalkDir;
pub fn get_home_dir() -> String {
let hd = home_dir();
let home_directory = match hd.as_ref() {
None => "",
Some(dir) => dir.to_str().unwrap(),
};
String::from(home_directory)
}
pub fn get_node_module_paths(starting_directory: String) -> Vec<(String, u64)> {
Path::new(&starting_directory)
.try_exists()
.expect("Path doesn't exist");
let expanded_starting_directory = shellexpand::tilde(&starting_directory);
let mut sp = Spinner::new(Spinners::Dots, "Searching for node_modules".into());
let mut node_modules: Vec<(String, u64)> = WalkDir::new(expanded_starting_directory.as_ref())
.follow_links(true)
.into_iter()
.filter(|e| e.is_ok())
.filter(|e| {
let matches = e
.as_ref()
.unwrap()
.path()
.to_string_lossy()
.matches("node_modules")
.count();
return matches == 1
&& e.as_ref()
.unwrap()
.file_name()
.to_string_lossy()
.ends_with("node_modules");
})
.filter(|e| e.as_ref().unwrap().path().exists())
.map(|e| {
let owned_e = e.unwrap();
return (
owned_e.path().to_str().unwrap().to_owned(),
get_size(owned_e.path()).unwrap_or(0),
);
})
.collect();
node_modules.sort_unstable_by_key(|k| k.1);
node_modules.reverse();
sp.stop();
node_modules
}
pub fn interactive_clean() {
let home_dir = get_home_dir();
let starting_directory: String = Input::new()
.with_prompt("Where should I start searching?")
.default(home_dir)
.interact_text()
.unwrap();
let node_modules = get_node_module_paths(starting_directory);
let _count = node_modules.len();
if _count == 0 {
println!("No node_modules found, exiting.");
exit(0);
}
let _total_size =
human_bytes::human_bytes(node_modules.iter().fold(0, |acc, x| acc + x.1) as f64);
println!("Found {_count} node_module folders with a total size of {_total_size}");
let paths: Vec<String> = node_modules.iter().map(|e| e.0.clone()).collect();
let names: Vec<String> = node_modules
.iter()
.map(|e| {
let color = match e.1 {
1..=100_000_000 => "green",
100_000_001..=1_000_000_000 => "yellow",
_ => "red",
};
let _human_size = human_bytes::human_bytes(e.1 as f64).color(color);
let _path_ref: &str = e.0.as_ref();
let parent = Path::new(_path_ref).parent();
let mut parent_last_modified: DateTime<Local> = Local::now();
if let Some(dir) = parent {
if let Ok(metadata) = fs::metadata(dir) {
parent_last_modified = DateTime::from(metadata.modified().unwrap());
} else {
}
} else {
parent_last_modified = DateTime::from(fs::metadata(_path_ref).unwrap().modified().unwrap());
}
let _last_modified_formatted = HumanTime::from(parent_last_modified);
let _english = format!("{}", _last_modified_formatted);
return format!("{_human_size} {path} | Last modified {_english}", path = e.0);
})
.collect();
let selections = MultiSelect::with_theme(&SimpleTheme)
.with_prompt("Choose which node_modules to delete")
.items(&names)
.interact()
.unwrap();
let mut deleted = 0;
if selections.is_empty() {
println!("No node_modules selected. Aborting...");
} else {
println!("The following node_modules will be deleted:");
for selection in &selections {
println!(" {}", &names[*selection]);
}
if Confirm::new().with_prompt("Continue? ").interact().unwrap() {
for selection in &selections {
match remove_dir_all(&paths[*selection]) {
Ok(_) => deleted += &node_modules[*selection].1,
Err(e) => println!("Failed to delete {}. Error: {}", &names[*selection], e),
}
}
println!("Freed {} bytes", human_bytes::human_bytes(deleted as f64));
} else {
println!("Aborting...");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::fs::create_dir_all;
use std::path::PathBuf;
#[test]
fn gets_home_directory() {
let homedir = home_dir().unwrap();
let home_dir = get_home_dir();
assert_eq!(homedir.to_str().unwrap(), home_dir)
}
#[test]
fn finds_node_modules_paths() {
let temp = assert_fs::TempDir::new().unwrap();
let node_modules_path_str: PathBuf =
[temp.to_str().unwrap(), "node_modules"].iter().collect();
let node_modules_file_path_str: PathBuf =
[node_modules_path_str.to_str().unwrap(), "package.json"]
.iter()
.collect();
if let Err(e) = create_dir_all(&node_modules_path_str) {
panic!("Error creating tests node_modules directory: {e}")
}
assert!(&node_modules_path_str.as_path().exists());
let temp_path = temp.to_str().unwrap().to_owned();
let mut paths = get_node_module_paths(temp_path.clone());
assert_eq!(
paths.first().unwrap().0,
node_modules_path_str.to_str().unwrap()
);
assert_eq!(paths.first().unwrap().1, 0);
let data = "hello";
if let Err(e) = fs::write(&node_modules_file_path_str, data) {
panic!("Error writing to temp file: {e}");
};
paths = get_node_module_paths(temp_path);
assert_eq!(paths[0].0, node_modules_path_str.to_str().unwrap());
let file_contents = fs::read_to_string(node_modules_file_path_str.as_path());
assert_eq!(file_contents.unwrap(), data);
let file_size = paths[0].1;
assert_eq!(file_size, data.as_bytes().len() as u64);
temp.close().unwrap();
}
}