diff options
-rw-r--r-- | CHANGELOG.md | 18 | ||||
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/app/app.rs | 4 | ||||
-rw-r--r-- | src/browser/browser_state.rs | 28 | ||||
-rw-r--r-- | src/clap.rs | 15 | ||||
-rw-r--r-- | src/display/displayable_tree.rs | 73 | ||||
-rw-r--r-- | src/file_sizes/file_sizes_default.rs | 81 | ||||
-rw-r--r-- | src/file_sizes/file_sizes_unix.rs | 95 | ||||
-rw-r--r-- | src/file_sizes/mod.rs | 108 | ||||
-rw-r--r-- | src/file_sum/mod.rs | 150 | ||||
-rw-r--r-- | src/file_sum/sum_computation.rs | 152 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/skin/style_map.rs | 1 | ||||
-rw-r--r-- | src/tree/sort.rs | 15 | ||||
-rw-r--r-- | src/tree/tree.rs | 68 | ||||
-rw-r--r-- | src/tree/tree_line.rs | 24 | ||||
-rw-r--r-- | src/tree/tree_options.rs | 32 | ||||
-rw-r--r-- | src/tree_build/bline.rs | 2 | ||||
-rw-r--r-- | src/tree_build/builder.rs | 2 | ||||
-rw-r--r-- | src/verb/builtin.rs | 2 | ||||
-rw-r--r-- | src/verb/internal.rs | 2 | ||||
-rw-r--r-- | website/docs/navigation.md | 2 |
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> @@ -84,7 +84,7 @@ dependencies = [ [[package]] name = "broot" -version = "0.17.0" +version = "0.18.0-dev" dependencies = [ "bet", "chrono", @@ -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 |