diff --git a/Cargo.lock b/Cargo.lock index 558deecb..a6c9ff33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,7 @@ dependencies = [ "ansi_term", "clap", "crossbeam", + "filesize", "ignore", "indoc", "lscolors", @@ -200,6 +201,15 @@ dependencies = [ "libc", ] +[[package]] +name = "filesize" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d741e2415d4e2e5bd1c1d00409d1a8865a57892c2d689b504365655d237d43" +dependencies = [ + "winapi", +] + [[package]] name = "fnv" version = "1.0.7" diff --git a/Cargo.toml b/Cargo.toml index c059b4db..7ac23a90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ path = "src/main.rs" ansi_term = "0.12.1" clap = { version = "4.1.1", features = ["derive"] } crossbeam = "0.8.2" +filesize = "0.2.0" ignore = "0.4.2" lscolors = { version = "0.13.0", features = ["ansi_term"] } once_cell = "1.17.0" diff --git a/src/cli.rs b/src/cli.rs index f23080f5..dbf3ba52 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,15 +7,14 @@ use std::{ convert::From, error::Error as StdError, fmt::{self, Display, Formatter}, - fs, - io, + fs, io, path::{Path, PathBuf}, usize, }; /// Defines the CLI. #[derive(Parser, Debug)] -#[command(name = "Erdtree")] +#[command(name = "erdtree")] #[command(author = "Benjamin Nguyen. ")] #[command(version = "1.2")] #[command(about = "erdtree (et) is a multi-threaded filetree visualizer and disk usage analyzer.", long_about = None)] @@ -23,6 +22,10 @@ pub struct Clargs { /// Root directory to traverse; defaults to current working directory dir: Option, + /// Print physical or logical file size + #[arg(short, long, value_enum, default_value_t = DiskUsage::Logical)] + disk_usage: DiskUsage, + /// Include or exclude files using glob patterns #[arg(short, long)] glob: Vec, @@ -78,21 +81,33 @@ pub enum Order { /// Sort entries by file name Name, - /// Sort entries by size in descending order + /// Sort entries by size smallest to largest, top to bottom Size, + /// Sort entries by size largest to smallest, bottom to top + SizeRev, + /// No sorting None, } +/// Display disk usage output as either logical size or physical size. +#[derive(Copy, Clone, Debug, ValueEnum)] +pub enum DiskUsage { + /// How many bytes does a file contain + Logical, + + /// How much actual space on disk based on blocks allocated, taking into account sparse files + /// and compression. + Physical, +} + impl Clargs { /// Returns reference to the path of the root directory to be traversed. pub fn dir(&self) -> &Path { - if let Some(ref path) = self.dir { - path.as_path() - } else { - Path::new(".") - } + self.dir + .as_ref() + .map_or_else(|| Path::new("."), |pb| pb.as_path()) } /// The sort-order used for printing. @@ -100,10 +115,16 @@ impl Clargs { self.sort } + /// Getter for `dirs_first` field. pub fn dirs_first(&self) -> bool { self.dirs_first } + /// Getter for `disk_usage` field. + pub fn disk_usage(&self) -> &DiskUsage { + &self.disk_usage + } + /// The max depth to print. Note that all directories are fully traversed to compute file /// sizes; this just determines how much to print. pub fn level(&self) -> Option { @@ -146,8 +167,7 @@ impl TryFrom<&Clargs> for WalkParallel { fn try_from(clargs: &Clargs) -> Result { let root = fs::canonicalize(clargs.dir())?; - fs::metadata(&root) - .map_err(|e| Error::DirNotFound(format!("{}: {e}", root.display())))?; + fs::metadata(&root).map_err(|e| Error::DirNotFound(format!("{}: {e}", root.display())))?; Ok(WalkBuilder::new(root) .follow_links(clargs.follow_links) diff --git a/src/fs/erdtree/disk_usage.rs b/src/fs/erdtree/disk_usage.rs new file mode 100644 index 00000000..b0323328 --- /dev/null +++ b/src/fs/erdtree/disk_usage.rs @@ -0,0 +1,21 @@ +use crate::cli; +use std::convert::From; + +/// Determines between logical or physical size for display +#[derive(Debug)] +pub enum DiskUsage { + /// How many bytes does a file contain + Logical, + + /// How much actual space on disk, taking into account sparse files and compression. + Physical, +} + +impl From<&cli::DiskUsage> for DiskUsage { + fn from(du: &cli::DiskUsage) -> Self { + match du { + cli::DiskUsage::Logical => Self::Logical, + cli::DiskUsage::Physical => Self::Physical, + } + } +} diff --git a/src/fs/erdtree/mod.rs b/src/fs/erdtree/mod.rs index 7c398e26..5c8da342 100644 --- a/src/fs/erdtree/mod.rs +++ b/src/fs/erdtree/mod.rs @@ -1,3 +1,6 @@ +/// Operations that decide how to present info about disk usage. +pub mod disk_usage; + /// Contains components of the [`Tree`] data structure that derive from [`DirEntry`]. /// /// [`Tree`]: tree::Tree diff --git a/src/fs/erdtree/node.rs b/src/fs/erdtree/node.rs index c7183f72..daf7fbb3 100644 --- a/src/fs/erdtree/node.rs +++ b/src/fs/erdtree/node.rs @@ -1,17 +1,18 @@ -use super::get_ls_colors; -use ansi_term::Color; +use super::{disk_usage::DiskUsage, get_ls_colors}; use crate::{ fs::file_size::FileSize, - icons::{self, icon_from_ext, icon_from_file_name, icon_from_file_type} + icons::{self, icon_from_ext, icon_from_file_name, icon_from_file_type}, }; +use ansi_term::Color; use ansi_term::Style; +use filesize::PathExt; use ignore::DirEntry; use lscolors::Style as LS_Style; use std::{ borrow::Cow, convert::From, - fmt::{self, Display, Formatter}, ffi::{OsStr, OsString}, + fmt::{self, Display, Formatter}, fs::{self, FileType}, path::{Path, PathBuf}, slice::Iter, @@ -81,17 +82,14 @@ impl Node { /// Converts `OsStr` to `String`; if fails does a lossy conversion replacing non-Unicode /// sequences with Unicode replacement scalar value. pub fn file_name_lossy(&self) -> Cow<'_, str> { - self.file_name().to_str().map_or_else( - || self.file_name().to_string_lossy(), - |s| Cow::from(s) - ) + self.file_name() + .to_str() + .map_or_else(|| self.file_name().to_string_lossy(), |s| Cow::from(s)) } /// Returns `true` if node is a directory. pub fn is_dir(&self) -> bool { - self.file_type() - .map(|ft| ft.is_dir()) - .unwrap_or(false) + self.file_type().map(|ft| ft.is_dir()).unwrap_or(false) } /// Is the Node a symlink. @@ -106,7 +104,9 @@ impl Node { /// Returns the file name of the symlink target if [Node] represents a symlink. pub fn symlink_target_file_name(&self) -> Option<&OsStr> { - self.symlink_target_path().map(|path| path.file_name()).flatten() + self.symlink_target_path() + .map(|path| path.file_name()) + .flatten() } /// Returns reference to underlying [FileType]. @@ -149,9 +149,11 @@ impl Node { /// Gets stylized icon for node if enabled. Icons without extensions are styled based on the /// [`LS_COLORS`] foreground configuration of the associated file name. /// - /// [`LS_COLORS`]: super::tree::ui::LS_COLORS + /// [`LS_COLORS`]: super::tree::ui::LS_COLORS fn get_icon(&self) -> Option { - if !self.show_icon { return None } + if !self.show_icon { + return None; + } let path = self.symlink_target_path().unwrap_or_else(|| self.path()); @@ -163,7 +165,9 @@ impl Node { return Some(self.stylize(icon)); } - let file_name = self.symlink_target_file_name().unwrap_or_else(|| self.file_name()); + let file_name = self + .symlink_target_file_name() + .unwrap_or_else(|| self.file_name()); if let Some(icon) = icon_from_file_name(file_name) { return Some(self.stylize(icon)); @@ -174,42 +178,50 @@ impl Node { /// Stylizes input, `entity` based on [`LS_COLORS`] /// - /// [`LS_COLORS`]: super::tree::ui::LS_COLORS + /// [`LS_COLORS`]: super::tree::ui::LS_COLORS fn stylize(&self, entity: &str) -> String { self.style().foreground.map_or_else( || entity.to_string(), - |fg| fg.bold().paint(entity).to_string() + |fg| fg.bold().paint(entity).to_string(), ) } /// Stylizes symlink name for display. fn stylize_link_name(&self) -> Option { - self.symlink_target_file_name() - .map(|name| { - let file_name = self.file_name_lossy(); - let styled_name = self.stylize(&file_name); - let target_name = Color::Red.paint(format!("\u{2192} {}", name.to_string_lossy())); - format!("{} {}", styled_name, target_name) - }) + self.symlink_target_file_name().map(|name| { + let file_name = self.file_name_lossy(); + let styled_name = self.stylize(&file_name); + let target_name = Color::Red.paint(format!("\u{2192} {}", name.to_string_lossy())); + format!("{} {}", styled_name, target_name) + }) } } /// Used to be converted directly into a [Node]. -pub struct NodePrecursor { +pub struct NodePrecursor<'a> { + disk_usage: &'a DiskUsage, dir_entry: DirEntry, show_icon: bool, } -impl NodePrecursor { +impl<'a> NodePrecursor<'a> { /// Yields a [NodePrecursor] which is used for convenient conversion into a [Node]. - pub fn new(dir_entry: DirEntry, show_icon: bool) -> Self { - Self { dir_entry, show_icon } + pub fn new(disk_usage: &'a DiskUsage, dir_entry: DirEntry, show_icon: bool) -> Self { + Self { + disk_usage, + dir_entry, + show_icon, + } } } -impl From for Node { +impl From> for Node { fn from(precursor: NodePrecursor) -> Self { - let NodePrecursor { dir_entry, show_icon } = precursor; + let NodePrecursor { + disk_usage, + dir_entry, + show_icon, + } = precursor; let children = None; @@ -217,8 +229,6 @@ impl From for Node { let file_type = dir_entry.file_type(); - let metadata = dir_entry.metadata().ok(); - let path = dir_entry.path(); let symlink_target = dir_entry @@ -233,6 +243,8 @@ impl From for Node { |os_str| os_str.to_owned(), ); + let metadata = dir_entry.metadata().ok(); + let style = get_ls_colors() .style_for_path_with_metadata(path, metadata.as_ref()) .map(LS_Style::to_ansi_term_style) @@ -242,8 +254,11 @@ impl From for Node { if let Some(ref ft) = file_type { if ft.is_file() { - if let Some(md) = metadata { - file_size = Some(md.len()); + if let Some(ref md) = metadata { + file_size = match disk_usage { + DiskUsage::Logical => Some(md.len()), + DiskUsage::Physical => path.size_on_disk_fast(md).ok(), + } } } }; @@ -264,19 +279,22 @@ impl From for Node { impl Display for Node { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let size = self.file_size + let size = self + .file_size .map(|size| format!("({})", FileSize::new(size))) .or_else(|| Some("".to_owned())) .unwrap(); - let icon = self.show_icon + let icon = self + .show_icon .then(|| self.get_icon()) .flatten() .unwrap_or("".to_owned()); let icon_padding = (icon.len() > 1).then(|| icon.len() - 1).unwrap_or(0); - let (styled_name, name_padding) = self.stylize_link_name() + let (styled_name, name_padding) = self + .stylize_link_name() .map(|name| { let padding = name.len() - 1; (name, padding) @@ -292,11 +310,11 @@ impl Display for Node { let output = format!( "{: Option Ordering + '_>> { if self.dir_first { - Some(Box::new(|a, b| { + return Some(Box::new(|a, b| { Self::dir_comparator(a, b, self.sort.comparator()) - })) - } else { - self.sort.comparator() + })); } + + self.sort.comparator() } fn dir_comparator( @@ -36,13 +37,7 @@ impl Order { match (a.is_dir(), b.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, - _ => { - if let Some(sort) = fallback { - sort(a, b) - } else { - Ordering::Equal - } - } + _ => fallback.map_or_else(|| Ordering::Equal, |sort| sort(a, b)), } } } @@ -53,6 +48,7 @@ impl SortType { match self { Self::Name => Some(Box::new(Self::name_comparator)), Self::Size => Some(Box::new(Self::size_comparator)), + Self::SizeRev => Some(Box::new(Self::size_rev_comparator)), _ => None, } } @@ -62,11 +58,18 @@ impl SortType { a.file_name().cmp(b.file_name()) } - /// Comparator based on `Node` file sizes + /// Comparator that sorts [Node]s by size smallest to largest. fn size_comparator(a: &Node, b: &Node) -> Ordering { let a_size = a.file_size.unwrap_or(0); let b_size = b.file_size.unwrap_or(0); + a_size.cmp(&b_size) + } + + /// Comparator that sorts [Node]s by size largest to smallest. + fn size_rev_comparator(a: &Node, b: &Node) -> Ordering { + let a_size = a.file_size.unwrap_or(0); + let b_size = b.file_size.unwrap_or(0); b_size.cmp(&a_size) } } @@ -85,6 +88,7 @@ impl From for SortType { match ord { cli::Order::Name => SortType::Name, cli::Order::Size => SortType::Size, + cli::Order::SizeRev => SortType::SizeRev, cli::Order::None => SortType::None, } } diff --git a/src/fs/erdtree/tree/mod.rs b/src/fs/erdtree/tree/mod.rs index 8684785e..aca13610 100644 --- a/src/fs/erdtree/tree/mod.rs +++ b/src/fs/erdtree/tree/mod.rs @@ -1,9 +1,10 @@ -use crate::cli::Clargs; -use super::order::Order; use super::{ super::error::Error, - node::{Node, NodePrecursor} + disk_usage::DiskUsage, + node::{Node, NodePrecursor}, + order::Order, }; +use crate::cli::Clargs; use crossbeam::channel::{self, Sender}; use ignore::{WalkParallel, WalkState}; use std::{ @@ -22,6 +23,8 @@ pub mod ui; /// hidden file rules depending on [WalkParallel] config. #[derive(Debug)] pub struct Tree { + #[allow(dead_code)] + disk_usage: DiskUsage, #[allow(dead_code)] icons: bool, level: Option, @@ -36,10 +39,22 @@ pub type TreeComponents = (Node, Branches); impl Tree { /// Initializes a [Tree]. - pub fn new(walker: WalkParallel, order: Order, level: Option, icons: bool) -> TreeResult { - let root = Self::traverse(walker, &order, icons)?; - - Ok(Self { level, order, root, icons }) + pub fn new( + walker: WalkParallel, + order: Order, + level: Option, + icons: bool, + disk_usage: DiskUsage, + ) -> TreeResult { + let root = Self::traverse(walker, &order, icons, &disk_usage)?; + + Ok(Self { + disk_usage, + level, + order, + root, + icons, + }) } /// Returns a reference to the root [Node]. @@ -52,7 +67,12 @@ impl Tree { /// system calls are expected to occur during parallel traversal; thus post-processing of all /// directory entries should be completely CPU-bound. If filesystem I/O or system calls occur /// outside of the parallel traversal step please report an issue. - fn traverse(walker: WalkParallel, order: &Order, icons: bool) -> TreeResult { + fn traverse( + walker: WalkParallel, + order: &Order, + icons: bool, + disk_usage: &DiskUsage, + ) -> TreeResult { let (tx, rx) = channel::unbounded::(); // Receives directory entries from the workers used for parallel traversal to construct the @@ -96,7 +116,7 @@ impl Tree { let tx = Sender::clone(&tx); entry_res - .map(|entry| NodePrecursor::new(entry, icons)) + .map(|entry| NodePrecursor::new(disk_usage, entry, icons)) .map(Node::from) .map(|node| tx.send(node).unwrap()) .map(|_| WalkState::Continue) @@ -151,7 +171,8 @@ impl TryFrom for Tree { fn try_from(clargs: Clargs) -> Result { let walker = WalkParallel::try_from(&clargs)?; let order = Order::from((clargs.sort(), clargs.dirs_first())); - let tree = Tree::new(walker, order, clargs.level(), clargs.icons)?; + let du = DiskUsage::from(clargs.disk_usage()); + let tree = Tree::new(walker, order, clargs.level(), clargs.icons, du)?; Ok(tree) } } @@ -199,8 +220,10 @@ impl Display for Tree { if let Some(iter_children) = child.children() { let mut new_base = base_prefix.to_owned(); - let new_theme = - child.is_symlink().then(|| ui::get_link_theme()).unwrap_or(theme); + let new_theme = child + .is_symlink() + .then(|| ui::get_link_theme()) + .unwrap_or(theme); if last_entry { new_base.push_str(ui::SEP); diff --git a/src/icons.rs b/src/icons.rs index bae0ad34..c74608a8 100644 --- a/src/icons.rs +++ b/src/icons.rs @@ -1,10 +1,10 @@ -use ansi_term::Color; use crate::hash; +use ansi_term::Color; use once_cell::sync::Lazy; use std::{ collections::HashMap, ffi::{OsStr, OsString}, - fs::FileType + fs::FileType, }; /// Lazily evaluated static hash-map of special file-types and their corresponding styled icons. diff --git a/tests/sort.rs b/tests/sort.rs index 5117221a..753c17c1 100644 --- a/tests/sort.rs +++ b/tests/sort.rs @@ -51,15 +51,15 @@ fn sort_size() { indoc!( " data (1.24 KB) - ├─ lipsum (446.00 B) - │ └─ lipsum.txt (446.00 B) - ├─ dream_cycle (308.00 B) - │ └─ polaris.txt (308.00 B) - ├─ nemesis.txt (161.00 B) + ├─ necronomicon.txt (83.00 B) + ├─ nylarlathotep.txt (100.00 B) ├─ the_yellow_king (143.00 B) │ └─ cassildas_song.md (143.00 B) - ├─ nylarlathotep.txt (100.00 B) - └─ necronomicon.txt (83.00 B)" + ├─ nemesis.txt (161.00 B) + ├─ dream_cycle (308.00 B) + │ └─ polaris.txt (308.00 B) + └─ lipsum (446.00 B) + └─ lipsum.txt (446.00 B)" ), "Failed to sort by descending size" ) @@ -72,15 +72,15 @@ fn sort_size_dir_first() { indoc!( " data (1.24 KB) - ├─ lipsum (446.00 B) - │ └─ lipsum.txt (446.00 B) - ├─ dream_cycle (308.00 B) - │ └─ polaris.txt (308.00 B) ├─ the_yellow_king (143.00 B) │ └─ cassildas_song.md (143.00 B) - ├─ nemesis.txt (161.00 B) + ├─ dream_cycle (308.00 B) + │ └─ polaris.txt (308.00 B) + ├─ lipsum (446.00 B) + │ └─ lipsum.txt (446.00 B) + ├─ necronomicon.txt (83.00 B) ├─ nylarlathotep.txt (100.00 B) - └─ necronomicon.txt (83.00 B)" + └─ nemesis.txt (161.00 B)" ), "Failed to sort by directory and descending size" )