From 30bd6dbaa2a1c5b8904c54c019872b9e1ca4f9a6 Mon Sep 17 00:00:00 2001 From: Trent Billington Date: Mon, 27 Jan 2020 14:36:06 +1100 Subject: [PATCH] initial commit 0.1.0 --- .gitignore | 3 + Cargo.lock | 61 ++++++++++++++++ Cargo.toml | 26 +++++++ LICENSE | 21 ++++++ README.md | 45 ++++++++++++ src/main.rs | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 353 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6bc790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +/test_dir \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d5657fc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "kondo" +version = "0.1.0" +dependencies = [ + "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +"checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..272e1ce --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "kondo" +version = "0.1.0" +authors = ["Trent Billington "] +description = """ +kondo is a filesystem cleaning tool that recursively searches directories +for known project structures and determines how much space you could save by +deleting the unnecessary files. +""" +documentation = "https://github.com/tbillington/kondo" +homepage = "https://github.com/tbillington/kondo" +repository = "https://github.com/tbillington/kondo" +readme = "README.md" +categories = ["command-line-utilities"] +license = "MIT" +exclude = ["test_dir"] +edition = "2018" + + +[dependencies] +walkdir = "2" + +[profile.release] +incremental = false +lto = true +codegen-units = 1 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8695feb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Trent Billington + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..abe2ef2 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Kondo 🧹 + +Cleans unneeded directories and files from your system. + +It will identify the disk space savings you would get from deleting temporary/unnecessary files from project directories, such as `target` from Cargo projects and `node_modules` from Node projects. Currently `kondo` doesn't actually delete any files. + +Supports: + +- Cargo projects +- Node projects +- Unity Projects + +## Roadmap + +- Actually delete (with prompt) +- Handle Unity cache, editor cache + +## Operation + +Running `kondo` without a directory specified will run in the current directory. + +``` +$ kondo +``` + +Supplying an argument will tell `kondo` where to start. + +``` +$ kondo code/my_project +``` + +## Example Output + +``` +$ kondo ~ +Scanning "C:/Users/Trent" +3 projects found +Calculating savings per project + (redacted 1000~ lines) + 385.6MB UnityTestApp (Unity) C:\Users\Trent\code\UnityTestApp + 458.7MB tokio (Cargo) C:\Users\Trent\code\tokio + 1.5GB ui-testing (Node) C:\Users\Trent\code\ui-testing + 4.0GB rust-analyzer (Cargo) C:\Users\Trent\code\rust-analyzer +9.5GB possible savings +``` diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8d79327 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,197 @@ +use std::{env, io, path}; +use walkdir; + +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 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", +]; + +#[derive(Clone, Debug)] +enum ProjectType { + Cargo, + Node, + Unity, +} + +#[derive(Clone, Debug)] +struct ProjectDir { + r#type: ProjectType, + path: path::PathBuf, +} + +fn is_hidden(entry: &walkdir::DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.starts_with(".")) + .unwrap_or(false) +} + +fn scan>(path: &P) -> Vec { + walkdir::WalkDir::new(path) + .follow_links(SYMLINK_FOLLOW) + .into_iter() + .filter_entry(|e| !is_hidden(e)) + .filter_map(|e| e.ok()) + .filter_map(|entry| { + if entry.file_type().is_file() { + Some(ProjectDir { + r#type: match entry.file_name().to_str() { + Some(FILE_CARGO_TOML) => ProjectType::Cargo, + Some(FILE_PACKAGE_JSON) => ProjectType::Node, + Some(FILE_ASSEMBLY_CSHARP) => ProjectType::Unity, + _ => return None, + }, + path: entry + .path() + .parent() + .expect("it's a file, so it should definitely have a parent...") + .to_path_buf(), + }) + } else { + None + } + }) + .collect() +} + +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()) + .map(|e: walkdir::DirEntry| e.metadata()) + .filter_map(|md| md.ok()) + .map(|e| e.len()) + .sum() +} + +fn pretty_size(size: u64) -> String { + let size = size as f64; + const KIBIBYTE: f64 = 1024.0; + const MEBIBYTE: f64 = 1_048_576.0; + const GIBIBYTE: f64 = 1_073_741_824.0; + const TEBIBYTE: f64 = 1_099_511_627_776.0; + const PEBIBYTE: f64 = 1_125_899_906_842_624.0; + const EXBIBYTE: f64 = 1_152_921_504_606_846_976.0; + + let (size, symbol) = if size < KIBIBYTE { + (size, "B") + } else if size < MEBIBYTE { + (size / KIBIBYTE, "KB") + } else if size < GIBIBYTE { + (size / MEBIBYTE, "MB") + } else if size < TEBIBYTE { + (size / GIBIBYTE, "GB") + } else if size < PEBIBYTE { + (size / TEBIBYTE, "TB") + } else if size < EXBIBYTE { + (size / PEBIBYTE, "PB") + } else { + (size / EXBIBYTE, "EB") + }; + + format!("{:.1}{}", size, symbol) +} + +fn main() -> Result<(), Box> { + use io::Write; + let dir = { + let mut args: Vec = env::args().collect(); + if args.len() == 2 { + path::PathBuf::from(args.pop().unwrap()) + } else { + env::current_dir()? + } + }; + + let stdout = io::stdout(); + let mut write_handle = stdout.lock(); + + writeln!(&mut write_handle, "Scanning {:?}", dir)?; + + let mut project_dirs = scan(&dir); + + { + // Remove child directories if they have a parent in the list + let mut i = 0; + 'outer: while i < project_dirs.len() { + let mut j = i + 1; + + while j < project_dirs.len() { + let (p1, p2) = (&project_dirs[i].path, &project_dirs[j].path); + + if p1.starts_with(p2) { + project_dirs.remove(i); + continue 'outer; + } else if p2.starts_with(p1) { + project_dirs.remove(j); + continue; + } + + j += 1; + } + + i += 1; + } + } + + writeln!(&mut write_handle, "{} projects found", project_dirs.len())?; + + writeln!(&mut write_handle, "Calculating savings per project")?; + + let mut total = 0; + + let mut project_sizes: Vec<(u64, String)> = project_dirs + .iter() + .map(|ProjectDir { r#type, path }| { + let (name, dirs) = match r#type { + ProjectType::Cargo => ("Cargo", PROJECT_CARGO_DIRS.iter()), + ProjectType::Node => ("Node", PROJECT_NODE_DIRS.iter()), + ProjectType::Unity => ("Unity", PROJECT_UNITY_DIRS.iter()), + }; + + let size = dirs.map(|p| dir_size(&path.join(p))).sum(); + total += size; + + ( + size, + format!( + "{} ({}) {}", + path.strip_prefix(&dir) + .unwrap() + .file_name() + .map(|n| n.to_str().unwrap()) + .unwrap_or("."), + name, + path.display() + ), + ) + }) + .filter(|(size, _)| *size > 1) + .collect(); + + project_sizes.sort_unstable_by_key(|p| p.0); + + for (size, project) in project_sizes.iter() { + writeln!(&mut write_handle, "{:>9} {}", pretty_size(*size), project)?; + } + + writeln!(&mut write_handle, "{} possible savings", pretty_size(total))?; + + Ok(()) +}