diff --git a/Cargo.lock b/Cargo.lock index c9ec3ba..f114da3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -426,6 +432,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "parallel-disk-usage" version = "0.10.0" @@ -448,6 +463,7 @@ dependencies = [ "serde", "serde_json", "smart-default", + "sysinfo", "terminal_size", "text-block-macros 0.2.0", "zero-copy-pads", @@ -673,6 +689,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + [[package]] name = "terminal_size" version = "0.4.0" @@ -757,6 +787,81 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 9fd3876..a4b3794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ clap_complete = { version = "^4.5.36", optional = true } clap-utilities = { version = "^0.2.0", optional = true } serde = { version = "^1.0.214", optional = true } serde_json = { version = "^1.0.132", optional = true } +sysinfo = "0.32.0" [dev-dependencies] build-fs-tree = "^0.7.1" diff --git a/src/app/sub.rs b/src/app/sub.rs index 8c97cd3..1314e04 100644 --- a/src/app/sub.rs +++ b/src/app/sub.rs @@ -1,3 +1,6 @@ +mod hdd; +mod mount_point; + use crate::{ args::Fraction, data_tree::{DataTree, DataTreeReflection}, @@ -11,8 +14,10 @@ use crate::{ status_board::GLOBAL_STATUS_BOARD, visualizer::{BarAlignment, ColumnWidthDistribution, Direction, Visualizer}, }; +use hdd::any_path_is_in_hdd; use serde::Serialize; use std::{io::stdout, iter::once, num::NonZeroUsize, path::PathBuf}; +use sysinfo::Disks; /// The sub program of the main application. pub struct Sub @@ -69,6 +74,17 @@ where no_sort, } = self; + // If one of the files is on HDD, set thread number to 1 + let disks = Disks::new_with_refreshed_list(); + + if any_path_is_in_hdd::(&files, &disks) { + eprintln!("warning: HDD detected, the thread limit will be set to 1"); + rayon::ThreadPoolBuilder::new() + .num_threads(1) + .build_global() + .unwrap_or_else(|_| eprintln!("warning: Failed to set thread limit to 1")); + } + let mut iter = files .into_iter() .map(|root| -> DataTree { diff --git a/src/app/sub/hdd.rs b/src/app/sub/hdd.rs new file mode 100644 index 0000000..58f6a84 --- /dev/null +++ b/src/app/sub/hdd.rs @@ -0,0 +1,152 @@ +use super::mount_point::find_mount_point; +use std::{ + fs::canonicalize, + io, + path::{Path, PathBuf}, +}; +use sysinfo::{Disk, DiskKind}; + +/// Mockable APIs to interact with the system. +pub trait Api { + type Disk; + fn get_disk_kind(disk: &Self::Disk) -> DiskKind; + fn get_mount_point(disk: &Self::Disk) -> &Path; + fn canonicalize(path: &Path) -> io::Result; +} + +/// Implementation of [`Api`] that interacts with the real system. +pub struct RealApi; +impl Api for RealApi { + type Disk = Disk; + + fn get_disk_kind(disk: &Self::Disk) -> DiskKind { + disk.kind() + } + + fn get_mount_point(disk: &Self::Disk) -> &Path { + disk.mount_point() + } + + fn canonicalize(path: &Path) -> io::Result { + canonicalize(path) + } +} + +/// Check if any path is in any HDD. +pub fn any_path_is_in_hdd(paths: &[PathBuf], disks: &[Api::Disk]) -> bool { + paths + .iter() + .filter_map(|file| Api::canonicalize(file).ok()) + .any(|path| path_is_in_hdd::(&path, disks)) +} + +/// Check if path is in any HDD. +fn path_is_in_hdd(path: &Path, disks: &[Api::Disk]) -> bool { + let Some(mount_point) = find_mount_point(path, disks.iter().map(Api::get_mount_point)) else { + return false; + }; + disks + .iter() + .filter(|disk| Api::get_disk_kind(disk) == DiskKind::HDD) + .any(|disk| Api::get_mount_point(disk) == mount_point) +} + +#[cfg(test)] +mod tests { + use super::{any_path_is_in_hdd, path_is_in_hdd, Api}; + use pipe_trait::Pipe; + use pretty_assertions::assert_eq; + use std::path::{Path, PathBuf}; + use sysinfo::DiskKind; + + /// Fake disk for [`Api`]. + struct Disk { + kind: DiskKind, + mount_point: &'static str, + } + + impl Disk { + fn new(kind: DiskKind, mount_point: &'static str) -> Self { + Self { kind, mount_point } + } + } + + /// Mocked implementation of [`Api`] for testing purposes. + struct MockedApi; + impl Api for MockedApi { + type Disk = Disk; + + fn get_disk_kind(disk: &Self::Disk) -> DiskKind { + disk.kind + } + + fn get_mount_point(disk: &Self::Disk) -> &Path { + Path::new(disk.mount_point) + } + + fn canonicalize(path: &Path) -> std::io::Result { + path.to_path_buf().pipe(Ok) + } + } + + #[test] + fn test_any_path_in_hdd() { + let disks = &[ + Disk::new(DiskKind::SSD, "/"), + Disk::new(DiskKind::HDD, "/home"), + Disk::new(DiskKind::HDD, "/mnt/hdd-data"), + Disk::new(DiskKind::SSD, "/mnt/ssd-data"), + Disk::new(DiskKind::HDD, "/mnt/hdd-data/repo"), + ]; + + let cases: &[(&[&str], bool)] = &[ + (&[], false), + (&["/"], false), + (&["/home"], true), + (&["/mnt"], false), + (&["/mnt/ssd-data"], false), + (&["/mnt/hdd-data"], true), + (&["/mnt/hdd-data/repo"], true), + (&["/etc/fstab"], false), + (&["/home/usr/file"], true), + (&["/home/data/repo/test"], true), + (&["/usr/share"], false), + (&["/mnt/ssd-data/test"], false), + (&["/etc/fstab", "/home/user/file"], true), + (&["/mnt/hdd-data/file", "/mnt/hdd-data/repo/test"], true), + (&["/usr/share", "/mnt/ssd-data/test"], false), + ( + &["/etc/fstab", "/home/user", "/mnt/hdd-data", "/usr/share"], + true, + ), + ]; + + for (paths, in_hdd) in cases { + let paths: Vec<_> = paths.iter().map(PathBuf::from).collect(); + println!("CASE: {paths:?} → {in_hdd:?}"); + assert_eq!(any_path_is_in_hdd::(&paths, disks), *in_hdd); + } + } + + #[test] + fn test_path_in_hdd() { + let disks = &[ + Disk::new(DiskKind::SSD, "/"), + Disk::new(DiskKind::HDD, "/home"), + Disk::new(DiskKind::HDD, "/mnt/hdd-data"), + Disk::new(DiskKind::SSD, "/mnt/ssd-data"), + Disk::new(DiskKind::HDD, "/mnt/hdd-data/repo"), + ]; + + for (path, in_hdd) in [ + ("/etc/fstab", false), + ("/mnt/", false), + ("/mnt/hdd-data/repo/test", true), + ("/mnt/hdd-data/test/test", true), + ("/mnt/ssd-data/test/test", false), + ] { + println!("CASE: {path} → {in_hdd:?}"); + assert_eq!(path_is_in_hdd::(Path::new(path), disks), in_hdd); + } + } +} diff --git a/src/app/sub/mount_point.rs b/src/app/sub/mount_point.rs new file mode 100644 index 0000000..6691af4 --- /dev/null +++ b/src/app/sub/mount_point.rs @@ -0,0 +1,39 @@ +use std::{ffi::OsStr, path::Path}; + +/// Find a mount point that contains `absolute_path`. +pub fn find_mount_point<'a>( + absolute_path: &Path, + all_mount_points: impl IntoIterator, +) -> Option<&'a Path> { + all_mount_points + .into_iter() + .filter(|mnt| absolute_path.starts_with(mnt)) + .max_by_key(|mnt| AsRef::::as_ref(mnt).len()) // Mount points can be nested in each other +} + +#[cfg(test)] +mod tests { + use super::find_mount_point; + use pretty_assertions::assert_eq; + use std::path::Path; + + #[test] + fn test_mount_point() { + let all_mount_points = ["/", "/home", "/mnt/data", "/mnt/data/repo", "/mnt/repo"]; + + for (path, expected_mount_point) in &[ + ("/etc/fstab", "/"), + ("/home/user", "/home"), + ("/mnt/data/repo/test", "/mnt/data/repo"), + ("/mnt/data/test/test", "/mnt/data/"), + ("/mnt/repo/test/test", "/mnt/repo/"), + ] { + println!("CASE: {path} → {expected_mount_point}"); + let all_mount_points = all_mount_points.map(Path::new); + assert_eq!( + find_mount_point(Path::new(path), all_mount_points).unwrap(), + Path::new(expected_mount_point), + ); + } + } +}