summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md18
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--src/app/app.rs4
-rw-r--r--src/browser/browser_state.rs28
-rw-r--r--src/clap.rs15
-rw-r--r--src/display/displayable_tree.rs73
-rw-r--r--src/file_sizes/file_sizes_default.rs81
-rw-r--r--src/file_sizes/file_sizes_unix.rs95
-rw-r--r--src/file_sizes/mod.rs108
-rw-r--r--src/file_sum/mod.rs150
-rw-r--r--src/file_sum/sum_computation.rs152
-rw-r--r--src/lib.rs2
-rw-r--r--src/skin/style_map.rs1
-rw-r--r--src/tree/sort.rs15
-rw-r--r--src/tree/tree.rs68
-rw-r--r--src/tree/tree_line.rs24
-rw-r--r--src/tree/tree_options.rs32
-rw-r--r--src/tree_build/bline.rs2
-rw-r--r--src/tree_build/builder.rs2
-rw-r--r--src/verb/builtin.rs2
-rw-r--r--src/verb/internal.rs2
-rw-r--r--website/docs/navigation.md2
23 files changed, 477 insertions, 403 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a19135..c8e33bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,19 @@
### Next version
+#### Major change: Recursive date computation
+The date of directories is now the modification date of the last modified inner file, whatever its depth. This is computed in the background and doesn't slow your navigation.
#### Major change: Sort mode
Size can now be displayed out of sort mode, which concerns either size or dates.
-There are two new launch arguments:
-`--sort-by-dates` : sort by... dates
-`--sort-by-sizes` or `-w` : "whale hunt" mode, the equivalent of what was previously `-s`
+There are new launch arguments:
+`--sort-by-count` : sort by number of files in directories
+`--sort-by-date` : sort by dates, taking content into account (make it easy to find deep recent files)
+`--sort-by-size` or `-w` : "whale spotting" mode, the equivalent of what was previously `-s`
The `-s` launch argument now works similarly to -d or -p : it doesn't activate a sort mode but activates showing the sizes.
-Similarly three new verbs have been defined:
-`:sort_by_dates` has for shortcut `sd`
-`:sort_by_sizes` has `ss` as shortcut
+Similarly new verbs have been defined:
+`:toggle_counts`, with shortcut `counts` shows the number of files in directories
+`:toggle_sizes`, with shortcut `sizes` shows the sizes of files and directories
+`:sort_by_count` has for shortcut `sc`
+`:sort_by_date` has for shortcut `sd`
+`:sort_by_size` has `ss` as shortcut
`:no_sort` removes the current sort mode, if any
<a name="v0.17.0"></a>
diff --git a/Cargo.lock b/Cargo.lock
index 0e15f1c..f861c3c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "broot"
-version = "0.17.0"
+version = "0.18.0-dev"
dependencies = [
"bet",
"chrono",
diff --git a/Cargo.toml b/Cargo.toml
index 3070fa3..88406a8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "broot"
-version = "0.17.0"
+version = "0.18.0-dev"
authors = ["dystroy <denys.seguret@gmail.com>"]
repository = "https://github.com/Canop/broot"
documentation = "https://dystroy.org/broot"
diff --git a/src/app/app.rs b/src/app/app.rs
index 64f554d..b2b9d0c 100644
--- a/src/app/app.rs
+++ b/src/app/app.rs
@@ -6,7 +6,7 @@ use {
conf::Conf,
display::{Areas, Screen, W},
errors::ProgramError,
- file_sizes, git,
+ file_sum, git,
launchable::Launchable,
skin::*,
task_sync::Dam,
@@ -375,6 +375,6 @@ impl App {
/// This should be done on Refresh actions and after any external
/// command.
fn clear_caches() {
- file_sizes::clear_cache();
+ file_sum::clear_cache();
git::clear_status_computer_cache();
}
diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs
index e670afc..0da7406 100644
--- a/src/browser/browser_state.rs
+++ b/src/browser/browser_state.rs
@@ -262,8 +262,8 @@ impl AppState for BrowserState {
fn get_pending_task(&self) -> Option<&'static str> {
if self.pending_pattern.is_some() {
Some("searching")
- } else if self.displayed_tree().has_dir_missing_size() {
- Some("computing sizes")
+ } else if self.displayed_tree().has_dir_missing_sum() {
+ Some("computing stats")
} else if self.displayed_tree().is_missing_git_status_computation() {
Some("computing git status")
} else {
@@ -550,10 +550,25 @@ impl AppState for BrowserState {
}
}
}
+ Internal::sort_by_count => {
+ self.with_new_options(
+ screen, &|o| {
+ if o.sort == Sort::Count {
+ o.sort = Sort::None;
+ o.show_counts = false;
+ } else {
+ o.sort = Sort::Count;
+ o.show_counts = true;
+ }
+ },
+ bang,
+ con,
+ )
+ }
Internal::sort_by_date => {
self.with_new_options(
screen, &|o| {
- if o.sort.is_date() {
+ if o.sort == Sort::Date {
o.sort = Sort::None;
o.show_dates = false;
} else {
@@ -568,7 +583,7 @@ impl AppState for BrowserState {
Internal::sort_by_size => {
self.with_new_options(
screen, &|o| {
- if o.sort.is_size() {
+ if o.sort == Sort::Size {
o.sort = Sort::None;
o.show_sizes = false;
} else {
@@ -583,6 +598,9 @@ impl AppState for BrowserState {
Internal::no_sort => {
self.with_new_options(screen, &|o| o.sort = Sort::None, bang, con)
}
+ Internal::toggle_counts => {
+ self.with_new_options(screen, &|o| o.show_counts ^= true, bang, con)
+ }
Internal::toggle_dates => {
self.with_new_options(screen, &|o| o.show_dates ^= true, bang, con)
}
@@ -670,7 +688,7 @@ impl AppState for BrowserState {
let git_status = git::get_tree_status(root_path, dam);
self.displayed_tree_mut().git_status = git_status;
} else {
- self.displayed_tree_mut().fetch_some_missing_dir_size(dam);
+ self.displayed_tree_mut().fetch_some_missing_dir_sum(dam);
}
}
diff --git a/src/clap.rs b/src/clap.rs
index cd90559..8907307 100644
--- a/src/clap.rs
+++ b/src/clap.rs
@@ -94,15 +94,20 @@ pub fn clap_app() -> clap::App<'static, 'static> {
.help("Don't show sizes"),
)
.arg(
- clap::Arg::with_name("sort-by-dates")
- .long("sort-by-dates")
+ clap::Arg::with_name("sort-by-count")
+ .long("sort-by-count")
+ .help("Sort by count (only show one level of the tree)"),
+ )
+ .arg(
+ clap::Arg::with_name("sort-by-date")
+ .long("sort-by-date")
.help("Sort by date (only show one level of the tree)"),
)
.arg(
- clap::Arg::with_name("sort-by-sizes")
+ clap::Arg::with_name("sort-by-size")
.short("w")
- .long("sort-by-sizes")
- .help("Sort by size (\"whale hunt\" mode)"),
+ .long("sort-by-size")
+ .help("Sort by size (\"whale spotting\" mode)"),
)
.arg(
clap::Arg::with_name("no-sort")
diff --git a/src/display/displayable_tree.rs b/src/display/displayable_tree.rs
index a9cb21e..c8bf575 100644
--- a/src/display/displayable_tree.rs
+++ b/src/display/displayable_tree.rs
@@ -7,13 +7,13 @@ use {
crate::{
content_search::ContentMatch,
errors::ProgramError,
- file_sizes::FileSize,
+ file_sum::FileSum,
pattern::PatternObject,
skin::StyleMap,
task_sync::ComputationResult,
tree::{Tree, TreeLine, TreeLineType},
},
- chrono::{offset::Local, DateTime},
+ chrono::{Local, DateTime, TimeZone},
crossterm::{
cursor,
style::{Color, SetBackgroundColor},
@@ -21,7 +21,7 @@ use {
QueueableCommand,
},
git2::Status,
- std::{io::Write, time::SystemTime},
+ std::io::Write,
termimad::{CompoundStyle, ProgressBar},
};
@@ -73,6 +73,24 @@ impl<'s, 't> DisplayableTree<'s, 't> {
}
}
+ fn write_line_count<'w, W>(
+ &self,
+ cw: &mut CropWriter<'w, W>,
+ line: &TreeLine,
+ selected: bool,
+ ) -> Result<(), termimad::Error>
+ where
+ W: Write,
+ {
+ if let Some(s) = line.sum {
+ cond_bg!(count_style, self, selected, self.skin.count);
+ cw.queue_string(&count_style, format!("{:>8}", s.to_count()))?;
+ cw.queue_char(&self.skin.default, ' ')
+ } else {
+ cw.queue_str(&self.skin.tree, "─────────")
+ }
+ }
+
fn write_line_size<'w, W>(
&self,
cw: &mut CropWriter<'w, W>,
@@ -82,11 +100,12 @@ impl<'s, 't> DisplayableTree<'s, 't> {
where
W: Write,
{
- if let Some(s) = line.size {
+ if let Some(s) = line.sum {
cond_bg!(size_style, self, selected, self.name_style(&line));
- cw.queue_string(&size_style, format!("{} ", s))
+ cw.queue_string(&size_style, s.to_size_string())?;
+ cw.queue_char(&self.skin.default, ' ')
} else {
- cw.queue_str(&self.skin.tree, "───────")
+ cw.queue_str(&self.skin.tree, "─────")
}
}
@@ -96,21 +115,21 @@ impl<'s, 't> DisplayableTree<'s, 't> {
&self,
cw: &mut CropWriter<'w, W>,
line: &TreeLine,
- total_size: FileSize,
+ total_size: FileSum,
selected: bool,
) -> Result<(), termimad::Error>
where
W: Write,
{
- if let Some(s) = line.size {
- let pb = ProgressBar::new(s.part_of(total_size), 10);
+ if let Some(s) = line.sum {
+ let pb = ProgressBar::new(s.part_of_size(total_size), 10);
cond_bg!(size_style, self, selected, self.name_style(&line));
cond_bg!(sparse_style, self, selected, self.skin.sparse);
- cw.queue_string(&size_style, format!("{:>5}", s.to_string()))?;
- cw.queue_char(&sparse_style, if s.sparse { 's' } else { ' ' })?;
+ cw.queue_string(&size_style, format!("{:>5}", s.to_size_string()))?;
+ cw.queue_char(&sparse_style, if s.is_sparse() { 's' } else { ' ' })?;
cw.queue_string(&size_style, format!("{:<10} ", pb))
} else {
- cw.queue_str(&self.skin.tree, "──────────────── ")
+ cw.queue_str(&self.skin.tree, "─────────────────")
}
}
@@ -140,13 +159,13 @@ impl<'s, 't> DisplayableTree<'s, 't> {
fn write_date<'w, W>(
&self,
cw: &mut CropWriter<'w, W>,
- system_time: SystemTime,
+ seconds: i64,
selected: bool,
) -> Result<(), termimad::Error>
where
W: Write,
{
- let date_time: DateTime<Local> = system_time.into();
+ let date_time: DateTime<Local> = Local.timestamp(seconds, 0);
cond_bg!(date_style, self, selected, self.skin.dates);
cw.queue_string(date_style, date_time.format(self.tree.options.date_time_format).to_string())
}
@@ -359,7 +378,7 @@ impl<'s, 't> DisplayableTree<'s, 't> {
let tree = self.tree;
#[cfg(unix)]
let user_group_max_lengths = user_group_max_lengths(&tree);
- let total_size = tree.total_size();
+ let total_size = tree.total_sum();
let scrollbar = if self.in_app {
self.area
.scrollbar(tree.scroll, tree.lines.len() as i32 - 1)
@@ -389,7 +408,6 @@ impl<'s, 't> DisplayableTree<'s, 't> {
if line_index < tree.lines.len() {
let line = &tree.lines[line_index];
selected = self.in_app && line_index == tree.selection;
- //let pattern_match = tree.options.pattern.pattern.find(Candidate.from(line));
if !tree.git_status.is_none() {
self.write_line_git_status(cw, line)?;
}
@@ -411,14 +429,6 @@ impl<'s, 't> DisplayableTree<'s, 't> {
},
)?;
}
- if tree.options.show_sizes {
- if tree.options.sort.is_some() {
- // as soon as there's only one level displayed we can show the size bars
- self.write_line_size_with_bar(cw, line, total_size, selected)?;
- } else {
- self.write_line_size(cw, line, selected)?;
- }
- }
#[cfg(unix)]
{
if tree.options.show_permissions {
@@ -446,12 +456,23 @@ impl<'s, 't> DisplayableTree<'s, 't> {
}
}
if tree.options.show_dates {
- if let Some(date) = line.modified() {
- self.write_date(cw, date, selected)?;
+ if let Some(seconds) = line.sum.and_then(|sum| sum.to_valid_seconds()) {
+ self.write_date(cw, seconds, selected)?;
} else {
cw.queue_str(&self.skin.tree, "─────────────────")?;
}
}
+ if tree.options.show_sizes {
+ if tree.options.sort.is_some() {
+ // as soon as there's only one level displayed we can show the size bars
+ self.write_line_size_with_bar(cw, line, total_size, selected)?;
+ } else {
+ self.write_line_size(cw, line, selected)?;
+ }
+ }
+ if tree.options.show_counts {
+ self.write_line_count(cw, line, selected)?;
+ }
self.write_line_label(cw, line, pattern_object, selected)?;
if cw.allowed > 8 && pattern_object.content {
let extract = tree.options.pattern.pattern.search_content(&line.path, cw.allowed - 2);
diff --git a/src/file_sizes/file_sizes_default.rs b/src/file_sizes/file_sizes_default.rs
deleted file mode 100644
index 178b75c..0000000
--- a/src/file_sizes/file_sizes_default.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-//! size computation for non linux
-
-use {
- super::FileSize,
- crate::task_sync::Dam,
- crossbeam::{channel::unbounded, sync::WaitGroup},
- std::{
- fs,
- path::{Path, PathBuf},
- sync::atomic::{AtomicIsize, AtomicUsize, Ordering},
- sync::Arc,
- thread,
- time::Duration,
- },
-};
-
-// Note that this version doesn't try to compute the real size taken
-// on disk but report the value given by the `len` function
-pub fn compute_dir_size(path: &Path, dam: &Dam) -> Option<u64> {
- let size = Arc::new(AtomicUsize::new(0));
-
- // this MPMC channel contains the directory paths which must be handled
- let (dirs_sender, dirs_receiver) = unbounded();
-
- // busy is the number of directories which are either being processed or queued
- // We use this count to determine when threads can stop waiting for tasks
- let busy = Arc::new(AtomicIsize::new(0));
- busy.fetch_add(1, Ordering::Relaxed);
- dirs_sender.send(Some(PathBuf::from(path))).unwrap();
-
- let wg = WaitGroup::new();
- let period = Duration::from_micros(50);
- for _ in 0..8 {
- let size = Arc::clone(&size);
- let busy = Arc::clone(&busy);
- let wg = wg.clone();
- let (dirs_sender, dirs_receiver) = (dirs_sender.clone(), dirs_receiver.clone());
- let observer = dam.observer();
- thread::spawn(move || {
- loop {
- let o = dirs_receiver.recv_timeout(period);
- if let Ok(Some(open_dir)) = o {
- if let Ok(entries) = fs::read_dir(&open_dir) {
- for e in entries.flatten() {
- if let Ok(md) = e.metadata() {
- if md.is_dir() {
- busy.fetch_add(1, Ordering::Relaxed);
- dirs_sender.send(Some(e.path())).unwrap();
- }
- size.fetch_add(md.len() as usize, Ordering::Relaxed);
- }
- }
- }
- busy.fetch_sub(1, Ordering::Relaxed);
- dirs_sender.send(None).unwrap();
- } else if busy.load(Ordering::Relaxed) < 1 {
- break;
- }
- if observer.has_event() {
- break;
- }
- }
- drop(wg);
- });
- }
- wg.wait();
-
- if dam.has_event() {
- return None;
- }
- let size: usize = size.load(Ordering::Relaxed);
- let size: u64 = size as u64;
- Some(size)
-}
-
-pub fn compute_file_size(path: &Path) -> FileSize {
- match fs::metadata(path) {
- Ok(m) => FileSize::new(m.len(), false),
- Err(_) => FileSize::new(0, false),
- }
-}
diff --git a/src/file_sizes/file_sizes_unix.rs b/src/file_sizes/file_sizes_unix.rs
deleted file mode 100644
index 2c8b932..0000000
--- a/src/file_sizes/file_sizes_unix.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use {
- super::FileSize,
- crate::task_sync::Dam,
- crossbeam::{channel::unbounded, sync::WaitGroup},
- std::{
- collections::HashSet,
- fs,
- os::unix::fs::MetadataExt,
- path::{Path, PathBuf},
- sync::{
- atomic::{AtomicIsize, AtomicU64, Ordering},
- Arc, Mutex,
- },
- thread,
- time::Duration,
- },
-};
-
-pub fn compute_dir_size(path: &Path, dam: &Dam) -> Option<u64> {
- //debug!("compute size of dir {:?} --------------- ", path);
- let inodes = Arc::new(Mutex::new(HashSet::<u64>::default())); // to avoid counting twice an inode
- // the computation is done on blocks of 512 bytes
- // see https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.blocks
- let blocks = Arc::new(AtomicU64::new(0));
-
- // this MPMC channel contains the directory paths which must be handled
- let (dirs_sender, dirs_receiver) = unbounded();
-
- // busy is the number of directories which are either being processed or queued
- // We use this count to determine when threads can stop waiting for tasks
- let busy = Arc::new(AtomicIsize::new(0));
- busy.fetch_add(1, Ordering::Relaxed);
- dirs_sender.send(Some(PathBuf::from(path))).unwrap();
-
- let wg = WaitGroup::new();
- let period = Duration::from_micros(50);
- for _ in 0..8 {
- let blocks = Arc::clone(&blocks);
- let busy = Arc::clone(&busy);
- let wg = wg.clone();
- let (dirs_sender, dirs_receiver) = (dirs_sender.clone(), dirs_receiver.clone());
- let inodes = inodes.clone();
- let observer = dam.observer();
- thread::spawn(move || {
- loop {
- let o = dirs_receiver.recv_timeout(period);
- if let Ok(Some(open_dir)) = o {
- if let Ok(entries) = fs::read_dir(&open_dir) {
- for e in entries.flatten() {
- if let Ok(md) = e.metadata() {
- if md.is_dir() {
- busy.fetch_add(1, Ordering::Relaxed);
- dirs_sender.send(Some(e.path())).unwrap();
- } else if md.nlink() > 1 {
- let mut inodes = inodes.lock().unwrap();
- if !inodes.insert(md.ino()) {
- // it was already in the set
- continue; // let's not add the blocks
- }
- }
- blocks.fetch_add(md.blocks(), Ordering::Relaxed);
- }
- }
- }
- busy.fetch_sub(1, Ordering::Relaxed);
- dirs_sender.send(None).unwrap();
- } else if busy.load(Ordering::Relaxed) < 1 {
- break;
- }
- if observer.has_event() {
- break;
- }
- }
- drop(wg);
- });
- }
- wg.wait();
-
- if dam.has_event() {
- return None;
- }
- let blocks = blocks.load(Ordering::Relaxed);
- Some(blocks * 512)
-}
-
-pub fn compute_file_size(path: &Path) -> FileSize {
- match fs::metadata(path) {
- Ok(md) => {
- let nominal_size = md.size();
- let block_size = md.blocks() * 512;
- FileSize::new(block_size.min(nominal_size), block_size < nominal_size)
- }
- Err(_) => FileSize::new(0, false),
- }
-}
diff --git a/src/file_sizes/mod.rs b/src/file_sizes/mod.rs
deleted file mode 100644
index 0e9451d..0000000
--- a/src/file_sizes/mod.rs
+++ /dev/null
@@ -1,108 +0,0 @@
-/// compute the summed size of directories.
-/// A cache is used to avoid recomputing the
-/// same directories again and again.
-/// Hard links are checked to avoid counting
-/// twice an inode.
-///
-use {
- crate::task_sync::Dam,
- std::{
- collections::HashMap,
- fmt,
- ops::AddAssign,
- path::{Path, PathBuf},
- sync::Mutex,
- },
-};
-
-const SIZE_NAMES: &[&str] = &["", "K", "M", "G", "T", "P", "E", "Z", "Y"];
-
-lazy_static! {
- static ref SIZE_CACHE_MUTEX: Mutex<HashMap<PathBuf, u64>> = Mutex::new(HashMap::new());
-}
-
-pub fn clear_cache() {
- let mut size_cache = SIZE_CACHE_MUTEX.lock().unwrap();
- size_cache.clear();
-}
-
-#[derive(Debug, Copy, Clone)]
-pub struct FileSize {
- real_size: u64, // bytes, the space it takes on disk
- pub sparse: bool, // only for non directories: tells whether the file is sparse
-}
-
-impl FileSize {
- pub fn new(real_size: u64, sparse: bool) -> Self {
- Self { real_size, sparse }
- }
-
- /// return the size of the given file, which is assumed
- /// to be a normal file (ie not a directory)
- pub fn from_file(path: &Path) -> Self {
- compute_file_size(path)
- }
-
- /// Return the size of the directory, either by computing it of by
- /// fetching it from cache.
- /// If the lifetime expires before complete computation, None is returned.
- pub fn from_dir(path: &Path, dam: &Dam) -> Option<Self> {
- let mut size_cache = SIZE_CACHE_MUTEX.lock().unwrap();
- if let Some(s) = size_cache.get(path) {
- return Some(Self::new(*s, false));
- }
- if let Some(s) = time!(Debug, "size sum", path, compute_dir_size(path, dam)) {
- size_cache.insert(PathBuf::from(path), s);
- Some(FileSize::new(s, false))
- } else {
- None
- }
- }
-
- pub fn part_of(self, total: Self) -> f32 {
- if total.real_size == 0 {
- 0.0
- } else {
- self.real_size as f32 / total.real_size as f32
- }
- }
-}
-
-impl fmt::Display for FileSize {
- /// format a number of bytes as a string, for example 247K
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let mut v = self.real_size;
- let mut i = 0;
- while v >= 5000 && i < SIZE_NAMES.len() - 1 {
- //v >>= 10;
- v /= 1000;
- i += 1;
- }
- write!(f, "{}{}", v, &SIZE_NAMES[i])
- }
-}
-
-impl AddAssign for FileSize {
- #[allow(clippy::suspicious_op_assign_impl)]
- fn add_assign(&mut self, other: Self) {
- *self = Self::new(self.real_size + other.real_size, self.sparse | other.sparse);
- }
-}
-
-impl Into<u64> for FileSize {
- fn into(self) -> u64 {
- self.real_size
- }
-}
-
-// ---------------- OS dependent implementations
-
-#[cfg(unix)]
-mod file_sizes_unix;
-#[cfg(unix)]
-use file_sizes_unix::*;
-
-#[cfg(not(unix))]
-mod file_sizes_default;
-#[cfg(not(unix))]
-use file_sizes_default::*;
diff --git a/src/file_sum/mod.rs b/src/file_sum/mod.rs
new file mode 100644
index 0000000..fa6eaf5
--- /dev/null
+++ b/src/file_sum/mod.rs
@@ -0,0 +1,150 @@
+/// compute the summed sum of directories.
+/// A cache is used to avoid recomputing the
+/// same directories again and again.
+/// Hard links are checked to avoid counting
+/// twice an inode.
+///
+
+mod sum_computation;
+
+use {
+ crate::task_sync::Dam,
+ std::{
+ collections::HashMap,
+ fs::Metadata,
+ ops::AddAssign,
+ path::{Path, PathBuf},
+ sync::Mutex,
+ time::UNIX_EPOCH,
+ },
+};
+
+const SUM_NAMES: &[&str] = &["", "K", "M", "G", "T", "P", "E", "Z", "Y"];
+
+lazy_static! {
+ static ref SUM_CACHE_MUTEX: Mutex<HashMap<PathBuf, FileSum>> = Mutex::new(HashMap::new());
+}
+
+pub fn clear_cache() {
+ let mut sum_cache = SUM_CACHE_MUTEX.lock().unwrap();
+ sum_cache.clear();
+}
+
+pub fn extract_seconds(md: &Metadata) -> u64 {
+ md.modified().map_or(
+ 0,
+ |st| st.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
+ )
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct FileSum {
+ real_size: u64, // bytes, the space it takes on disk
+ sparse: bool, // only for non directories: tells whether the file is sparse
+ count: usize, // number of files
+ modified: u64, // seconds from Epoch to last modification, or 0 if there was an error
+}
+
+impl FileSum {
+ pub fn new(
+ real_size: u64,
+ sparse: bool,
+ count: usize,
+ modified: u64,
+ ) -> Self {
+ Self { real_size, sparse, count, modified }
+ }
+
+ pub fn zero() -> Self {
+ Self::new(0, false, 0, 0)
+ }
+
+ pub fn incr(&mut self) {
+ self.count += 1;
+ }
+
+ /// return the sum of the given file, which is assumed
+ /// to be a normal file (ie not a directory)
+ pub fn from_file(path: &Path) -> Self {
+ sum_computation::compute_file_sum(path)
+ }
+
+ /// Return the sum of the directory, either by computing it of by
+ /// fetching it from cache.
+ /// If the lifetime expires before complete computation, None is returned.
+ pub fn from_dir(path: &Path, dam: &Dam) -> Option<Self> {
+ let mut sum_cache = SUM_CACHE_MUTEX.lock().unwrap();
+ match sum_cache.get(path) {
+ Some(sum) => Some(*sum),
+ None => {
+ let sum = time!(
+ Debug,
+ "sum computation",
+ path,
+ sum_computation::compute_dir_sum(path, dam),
+ );
+ if let Some(sum) = sum {
+ sum_cache.insert(PathBuf::from(path), sum);
+ }
+ sum
+ }
+ }
+ }
+
+ pub fn part_of_size(self, total: Self) -> f32 {
+ if total.real_size == 0 {
+ 0.0
+ } else {
+ self.real_size as f32 / total.real_size as f32
+ }
+ }
+ /// format a number of bytes as a string, for example 247K
+ pub fn to_size_string(self) -> String {
+ let mut v = self.real_size;
+ let mut i = 0;
+ while v >= 5000 && i < SUM_NAMES.len() - 1 {
+ v /= 1000;
+ i += 1;
+ }
+ format!("{}{}", v, &SUM_NAMES[i])
+ }
+ /// return the number of files (normally at least 1)
+ pub fn to_count(self) -> usize {
+ self.count
+ }
+ /// return the number of seconds from Epoch to last modification,
+ /// or 0 if the computation failed
+ pub fn to_seconds(self) -> u64 {
+ self.modified
+ }
+ /// return the size in bytes
+ pub fn to_size(self) -> u64 {
+ self.real_size
+ }
+ pub fn to_valid_seconds(self) -> Option<i64> {
+ if self.modified != 0 {
+ Some(self.modified as i64)
+ } else {
+ None
+ }
+ }
+ /// tell whether the file has holes (in which case the size displayed by
+ /// other tools may be greater than the "real" one returned by broot).
+ /// Not computed (return false) on windows or for directories.
+ pub fn is_sparse(self) -> bool {
+ self.sparse
+ }
+}
+
+impl AddAssign for FileSum {
+ #[allow(clippy::suspicious_op_assign_impl)]
+ fn add_assign(&mut self, other: Self) {
+ *self = Self::new(
+ self.real_size + other.real_size,
+ self.sparse | other.sparse,
+ self.count + other.count,
+ self.modified.max(other.modified),
+ );
+ }
+}
+
diff --git a/src/file_sum/sum_computation.rs b/src/file_sum/sum_computation.rs
new file mode 100644
index 0000000..6e7a099
--- /dev/null
+++ b/src/file_sum/sum_computation.rs
@@ -0,0 +1,152 @@
+use {
+ super::{extract_seconds, FileSum},
+ crate::task_sync::Dam,
+ crossbeam::channel,
+ std::{
+ collections::HashSet,
+ fs,
+ path::{Path, PathBuf},
+ sync::{
+ atomic::{AtomicIsize, Ordering},
+ Arc, Mutex,
+ },
+ thread,
+ },
+};
+
+#[cfg(unix)]
+use std::os::unix::fs::MetadataExt;
+
+const THREADS_COUNT: usize = 8;
+
+/// compute the consolidated numbers for a directory, with implementation
+/// varying depending on the OS:
+/// On unix, the computation is done on blocks of 512 bytes
+/// see https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.blocks
+pub fn compute_dir_sum(path: &Path, dam: &Dam) -> Option<FileSum> {
+ //debug!("compute size of dir {:?} --------------- ", path);
+
+ // to avoid counting twice an inode, we store them in a set
+ #[cfg(unix)]
+ let inodes = Arc::new(Mutex::new(HashSet::<u64>::default()));
+
+ // this MPMC channel contains the directory paths which must be handled.
+ // A None means there's nothing left and the thread may send its result and stop
+ le