mod cleaner;
mod decider;
#[cfg(feature = "highscore-board")]
mod highscore;
mod observer;
#[cfg(feature = "highscore-board")]
pub use crate::highscore::{render_board, HighscoreObserver, Highscores};
pub use crate::cleaner::*;
pub use crate::decider::*;
pub use crate::observer::*;
use jwalk::{ClientState, DirEntry, Parallelism};
use std::convert::{TryFrom, TryInto};
use std::fmt::{Display, Formatter};
use std::io::{Error, ErrorKind, Result};
use std::path::{Path, PathBuf};
use std::time::Duration;
pub struct FileToFolderMatch {
file_to_check: &'static str,
folder_to_remove: &'static str,
}
pub enum FolderProcessed {
Cleaned(usize),
NoRuleMatch,
Skipped,
Abort,
}
impl FileToFolderMatch {
pub const fn new(file_to_check: &'static str, folder_to_remove: &'static str) -> Self {
Self {
file_to_check,
folder_to_remove,
}
}
pub fn path_to_remove(&self, folder: impl AsRef<Path>) -> Option<impl AsRef<Path>> {
folder
.as_ref()
.canonicalize()
.map(|x| x.join(self.folder_to_remove))
.ok()
}
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct Folder(PathBuf);
impl Folder {
pub fn accept(
&self,
ctx: &DecisionContext,
rule: &FileToFolderMatch,
cleaner: &dyn DoCleanUp,
decider: &mut impl Decide,
observer: &mut dyn RunObserver,
) -> Result<FolderProcessed> {
if !rule.is_folder_to_remove(self) {
return Ok(FolderProcessed::NoRuleMatch);
}
let size_amount = self.calculate_size();
let size = size_amount.as_human_readable();
let folder = self.as_ref().display().to_string();
let folder = ctx
.working_dir
.components()
.take(ctx.working_dir.components().count() - 1)
.fold(folder, |acc, component| {
if let Some(s) = component.as_os_str().to_str() {
acc.replace(s, s.chars().next().unwrap_or(' ').to_string().as_str())
} else {
acc
}
});
ctx.println(format!("Cleaning {folder} with {size}"));
ctx.println(format!(
" ├─ because of {}",
PathBuf::from("..").join(rule.file_to_check).display()
));
let result = match decider.obtain_decision(ctx, "├─ delete directory recursively?") {
Ok(Decision::Yes) => match cleaner.do_cleanup(self.as_ref())? {
Clean::Cleaned => {
if let Some(hint) = observer.on_folder_cleaned(size_amount as u64) {
ctx.println(format!(" ├─ {hint}"));
}
ctx.println(format!(" └─ deleted {size}"));
FolderProcessed::Cleaned(size_amount)
}
Clean::NotCleaned => {
ctx.println(format!(
" └─ not deleted{}{size}",
if ctx.is_dry_run { " [dry-run] " } else { "" }
));
FolderProcessed::Skipped
}
},
Ok(Decision::Quit) => {
ctx.println(" └─ quiting");
FolderProcessed::Abort
}
_ => {
ctx.println(" └─ skipped");
FolderProcessed::Skipped
}
};
ctx.println("");
Ok(result)
}
fn calculate_size(&self) -> usize {
jwalk::WalkDirGeneric::<((), Option<usize>)>::new(self.as_ref())
.skip_hidden(false)
.follow_links(false)
.parallelism(Parallelism::RayonDefaultPool {
busy_timeout: Duration::from_secs(60),
})
.process_read_dir(|_, _, _, dir_entry_results| {
dir_entry_results.iter_mut().for_each(|dir_entry_result| {
if let Ok(dir_entry) = dir_entry_result {
if !dir_entry.file_type.is_dir() {
dir_entry.client_state = Some(
dir_entry
.metadata()
.map(|m| m.len() as usize)
.unwrap_or_default(),
);
}
}
})
})
.into_iter()
.filter_map(|f| f.ok())
.filter_map(|e| e.client_state)
.sum()
}
}
impl Display for Folder {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.display())
}
}
impl<A: ClientState> TryFrom<DirEntry<A>> for Folder {
type Error = std::io::Error;
fn try_from(value: DirEntry<A>) -> std::result::Result<Self, Self::Error> {
let path = value.path();
path.try_into() }
}
impl TryFrom<PathBuf> for Folder {
type Error = std::io::Error;
fn try_from(path: PathBuf) -> std::result::Result<Self, Self::Error> {
if !path.is_dir() || path.eq(Path::new(".")) || path.eq(Path::new("..")) {
Err(Error::from(ErrorKind::Unsupported))
} else {
let p = path.canonicalize()?;
Ok(Self(p))
}
}
}
impl TryFrom<&str> for Folder {
type Error = std::io::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
Folder::try_from(PathBuf::from(value))
}
}
impl AsRef<Path> for Folder {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
#[deprecated(since = "2.0.0", note = "use trait `IsFolderToRemove` instead")]
pub trait PathToRemoveResolver {
fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder>;
}
#[allow(deprecated)]
impl PathToRemoveResolver for FileToFolderMatch {
fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder> {
let folder = folder.as_ref();
let file_to_check = folder.join(self.file_to_check);
if file_to_check.exists() {
let path_to_remove = folder.join(self.folder_to_remove);
if path_to_remove.exists() {
return path_to_remove.try_into();
}
}
Err(Error::from(ErrorKind::Unsupported))
}
}
pub trait IsFolderToRemove {
fn is_folder_to_remove(&self, folder: &Folder) -> bool;
}
impl IsFolderToRemove for FileToFolderMatch {
fn is_folder_to_remove(&self, folder: &Folder) -> bool {
folder.as_ref().parent().map_or_else(
|| false,
|parent| {
parent.join(self.file_to_check).exists()
&& parent
.join(self.folder_to_remove)
.starts_with(folder.as_ref())
},
)
}
}
pub trait HumanReadable {
fn as_human_readable(&self) -> String;
}
impl HumanReadable for usize {
fn as_human_readable(&self) -> String {
const KIBIBYTE: usize = 1024;
const MEBIBYTE: usize = KIBIBYTE << 10;
const GIBIBYTE: usize = MEBIBYTE << 10;
const TEBIBYTE: usize = GIBIBYTE << 10;
const PEBIBYTE: usize = TEBIBYTE << 10;
const EXBIBYTE: usize = PEBIBYTE << 10;
let size = *self;
let (size, symbol) = match size {
size if size < KIBIBYTE => (size as f64, "B"),
size if size < MEBIBYTE => (size as f64 / KIBIBYTE as f64, "KiB"),
size if size < GIBIBYTE => (size as f64 / MEBIBYTE as f64, "MiB"),
size if size < TEBIBYTE => (size as f64 / GIBIBYTE as f64, "GiB"),
size if size < PEBIBYTE => (size as f64 / TEBIBYTE as f64, "TiB"),
size if size < EXBIBYTE => (size as f64 / PEBIBYTE as f64, "PiB"),
_ => (size as f64 / EXBIBYTE as f64, "EiB"),
};
format!("{size:.1}{symbol}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_size() {
assert_eq!(1_048_576, 1024 << 10);
}
#[test]
fn test_trait_is_folder_to_remove() {
let rule = FileToFolderMatch::new("Cargo.toml", "target");
let target_folder =
Folder::try_from(Path::new(".").canonicalize().unwrap().join("target")).unwrap();
assert!(rule.is_folder_to_remove(&target_folder));
let crate_root_folder = Folder::try_from(Path::new(".").canonicalize().unwrap()).unwrap();
assert!(!rule.is_folder_to_remove(&crate_root_folder));
}
}