use std::{error, fs, path};
const SYMLINK_FOLLOW: bool = true;
const FILE_CARGO_TOML: &str = "Cargo.toml";
const FILE_PACKAGE_JSON: &str = "package.json";
const FILE_ASSEMBLY_CSHARP: &str = "Assembly-CSharp.csproj";
const FILE_STACK_HASKELL: &str = "stack.yaml";
const FILE_SBT_BUILD: &str = "build.sbt";
const FILE_MVN_BUILD: &str = "pom.xml";
const FILE_UNREAL_SUFFIX: &str = ".uproject";
const PROJECT_CARGO_DIRS: [&str; 1] = ["target"];
const PROJECT_NODE_DIRS: [&str; 1] = ["node_modules"];
const PROJECT_UNITY_DIRS: [&str; 7] = [
"Library",
"Temp",
"Obj",
"Logs",
"MemoryCaptures",
"Build",
"Builds",
];
const PROJECT_STACK_DIRS: [&str; 1] = [".stack-work"];
const PROJECT_SBT_DIRS: [&str; 2] = ["target", "project/target"];
const PROJECT_MVN_DIRS: [&str; 1] = ["target"];
const PROJECT_UNREAL_DIRS: [&str; 5] = [
"Binaries",
"Build",
"Saved",
"DerivedDataCache",
"Intermediate",
];
const PROJECT_CARGO_NAME: &str = "Cargo";
const PROJECT_NODE_NAME: &str = "Node";
const PROJECT_UNITY_NAME: &str = "Unity";
const PROJECT_STACK_NAME: &str = "Stack";
const PROJECT_SBT_NAME: &str = "SBT";
const PROJECT_MVN_NAME: &str = "Maven";
const PROJECT_UNREAL_NAME: &str = "Unreal";
#[derive(Debug, Clone)]
pub enum ProjectType {
Cargo,
Node,
Unity,
Stack,
SBT,
Maven,
Unreal,
}
#[derive(Debug, Clone)]
pub struct Project {
pub project_type: ProjectType,
pub path: path::PathBuf,
}
#[derive(Debug, Clone)]
pub struct ProjectSize {
pub artifact_size: u64,
pub non_artifact_size: u64,
pub dirs: Vec<(String, u64, bool)>,
}
impl Project {
pub fn artifact_dirs(&self) -> &[&str] {
match self.project_type {
ProjectType::Cargo => &PROJECT_CARGO_DIRS,
ProjectType::Node => &PROJECT_NODE_DIRS,
ProjectType::Unity => &PROJECT_UNITY_DIRS,
ProjectType::Stack => &PROJECT_STACK_DIRS,
ProjectType::SBT => &PROJECT_SBT_DIRS,
ProjectType::Maven => &PROJECT_MVN_DIRS,
ProjectType::Unreal => &PROJECT_UNREAL_DIRS,
}
}
pub fn name(&self) -> String {
self.path.to_str().unwrap().to_string()
}
pub fn size(&self) -> u64 {
self.artifact_dirs()
.iter()
.copied()
.map(|p| dir_size(&self.path.join(p)))
.sum()
}
pub fn size_dirs(&self) -> ProjectSize {
let mut artifact_size = 0;
let mut non_artifact_size = 0;
let mut dirs = Vec::new();
let project_root = match fs::read_dir(&self.path) {
Err(_) => {
return ProjectSize {
artifact_size,
non_artifact_size,
dirs,
}
}
Ok(rd) => rd,
};
let artifact_dirs: Vec<&str> = self.artifact_dirs().iter().copied().collect();
for entry in project_root.filter_map(|rd| rd.ok()) {
let file_type = match entry.file_type() {
Err(_) => continue,
Ok(file_type) => file_type,
};
if file_type.is_file() {
if let Ok(metadata) = entry.metadata() {
non_artifact_size += metadata.len();
}
continue;
}
if file_type.is_dir() {
let file_name = match entry.file_name().into_string() {
Err(_) => continue,
Ok(file_name) => file_name,
};
let size = dir_size(&entry.path());
let artifact_dir = artifact_dirs.contains(&file_name.as_str());
if artifact_dir {
artifact_size += size;
} else {
non_artifact_size += size;
}
dirs.push((file_name, size, artifact_dir));
}
}
ProjectSize {
artifact_size,
non_artifact_size,
dirs,
}
}
pub fn type_name(&self) -> &'static str {
match self.project_type {
ProjectType::Cargo => PROJECT_CARGO_NAME,
ProjectType::Node => PROJECT_NODE_NAME,
ProjectType::Unity => PROJECT_UNITY_NAME,
ProjectType::Stack => PROJECT_STACK_NAME,
ProjectType::SBT => PROJECT_SBT_NAME,
ProjectType::Maven => PROJECT_MVN_NAME,
ProjectType::Unreal => PROJECT_UNREAL_NAME,
}
}
pub fn clean(&self) {
for artifact_dir in self
.artifact_dirs()
.iter()
.copied()
.map(|ad| self.path.join(ad))
.filter(|ad| ad.exists())
{
if let Err(e) = fs::remove_dir_all(&artifact_dir) {
eprintln!("error removing directory {:?}: {:?}", artifact_dir, e);
}
}
}
}
fn is_hidden(entry: &walkdir::DirEntry) -> bool {
entry
.file_name()
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false)
}
struct ProjectIter {
it: walkdir::IntoIter,
}
impl Iterator for ProjectIter {
type Item = Project;
fn next(&mut self) -> Option<Self::Item> {
loop {
let entry: walkdir::DirEntry = match self.it.next() {
None => return None,
Some(Err(_)) => continue,
Some(Ok(entry)) => entry,
};
if !entry.file_type().is_dir() {
continue;
}
if is_hidden(&entry) {
self.it.skip_current_dir();
continue;
}
let rd = match entry.path().read_dir() {
Err(_) => continue,
Ok(rd) => rd,
};
for dir_entry in rd.filter_map(|rd| rd.ok()) {
let file_name = match dir_entry.file_name().into_string() {
Err(_) => continue,
Ok(file_name) => file_name,
};
let p_type = match file_name.as_str() {
FILE_CARGO_TOML => Some(ProjectType::Cargo),
FILE_PACKAGE_JSON => Some(ProjectType::Node),
FILE_ASSEMBLY_CSHARP => Some(ProjectType::Unity),
FILE_STACK_HASKELL => Some(ProjectType::Stack),
FILE_SBT_BUILD => Some(ProjectType::SBT),
FILE_MVN_BUILD => Some(ProjectType::Maven),
file_name if file_name.ends_with(FILE_UNREAL_SUFFIX) => {
Some(ProjectType::Unreal)
}
_ => None,
};
if let Some(project_type) = p_type {
self.it.skip_current_dir();
return Some(Project {
project_type,
path: entry.path().to_path_buf(),
});
}
}
}
}
}
pub fn scan<P: AsRef<path::Path>>(p: &P) -> impl Iterator<Item = Project> {
ProjectIter {
it: walkdir::WalkDir::new(p)
.follow_links(SYMLINK_FOLLOW)
.into_iter(),
}
}
pub fn dir_size(path: &path::Path) -> u64 {
walkdir::WalkDir::new(path)
.follow_links(SYMLINK_FOLLOW)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.filter_map(|e| e.metadata().ok())
.map(|e| e.len())
.sum()
}
pub fn pretty_size(size: u64) -> String {
const KIBIBYTE: u64 = 1024;
const MEBIBYTE: u64 = 1_048_576;
const GIBIBYTE: u64 = 1_073_741_824;
const TEBIBYTE: u64 = 1_099_511_627_776;
const PEBIBYTE: u64 = 1_125_899_906_842_624;
const EXBIBYTE: u64 = 1_152_921_504_606_846_976;
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!("{:.1}{}", size, symbol)
}
pub fn clean(project_path: &str) -> Result<(), Box<dyn error::Error>> {
let project = fs::read_dir(project_path)?
.filter_map(|rd| rd.ok())
.find_map(|dir_entry| {
let file_name = dir_entry.file_name().into_string().ok()?;
let p_type = match file_name.as_str() {
FILE_CARGO_TOML => Some(ProjectType::Cargo),
FILE_PACKAGE_JSON => Some(ProjectType::Node),
FILE_ASSEMBLY_CSHARP => Some(ProjectType::Unity),
FILE_STACK_HASKELL => Some(ProjectType::Stack),
FILE_SBT_BUILD => Some(ProjectType::SBT),
FILE_MVN_BUILD => Some(ProjectType::Maven),
_ => None,
};
if let Some(project_type) = p_type {
return Some(Project {
project_type,
path: project_path.into(),
});
}
None
});
if let Some(project) = project {
for artifact_dir in project
.artifact_dirs()
.iter()
.copied()
.map(|ad| path::PathBuf::from(project_path).join(ad))
.filter(|ad| ad.exists())
{
if let Err(e) = fs::remove_dir_all(&artifact_dir) {
eprintln!("error removing directory {:?}: {:?}", artifact_dir, e);
}
}
}
Ok(())
}
pub fn path_canonicalise(base: &path::PathBuf, tail: path::PathBuf) -> path::PathBuf {
if tail.is_absolute() {
tail
} else {
base.join(tail)
.canonicalize()
.expect("Unable to canonicalize!")
}
}