From b694e5f67205d33733d673fc193349095b6fa290 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 17 Sep 2023 16:11:44 -0400 Subject: [PATCH] initial commit --- Cargo.lock | 344 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 ++ src/directory.rs | 128 ++++++++++++++++++ src/main.rs | 72 ++++++++++ src/walk.rs | 91 +++++++++++++ 5 files changed, 645 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/directory.rs create mode 100644 src/main.rs create mode 100644 src/walk.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..969c0ee --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,344 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "fud" +version = "0.1.0" +dependencies = [ + "clap", + "rayon", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..959e76c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "fud" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.4.3", features = ["derive"] } +rayon = "1.7.0" diff --git a/src/directory.rs b/src/directory.rs new file mode 100644 index 0000000..044a2da --- /dev/null +++ b/src/directory.rs @@ -0,0 +1,128 @@ +use std::path::{PathBuf, Component}; + +#[derive(Debug, Clone, Default)] +pub struct Directory { + pub name: String, + pub size: u64, + children: Vec, +} +impl Directory { + pub fn new(name: String, size: u64) -> Self { + Self { + name, + size, + children: Vec::new(), + } + } + + pub fn insert(&mut self, path: PathBuf, size: u64) { + let components = + path.components() + .filter_map( + |cmp| match cmp { + Component::Normal(os_str) => os_str.to_str(), + _ => None, + } + ).map(ToOwned::to_owned); + + let mut current = self; + for next in components { + if &next == ¤t.name { + continue; + } + + let idx = if let Some(idx) = current.children.iter().position(|d| d.name == next) { + idx + } else { + current.children.push(Directory::new(next, size)); + current.children.len()-1 + }; + current = &mut current.children[idx]; + } + } + + /// Recursive function to find the greaest file size + /// in this directory tree, used for formatting in the output + fn find_max_size(&self) -> u64 { + self.children.iter() + .map(|d| d.find_max_size()) + .max() + .unwrap_or(self.size) + } + + /// public-exposed print function, does the setup for the + /// real print function, `print2` + pub fn print(self) { + // fake print function to give the depth param + let max_size = self.find_max_size(); + let tabwidth = max_size.to_string().len() + 2; + + let mut stack = Vec::new(); + self.print2(tabwidth as usize, &mut stack); + } + + /// real print function that makes the tree + fn print2(self, tabwidth: usize, stack: &mut Vec) { + let indent = build_indent(stack); + println!("{:tabwidth$}{indent} {}", self.size.to_string(), self.name); + + stack.push(TreePart::Edge); + let mut iter = self.children.into_iter().peekable(); + while let Some(child) = iter.next() { + if iter.peek().is_none() { + let idx = stack.len()-1; + stack[idx] = TreePart::Corner; + }; + child.print2(tabwidth, stack); + } + stack.pop(); + } +} +impl Extend<(PathBuf, u64)> for Directory { + fn extend>(&mut self, iter: T) { + for (p, s) in iter { + self.insert(p, s); + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +enum TreePart { + /// Rightmost column, not the last in the directory + Edge, + /// Not rightmost, but dir not finished yet + Line, + /// Rightmost column in the last directory + Corner, + /// Not rightmost, but the dir has finished + Blank +} +impl TreePart { + /// convert to ascii art + pub fn display(&self) -> &'static str { + match self { + Self::Edge => "├──", + Self::Line => "│ ", + Self::Corner => "└──", + Self::Blank => " ", + } + } +} +/// Construct the indent string for the given stack. +/// must be called at the top of the recursive function, +/// and does mutate the stack +fn build_indent(stack: &mut Vec) -> String { + let mut indent = String::new(); + for (i, mut tp) in stack.iter().enumerate() { + if i < stack.len()-1 && tp == &TreePart::Edge { + tp = &TreePart::Line; + } + indent += tp.display(); + } + // essentially, if the last time element on the stack was a corner, + // make it blank for all future prints + if let Some(last @ TreePart::Corner) = stack.last_mut() { + *last = TreePart::Blank; + } + indent +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..12d0e89 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,72 @@ +use clap::Parser; + +mod walk; +mod directory; + +use walk::walk; + +use std::fs::File; +use std::process::ExitCode; + +#[derive(Parser, Debug, Clone)] +pub struct Args { + #[arg( + short, long, default_value_t = false, + help = "keep going if an error occurs", + long_help = "keep going if an error occurs (ex. unreadable subdirectories in a readable directory)" + )] + persistant: bool, + + #[arg( + short, long, + help = "minimize output", + long_help = "like -t, but does not print \"total: \" before the summary or the newline after. It also surpresses all error messages", + default_value_t = false, + )] + minimal: bool, + + #[arg( + short, long, + help = "only display the total size", + default_value_t = false, + )] + total_only: bool, + + #[arg(value_parser = validate_path, default_value_t = String::from("."), help = "directory to begin search from")] + path: String, +} + +fn validate_path(s: &str) -> Result { + let here = File::open(s).map_err(|e| e.to_string())?; + let meta = here.metadata().map_err(|e| e.to_string())?; + if meta.is_dir() { + return Ok(s.to_owned()) + } else if meta.is_file() { + return Err("this is a file (hint: use wc to view file sizes)".to_owned()); + } else { + return Err("this is not a directory".to_owned()); + } +} + +fn main() -> ExitCode { + let args = Args::parse(); + + let dir_structure = match walk(args.clone()) { + Ok(dir) => dir, + Err(e) => return e, + }; + + if args.minimal { + print!("{}", dir_structure.size); + } else { + let size = dir_structure.size; // copy size before print consumes dir_structure + dir_structure.print(); + // looks like "print the total unless asked to", but "total: " is to prevent + // the root from getting lost in massive outputs + if !args.total_only { + println!("total: {size}"); + } + } + + ExitCode::SUCCESS +} \ No newline at end of file diff --git a/src/walk.rs b/src/walk.rs new file mode 100644 index 0000000..574994f --- /dev/null +++ b/src/walk.rs @@ -0,0 +1,91 @@ +use rayon::prelude::*; + +use std::sync::mpsc::{Sender, channel}; +use std::fs::{read_dir, DirEntry}; +use std::io::Error; +use std::path::PathBuf; +use std::process::ExitCode; + +use crate::Args; +use crate::directory::Directory; + +pub fn walk<'a>(args: Args) -> Result { + let (tx, rx) = channel(); + + let mut total = 0; + for entry in read_dir(&args.path).unwrap() { + let entry = match entry { + Ok(e) => e, + Err(e) => { + if !args.minimal { + eprintln!("unable to open {}: {e}", args.path); + } + if args.persistant { + continue; + } else { + return Err(ExitCode::FAILURE); + } + }, + }; + + total += match total_entry(entry, &args, &tx) { + Ok(t) => t, + Err((path, error)) => { + if !args.minimal { + eprintln!("error opening {}: {error}", path.display()); + } + if args.persistant { + continue; + } else { + return Err(ExitCode::FAILURE); + } + }, + }; + } + // drop this to close the channel, so that into_iter() can end + drop(tx); + + let mut fs = Directory::new(args.path, total); + fs.extend(rx); + + Ok(fs) +} + +fn total_entry(entry: DirEntry, args: &Args, printer: &Sender<(PathBuf, u64)>) -> Result { + let path = entry.path(); + + match path.read_dir() { + Ok(dir) => { + let result = dir.par_bridge() + .filter_map(Result::ok) + .map(|entry| total_entry(entry, args, printer)) + .reduce(|| Ok(0), reduce_once); + + if let Ok(size) = result { + if !args.minimal && !args.total_only { + let _ = printer.send((path, size)); + } + } + + return result; + }, + Err(_) if path.is_file() => { + let size = unsafe { path.metadata().unwrap_unchecked() }.len(); + if !args.minimal && !args.total_only { + let _ = printer.send((path, size)); + } + return Ok(size); + }, + Err(e) => Err((path, e)), + } +} + +fn reduce_once(accum: Result, this: Result) -> Result { + // reduction function for total_entry(): + // short-circuit Errs, propagate Oks + // generic bc I'm lazy + match (accum, this) { + (Ok(n1), Ok(n2)) => Ok(n1 + n2), + (Err(e), _) | (_, Err(e)) => Err(e), + } +} \ No newline at end of file