diff --git a/src/fs/erdtree/node.rs b/src/fs/erdtree/node.rs index daf7fbb3..eb447432 100644 --- a/src/fs/erdtree/node.rs +++ b/src/fs/erdtree/node.rs @@ -1,4 +1,8 @@ -use super::{disk_usage::DiskUsage, get_ls_colors}; +use super::{ + super::inode::Inode, + 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}, @@ -31,6 +35,7 @@ pub struct Node { children: Option>, file_name: OsString, file_type: Option, + inode: Option, path: PathBuf, show_icon: bool, style: Style, @@ -45,6 +50,7 @@ impl Node { children: Option>, file_name: OsString, file_type: Option, + inode: Option, path: PathBuf, show_icon: bool, style: Style, @@ -56,6 +62,7 @@ impl Node { file_name, file_size, file_type, + inode, path, show_icon, style, @@ -146,6 +153,11 @@ impl Node { &self.style } + /// Returns reference to underlying [Inode] if any. + pub fn inode(&self) -> Option<&Inode> { + self.inode.as_ref() + } + /// Gets stylized icon for node if enabled. Icons without extensions are styled based on the /// [`LS_COLORS`] foreground configuration of the associated file name. /// @@ -263,12 +275,19 @@ impl From> for Node { } }; + let inode = metadata + .map(Inode::try_from) + .transpose() + .ok() + .flatten(); + Self::new( depth, file_size, children, file_name, file_type, + inode, path.into(), show_icon, style, diff --git a/src/fs/erdtree/tree/mod.rs b/src/fs/erdtree/tree/mod.rs index aca13610..e1b6f33c 100644 --- a/src/fs/erdtree/tree/mod.rs +++ b/src/fs/erdtree/tree/mod.rs @@ -8,7 +8,7 @@ use crate::cli::Clargs; use crossbeam::channel::{self, Sender}; use ignore::{WalkParallel, WalkState}; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, convert::TryFrom, fmt::{self, Display, Formatter}, path::PathBuf, @@ -79,6 +79,7 @@ impl Tree { // components needed to assemble a `Tree`. let tree_components = thread::spawn(move || -> TreeResult { let mut branches: Branches = HashMap::new(); + let mut inodes = HashSet::new(); let mut root = None; while let Ok(node) = rx.recv() { @@ -93,6 +94,13 @@ impl Tree { root = Some(node); continue; } + } else { + // If a hard-link is already accounted for skip the subsequent one. + if let Some(inode) = node.inode() { + if !inodes.insert(inode.properties()) { + continue + } + } } let parent = node.parent_path_buf().ok_or(Error::ExpectedParent)?; diff --git a/src/fs/inode.rs b/src/fs/inode.rs new file mode 100644 index 00000000..040d5580 --- /dev/null +++ b/src/fs/inode.rs @@ -0,0 +1,68 @@ +use std::{ + convert::TryFrom, + error::Error as StdError, + fmt::{self, Display}, + fs::Metadata, +}; + +/// Represents a file's underlying inode. +#[derive(Debug)] +pub struct Inode { + ino: u64, + dev: u64, + nlink: u64 +} + +impl Inode { + /// Initializer for an inode given all the properties that make it unique. + pub fn new(ino: u64, dev: u64, nlink: u64) -> Self { + Self { ino, dev, nlink } + } + + /// Returns a tuple of all the fields of the [Inode]. + pub fn properties(&self) -> (u64, u64, u64) { + (self.ino, self.dev, self.nlink) + } +} + +#[derive(Debug)] +pub struct Error; + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Insufficient information to compute inode") + } +} + +impl StdError for Error {} + +impl TryFrom for Inode { + type Error = Error; + + #[cfg(unix)] + fn try_from(md: Metadata) -> Result { + use std::os::unix::fs::MetadataExt; + + Ok(Self::new(md.ino(), md.dev(), md.nlink())) + } + + #[cfg(windows)] + fn try_from(md: Metadata) -> Result { + use std::os::windows::fs::MetadataExt; + + if let (Some(ino), Some(dev), Some(nlinks)) = ( + metadata.file_index(), + metadata.volume_serial_number(), + metadata.number_of_links(), + ) { + return Ok(Self::new(md, dev, nlink)); + } + + Err(Error {}) + } + + #[cfg(not(any(unix, windows)))] + fn try_from(md: Metadata) -> Result { + Err(Error {}) + } +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 1ebed201..d2bd8a27 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -1,6 +1,9 @@ /// Errors related to filesystem traversal. pub mod error; +/// Operations pertaining to Inodes. +pub mod inode; + /// Operations to present disk usage in human-readable format. pub mod file_size;