use std::convert::TryFrom;
use std::io::Result;
use std::path::PathBuf;
use argh::FromArgs;
use jwalk::Parallelism;
use putzen_cli::{
DecisionContext, DoCleanUp, DryRunCleaner, FileToFolderMatch, Folder, FolderProcessed,
HumanReadable, IsFolderToRemove, NiceInteractiveDecider, NoOpObserver, ProperCleaner,
RunObserver,
};
#[cfg(feature = "highscore-board")]
use putzen_cli::HighscoreObserver;
static FOLDER_TO_CLEANUP: [FileToFolderMatch; 3] = [
FileToFolderMatch::new("Cargo.toml", "target"),
FileToFolderMatch::new("package.json", "node_modules"),
FileToFolderMatch::new("CMakeLists.txt", "build"),
];
#[derive(FromArgs)]
struct PutzenCliArgs {
#[argh(switch, short = 'v')]
version: bool,
#[cfg(feature = "highscore-board")]
#[argh(switch)]
scores: bool,
#[argh(switch, short = 'd')]
dry_run: bool,
#[argh(switch, short = 'y')]
yes_to_all: bool,
#[argh(switch, short = 'L')]
follow: bool,
#[argh(switch, short = 'a')]
dive_into_hidden_folders: bool,
#[argh(positional, default = "PathBuf::from(\".\")")]
folder: PathBuf,
}
fn main() -> Result<()> {
let args: PutzenCliArgs = argh::from_env();
if args.version {
println!("{} {}", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION"));
return Ok(());
}
#[cfg(feature = "highscore-board")]
if args.scores {
let highscores = putzen_cli::Highscores::load()?;
println!("{}", putzen_cli::render_board(&highscores));
return Ok(());
}
visit_path(&args)
}
fn visit_path(args: &PutzenCliArgs) -> Result<()> {
let to_clean = &FOLDER_TO_CLEANUP;
let mut decider = NiceInteractiveDecider::default();
let mut amount_cleaned = 0;
let folder = args
.folder
.canonicalize()
.expect("Folder cannot be canonicalized.");
let ctx = DecisionContext {
working_dir: folder.clone(),
is_dry_run: args.dry_run,
yes_to_all: args.yes_to_all,
};
let cleaner: Box<dyn DoCleanUp> = if args.dry_run {
Box::new(DryRunCleaner)
} else {
Box::new(ProperCleaner)
};
let mut observer: Box<dyn RunObserver> = if !args.dry_run {
#[cfg(feature = "highscore-board")]
{
Box::new(HighscoreObserver::load()?)
}
#[cfg(not(feature = "highscore-board"))]
{
Box::new(NoOpObserver)
}
} else {
Box::new(NoOpObserver)
};
ctx.println(format!("Start cleaning at {}", folder.display()));
for folder in jwalk::WalkDirGeneric::<((), Option<Folder>)>::new(folder)
.skip_hidden(!args.dive_into_hidden_folders)
.follow_links(args.follow)
.parallelism(Parallelism::RayonNewPool(8))
.process_read_dir(move |_, _, _, children| {
children.retain(|dir_entry_result| {
dir_entry_result
.as_ref()
.map(|dir| dir.path().is_dir())
.unwrap_or(false)
});
children.iter_mut().for_each(|child| {
if let Ok(child) = child {
if let Ok(folder) = Folder::try_from(child.path()) {
for rule in to_clean {
if rule.is_folder_to_remove(&folder) {
child.client_state = Some(folder);
child.read_children_path = None;
return;
}
}
}
}
});
})
.into_iter()
.filter_map(|f| f.ok())
.filter_map(|f| f.client_state)
{
'rules: for rule in to_clean {
let result = folder.accept(&ctx, rule, &*cleaner, &mut decider, &mut *observer);
match result {
Ok(FolderProcessed::Abort) => return Ok(()),
Ok(FolderProcessed::Cleaned(size)) => {
amount_cleaned += size;
continue 'rules;
}
Ok(FolderProcessed::NoRuleMatch) => continue 'rules,
Ok(FolderProcessed::Skipped) => continue 'rules,
Err(error) => return Err(error),
};
}
}
if amount_cleaned > 0 {
ctx.println(format!("Freed: {}", amount_cleaned.as_human_readable()));
} else {
ctx.println("No space freed ;-(");
}
if let Some(medals) = observer.on_run_complete(amount_cleaned as u64) {
println!("{medals}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_e2e_scenario() {
let root_folder = tempfile::TempDir::new().unwrap();
let target_folder = root_folder.path().join("target");
std::fs::create_dir(&target_folder).unwrap();
std::fs::File::create(root_folder.path().join("Cargo.toml")).unwrap();
std::fs::File::create(target_folder.join("some_artefact")).unwrap();
let node_modules_folder = root_folder.path().join("node_modules");
std::fs::create_dir(&node_modules_folder).unwrap();
std::fs::File::create(root_folder.path().join("package.json")).unwrap();
std::fs::File::create(node_modules_folder.join("some_artefact")).unwrap();
let second_node_root_folder = root_folder.path().join("bar");
std::fs::create_dir(&second_node_root_folder).unwrap();
let nested_node_modules_folder = second_node_root_folder.join("node_modules");
std::fs::create_dir(&nested_node_modules_folder).unwrap();
std::fs::File::create(second_node_root_folder.join("package.json")).unwrap();
std::fs::File::create(nested_node_modules_folder.join("some_artefact")).unwrap();
let args = PutzenCliArgs {
version: false,
#[cfg(feature = "highscore-board")]
scores: false,
dry_run: false,
yes_to_all: true,
follow: false,
dive_into_hidden_folders: false,
folder: root_folder.path().to_path_buf(),
};
visit_path(&args).unwrap();
assert!(!target_folder.exists());
assert!(!node_modules_folder.exists());
assert!(!nested_node_modules_folder.exists());
assert!(root_folder.path().join("Cargo.toml").exists());
assert!(root_folder.path().join("package.json").exists());
assert!(second_node_root_folder.join("package.json").exists());
}
}