lfs-core-0.11.2/.cargo_vcs_info.json0000644000000001360000000000100126270ustar { "git": { "sha1": "d29b0bcea650f46dfd59eb7501f8585cbeff7e0f" }, "path_in_vcs": "" }lfs-core-0.11.2/.gitignore000064400000000000000000000000231046102023000134020ustar 00000000000000/target Cargo.lock lfs-core-0.11.2/Cargo.toml0000644000000016270000000000100106330ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "lfs-core" version = "0.11.2" authors = ["dystroy "] description = "give information on mounted disks" readme = "README.md" keywords = [ "linux", "filesystem", "fs", ] categories = ["filesystem"] license = "MIT" repository = "https://github.com/Canop/lfs-core" [dependencies.lazy-regex] version = "3.0.2" [dependencies.libc] version = "0.2" [dependencies.snafu] version = "0.7" lfs-core-0.11.2/Cargo.toml.orig000064400000000000000000000005751046102023000143150ustar 00000000000000[package] name = "lfs-core" version = "0.11.2" authors = ["dystroy "] edition = "2021" keywords = ["linux", "filesystem", "fs"] license = "MIT" categories = ["filesystem"] description = "give information on mounted disks" repository = "https://github.com/Canop/lfs-core" readme = "README.md" [dependencies] lazy-regex = "3.0.2" libc = "0.2" snafu = "0.7" lfs-core-0.11.2/LICENSE000064400000000000000000000020571046102023000124300ustar 00000000000000MIT License Copyright (c) 2018 Denys Séguret Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lfs-core-0.11.2/README.md000064400000000000000000000011231046102023000126730ustar 00000000000000[![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] [s1]: https://img.shields.io/crates/v/lfs-core.svg [l1]: https://crates.io/crates/lfs-core [s2]: https://img.shields.io/badge/license-MIT-blue.svg [l2]: LICENSE [s3]: https://docs.rs/lfs-core/badge.svg [l3]: https://docs.rs/lfs-core/ [s4]: https://miaou.dystroy.org/static/shields/room.svg [l4]: https://miaou.dystroy.org/3 Give information on the mounted disks. **lfs-core** provides the data of [lfs](https://github.com/Canop/lfs) and of the `:fs` screen of [broot](https://dystroy.org/broot). lfs-core-0.11.2/src/block_device.rs000064400000000000000000000056741046102023000152010ustar 00000000000000use { snafu::prelude::*, crate::*, std::{ fs, path::{Path, PathBuf}, str::FromStr, }, }; /// the list of all found block devices #[derive(Debug, Clone)] pub struct BlockDeviceList { list: Vec, } /// a "block device", that is a device listed in /// the /sys/block tree with a device id #[derive(Debug, Clone)] pub struct BlockDevice { pub name: String, /// a name for a /dev/mapper/ device pub dm_name: Option, pub id: DeviceId, pub parent: Option, } impl BlockDeviceList { pub fn read() -> Result { let mut list = Vec::new(); let root = PathBuf::from("/sys/block"); append_child_block_devices(None, &root, &mut list, 0)?; Ok(Self { list }) } pub fn find_by_id(&self, id: DeviceId) -> Option<&BlockDevice> { self.list.iter().find(|bd| bd.id == id) } pub fn find_by_dm_name(&self, dm_name: &str) -> Option<&BlockDevice> { self.list .iter() .find(|bd| bd.dm_name.as_ref().map_or(false, |s| s == dm_name)) } pub fn find_by_name(&self, name: &str) -> Option<&BlockDevice> { self.list.iter().find(|bd| bd.name == name) } pub fn find_top( &self, id: DeviceId, dm_name: Option<&str>, name: Option<&str>, ) -> Option<&BlockDevice> { self.find_by_id(id) .or_else(|| dm_name.and_then(|dm_name| self.find_by_dm_name(dm_name))) .or_else(|| name.and_then(|name| self.find_by_name(name))) .and_then(|bd| match bd.parent { Some(parent_id) => self.find_top(parent_id, None, None), None => Some(bd), }) } } fn append_child_block_devices( parent: Option, parent_path: &Path, list: &mut Vec, depth: usize, ) -> Result<(), Error> { let children = fs::read_dir(parent_path) .with_context(|_| CantReadDirSnafu { path: parent_path.to_path_buf() })?; for e in children.flatten() { let device_id = fs::read_to_string(e.path().join("dev")) .ok() .and_then(|s| DeviceId::from_str(s.trim()).ok()); if let Some(id) = device_id { if list.iter().any(|bd| bd.id == id) { // already present, probably because of a cycling link continue; } let name = e.file_name().to_string_lossy().to_string(); let dm_name = sys::read_file(&format!("/sys/block/{}/dm/name", name)) .ok() .map(|s| s.trim().to_string()); list.push(BlockDevice { name, dm_name, id, parent, }); if depth > 15 { // there's probably a link cycle continue; } append_child_block_devices(Some(id), &e.path(), list, depth + 1)?; } } Ok(()) } lfs-core-0.11.2/src/device_id.rs000064400000000000000000000032211046102023000144650ustar 00000000000000use { snafu::prelude::*, std::str::FromStr, }; /// Id of a device, as can be found in MetadataExt.dev(). #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct DeviceId { pub major: u32, pub minor: u32, } #[derive(Debug, Snafu)] #[snafu(display("Could not parse {string} as a device id"))] pub struct ParseDeviceIdError { string: String, } impl FromStr for DeviceId { type Err = ParseDeviceIdError; /// this code is based on `man 5 proc` and my stochastic interpretation fn from_str(string: &str) -> Result { (|| { let mut parts = string.split(':').fuse(); match (parts.next(), parts.next(), parts.next()) { (Some(major), Some(minor), None) => { let major = major.parse().ok()?; let minor = minor.parse().ok()?; Some(Self { major, minor }) } (Some(int), None, None) => { let int: u64 = int.parse().ok()?; Some( int.into() ) } _ => None, } })().with_context(|| ParseDeviceIdSnafu { string }) } } impl From for DeviceId { fn from(num: u64) -> Self { Self { major: (num >> 8) as u32, minor: (num & 0xFF) as u32, } } } impl DeviceId { pub fn new(major: u32, minor: u32) -> Self { Self { major, minor } } } #[test] fn test_from_str() { assert_eq!(DeviceId::new(8, 16), DeviceId::from_str("8:16").unwrap()); } #[test] fn test_from_u64() { assert_eq!(DeviceId::new(8, 16), DeviceId::from(2064u64)); } lfs-core-0.11.2/src/disk.rs000064400000000000000000000040161046102023000135070ustar 00000000000000use { super::*, lazy_regex::*, }; /// what we have most looking like a physical device #[derive(Debug, Clone)] pub struct Disk { /// a name, like "sda", "sdc", "nvme0n1", etc. pub name: String, /// true for HDD, false for SSD, None for unknown. /// This information isn't reliable for USB devices pub rotational: Option, /// whether the system thinks the media is removable. /// Seems reliable when not mapped pub removable: Option, /// whether it's a RAM disk pub ram: bool, /// whether it's on LVM pub lvm: bool, /// whether it's a crypted disk pub crypted: bool, } impl Disk { pub fn new(name: String) -> Self { let rotational = sys::read_file_as_bool(&format!("/sys/block/{}/queue/rotational", name)); let removable = sys::read_file_as_bool(&format!("/sys/block/{}/removable", name)); let ram = regex_is_match!(r#"^zram\d*$"#, &name); let dm_uuid = sys::read_file(&format!("/sys/block/{}/dm/uuid", name)).ok(); let crypted = dm_uuid .as_ref() .map_or(false, |uuid| uuid.starts_with("CRYPT-")); let lvm = dm_uuid.map_or(false, |uuid| uuid.starts_with("LVM-")); Self { name, rotational, removable, ram, lvm, crypted, } } /// a synthetic code trying to express the essence of the type of media, /// an empty str being returned when information couldn't be gathered. /// This code is for humans and may change in future minor versions. pub fn disk_type(&self) -> &'static str { if self.ram { "RAM" } else if self.crypted { "crypt" } else if self.lvm { "LVM" } else { match (self.removable, self.rotational) { (Some(true), _) => "remov", (Some(false), Some(true)) => "HDD", (Some(false), Some(false)) => "SSD", _ => "", } } } } lfs-core-0.11.2/src/error.rs000064400000000000000000000011111046102023000136770ustar 00000000000000/// lfs error type #[derive(Debug, snafu::Snafu)] #[snafu(visibility(pub(crate)))] pub enum Error { #[snafu(display("Could not read file {path:?}"))] CantReadFile { source: std::io::Error, path: std::path::PathBuf, }, #[snafu(display("Could not read dir {path:?}"))] CantReadDir { source: std::io::Error, path: std::path::PathBuf, }, #[snafu(display("Could not parse mountinfo"))] ParseMountInfo { source: crate::ParseMountInfoError, }, #[snafu(display("Unexpected format"))] UnexpectedFormat, } lfs-core-0.11.2/src/inodes.rs000064400000000000000000000016541046102023000140430ustar 00000000000000 /// inode information /// /// This structure isn't built if data aren't consistent #[derive(Debug, Clone)] pub struct Inodes { /// number of inodes, always > 0 pub files: u64, /// number of free inodes pub ffree: u64, /// number of free inodes for underpriviledged users pub favail: u64, } impl Inodes { /// Create the structure if the given values are consistent, /// return None if they aren't. pub fn new(files: u64, ffree: u64, favail: u64) -> Option { if files > 0 && ffree <= files && favail <= files { Some(Self { files, ffree, favail }) } else { None } } /// number of non available inodes, always > 0 pub fn used(&self) -> u64 { self.files - self.favail } /// share of non available inodes, always in [0, 1], never NaN pub fn use_share(&self) -> f64 { self.used() as f64 / self.files as f64 } } lfs-core-0.11.2/src/label.rs000064400000000000000000000023561046102023000136410ustar 00000000000000use { super::*, snafu::prelude::*, std::fs, }; /// the labelling of a file-system, that /// is the pair (label, fs) #[derive(Debug, Clone)] pub struct Labelling { pub label: String, pub fs_name: String, } /// try to find all file-system labels /// /// An error can't be excluded as not all systems expose /// this information the way lfs-core reads it. pub fn read_labels() -> Result, Error> { let path = "/dev/disk/by-label"; let entries = fs::read_dir(path).context(CantReadDirSnafu { path })?; let labels = entries .filter_map(|entry| entry.ok()) .filter_map(|entry| { let md = entry.metadata().ok()?; let file_type = md.file_type(); if !file_type.is_symlink() { return None; } let label = sys::decode_string(entry.file_name().to_string_lossy()); let linked_path = fs::read_link(entry.path()) .map(|path| path.to_string_lossy().to_string()) .ok()?; let fs_name = format!( "/dev/{}", linked_path.strip_prefix("../../")?, ); Some(Labelling { label, fs_name }) }) .collect(); Ok(labels) } lfs-core-0.11.2/src/lib.rs000064400000000000000000000014231046102023000133220ustar 00000000000000/*! Use `lfs_core::read_mounts` to get information on all mounted volumes on a unix system. ``` // get all mount points let options = lfs_core::ReadOptions::default(); let mut mounts = lfs_core::read_mounts(&options).unwrap(); // only keep the one with size stats mounts.retain(|m| m.stats.is_ok()); // print them for mount in mounts { dbg!(mount); } ``` The [lfs](https://github.com/Canop/lfs) application is a viewer for lfs-core and shows you the information you're expected to find in mounts. */ mod block_device; mod device_id; mod disk; mod error; mod inodes; mod label; mod mount; mod mountinfo; mod stats; mod sys; pub use { block_device::*, device_id::*, disk::*, error::*, inodes::*, label::*, mount::*, mountinfo::*, stats::*, }; lfs-core-0.11.2/src/mount.rs000064400000000000000000000052241046102023000137210ustar 00000000000000use super::*; /// A mount point #[derive(Debug, Clone)] pub struct Mount { pub info: MountInfo, pub fs_label: Option, pub disk: Option, pub stats: Result, } impl Mount { /// Return inodes information, when available and consistent pub fn inodes(&self) -> Option<&Inodes> { self.stats .as_ref() .ok() .and_then(|stats| stats.inodes.as_ref()) } /// Return the stats, if they could be fetched and /// make sense. /// /// Most often, you don't care *why* there are no stats, /// because the error cases are mostly non storage volumes, /// so it's a best practice to no try to analyze the error /// but just use this option returning method. /// /// The most interesting case is when a network volume is /// unreachable, which you can test with is_unreachable(). pub fn stats(&self) -> Option<&Stats> { self.stats.as_ref().ok() } /// Tell whether the reason we have no stats is because the /// filesystem is unreachable pub fn is_unreachable(&self) -> bool { matches!(self.stats, Err(StatsError::Unreachable)) } } #[derive(Debug, Clone)] pub struct ReadOptions { remote_stats: bool, } impl Default for ReadOptions { fn default() -> Self { Self { remote_stats: true, } } } impl ReadOptions { pub fn remote_stats(&mut self, v: bool) { self.remote_stats = v; } } /// Read all the mount points and load basic information on them pub fn read_mounts(options: &ReadOptions) -> Result, Error> { let labels = read_labels().ok(); // we'll find the disk for a filesystem by taking the longest // disk whose name starts the one of our partition // hence the sorting. let bd_list = BlockDeviceList::read()?; read_mountinfo()? .drain(..) .map(|info| { let top_bd = bd_list.find_top( info.dev, info.dm_name(), info.fs_name(), ); let fs_label = labels.as_ref() .and_then(|labels| { labels .iter() .find(|label| label.fs_name == info.fs) .map(|label| label.label.clone()) }); let disk = top_bd.map(|bd| Disk::new(bd.name.clone())); let stats = if !options.remote_stats && info.is_remote() { Err(StatsError::Excluded) } else { Stats::from(&info.mount_point) }; Ok(Mount { info, fs_label, disk, stats }) }) .collect() } lfs-core-0.11.2/src/mountinfo.rs000064400000000000000000000102441046102023000145730ustar 00000000000000use { crate::*, lazy_regex::*, snafu::prelude::*, std::{ path::PathBuf, str::FromStr, }, }; static REMOTE_ONLY_FS_TYPES: &[&str] = &["afs", "coda", "auristorfs", "fhgfs", "gpfs", "ibrix", "ocfs2", "vxfs"]; /// An id of a mount pub type MountId = u32; /// A mount point as described in /proc/self/mountinfo #[derive(Debug, Clone)] pub struct MountInfo { pub id: MountId, pub parent: MountId, pub dev: DeviceId, pub root: PathBuf, pub mount_point: PathBuf, pub fs: String, pub fs_type: String, /// whether it's a bound mount (usually mirroring part of another device) pub bound: bool, } impl MountInfo { /// return `` when the path is `/dev/mapper/` pub fn dm_name(&self) -> Option<&str> { regex_captures!(r#"^/dev/mapper/([^/]+)$"#, &self.fs) .map(|(_, dm_name)| dm_name) } /// return the last token of the fs path pub fn fs_name(&self) -> Option<&str> { regex_find!(r#"[^\\/]+$"#, &self.fs) } /// tell whether the mount looks remote /// /// Heuristics copied from https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c pub fn is_remote(&self) -> bool { self.fs.contains(':') || ( self.fs.starts_with("//") && ["cifs", "smb3", "smbfs"].contains(&self.fs_type.as_ref()) ) || REMOTE_ONLY_FS_TYPES.contains(&self.fs_type.as_ref()) || self.fs == "-hosts" } } #[derive(Debug, Snafu)] #[snafu(display("Could not parse {line} as mount info"))] pub struct ParseMountInfoError { line: String, } impl FromStr for MountInfo { type Err = ParseMountInfoError; fn from_str(line: &str) -> Result { (|| { // this parsing is based on `man 5 proc` let mut tokens = line.split_whitespace(); let id = tokens.next()?.parse().ok()?; let parent = tokens.next()?.parse().ok()?; let dev = tokens.next()?.parse().ok()?; let root = str_to_pathbuf(tokens.next()?); let mount_point = str_to_pathbuf(tokens.next()?); loop { let token = tokens.next()?; if token == "-" { break; } }; let fs_type = tokens.next()?.to_string(); let fs = tokens.next()?.to_string(); Some(Self { id, parent, dev, root, mount_point, fs, fs_type, bound: false, // determined by post-treatment }) })().with_context(|| ParseMountInfoSnafu { line }) } } /// convert a string to a pathbuf, converting ascii-octal encoded /// chars. /// This is necessary because some chars are encoded. For example /// the `/media/dys/USB DISK` is present as `/media/dys/USB\040DISK` fn str_to_pathbuf(s: &str) -> PathBuf { PathBuf::from(sys::decode_string(s)) } /// read all the mount points pub fn read_mountinfo() -> Result, Error> { let mut mounts: Vec = Vec::new(); let path = "/proc/self/mountinfo"; let file_content = sys::read_file(path) .context(CantReadDirSnafu { path })?; for line in file_content.trim().split('\n') { let mut mount: MountInfo = line.parse() .map_err(|source| Error::ParseMountInfo { source })?; mount.bound = mounts.iter().any(|m| m.dev == mount.dev); mounts.push(mount); } Ok(mounts) } #[test] fn test_from_str() { let mi = MountInfo::from_str( "47 21 0:41 / /dev/hugepages rw,relatime shared:27 - hugetlbfs hugetlbfs rw,pagesize=2M" ).unwrap(); assert_eq!(mi.id, 47); assert_eq!(mi.dev, DeviceId::new(0, 41)); assert_eq!(mi.root, PathBuf::from("/")); assert_eq!(mi.mount_point, PathBuf::from("/dev/hugepages")); let mi = MountInfo::from_str( "106 26 8:17 / /home/dys/dev rw,relatime shared:57 - xfs /dev/sdb1 rw,attr2,inode64,noquota" ).unwrap(); assert_eq!(mi.id, 106); assert_eq!(mi.dev, DeviceId::new(8, 17)); assert_eq!(&mi.fs, "/dev/sdb1"); assert_eq!(&mi.fs_type, "xfs"); } lfs-core-0.11.2/src/stats.rs000064400000000000000000000056701046102023000137220ustar 00000000000000use { crate::Inodes, std::{ ffi::CString, mem, os::unix::ffi::OsStrExt, path::Path, }, }; /// inode & blocs information given by statvfs #[derive(Debug, Clone)] pub struct Stats { /// block size pub bsize: u64, /// number of blocks pub blocks: u64, /// number of free blocks pub bfree: u64, /// number of free blocks for underprivileged users pub bavail: u64, /// information relative to inodes, if available pub inodes: Option, } #[derive(Debug, snafu::Snafu, Clone, Copy, PartialEq, Eq)] #[snafu(visibility(pub(crate)))] pub enum StatsError { #[snafu(display("Could not stat mount point"))] Unreachable, #[snafu(display("Unconsistent stats"))] Unconsistent, /// Options made us not even try #[snafu(display("Excluded"))] Excluded, } impl Stats { pub fn from(mount_point: &Path) -> Result { let c_mount_point = CString::new(mount_point.as_os_str().as_bytes()).unwrap(); unsafe { let mut statvfs = mem::MaybeUninit::::uninit(); let code = libc::statvfs(c_mount_point.as_ptr(), statvfs.as_mut_ptr()); match code { 0 => { let statvfs = statvfs.assume_init(); // blocks info let bsize = statvfs.f_bsize as u64; let blocks = statvfs.f_blocks as u64; let bfree = statvfs.f_bfree as u64; let bavail = statvfs.f_bavail as u64; if bsize == 0 || blocks == 0 || bfree > blocks || bavail > blocks { // unconsistent or void data return Err(StatsError::Unconsistent); } // inodes info, will be checked in Inodes::new let files = statvfs.f_files as u64; let ffree = statvfs.f_ffree as u64; let favail = statvfs.f_favail as u64; let inodes = Inodes::new(files, ffree, favail); Ok(Stats { bsize, blocks, bfree, bavail, inodes, }) } _ => { // the filesystem wasn't found, it's a strange one, for example a // docker one, or a disconnected remote one Err(StatsError::Unreachable) } } } } pub fn size(&self) -> u64 { self.bsize * self.blocks } pub fn available(&self) -> u64 { self.bsize * self.bavail } pub fn used(&self) -> u64 { self.size() - self.available() } pub fn use_share(&self) -> f64 { if self.size() == 0 { 0.0 } else { self.used() as f64 / (self.size() as f64) } } } lfs-core-0.11.2/src/sys.rs000064400000000000000000000021731046102023000133750ustar 00000000000000use { lazy_regex::*, std::{ fs::File, io::{self, Read}, path::Path, }, }; /// read a system file into a string pub fn read_file>(path: P) -> io::Result { let mut file = File::open(path.as_ref())?; let mut buf = String::new(); file.read_to_string(&mut buf)?; Ok(buf) } /// read a system file into a boolean (assuming "0" or "1") pub fn read_file_as_bool>(path: P) -> Option { read_file(path).ok().and_then(|c| match c.trim() { "0" => Some(false), "1" => Some(true), _ => None, }) } /// decode ascii-octal or ascii-hexa encoded strings pub fn decode_string>(s: S) -> String { // replacing octal escape sequences let s = regex_replace_all!(r#"\\0(\d\d)"#, s.as_ref(), |_, n: &str| { let c = u8::from_str_radix(n, 8).unwrap() as char; c.to_string() }); // replacing hexa escape sequences let s = regex_replace_all!(r#"\\x([0-9a-fA-F]{2})"#, &s, |_, n: &str| { let c = u8::from_str_radix(n, 16).unwrap() as char; c.to_string() }); s.to_string() }