fix: deduplicate disks in capacity calculation to prevent inflation (#1656)

This commit is contained in:
houseme
2026-01-30 00:03:21 +08:00
committed by GitHub
parent 022e3dfc21
commit 2ee81496b0
17 changed files with 631 additions and 275 deletions
+42 -43
View File
@@ -12,29 +12,40 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use nix::sys::stat::{self, stat};
use nix::sys::statfs::{self, FsType, statfs};
use super::{DiskInfo, IOStats};
use rustix::fs::statfs;
use std::fs::File;
use std::io::{self, BufRead, Error, ErrorKind};
use std::path::Path;
use super::{DiskInfo, IOStats};
/// Returns total and free bytes available in a directory, e.g. `/`.
pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
let path_display = p.as_ref().display();
let stat_fs = statfs(p.as_ref())?;
// Use statfs on Linux to get access to f_type (filesystem magic number)
let stat = statfs(p.as_ref())?;
let bsize = stat_fs.block_size() as u64;
let bfree = stat_fs.blocks_free() as u64;
let bavail = stat_fs.blocks_available() as u64;
let blocks = stat_fs.blocks() as u64;
// Linux statfs:
// f_bsize: Optimal transfer block size
// f_blocks: Total data blocks in file system
// f_frsize: Fragment size (since Linux 2.6) - unit for blocks
//
// If f_frsize is > 0, it is the unit for f_blocks, f_bfree, f_bavail.
// Otherwise f_bsize is used.
let bsize = if stat.f_frsize > 0 {
stat.f_frsize as u64
} else {
stat.f_bsize as u64
};
let bfree = stat.f_bfree as u64;
let bavail = stat.f_bavail as u64;
let blocks = stat.f_blocks as u64;
let reserved = match bfree.checked_sub(bavail) {
Some(reserved) => reserved,
None => {
return Err(Error::other(format!(
"detected f_bavail space ({bavail}) > f_bfree space ({bfree}), fs corruption at ({path_display}). please run 'fsck'"
"detected f_bavail space ({bavail}) > f_bfree space ({bfree}), fs corruption at ({path_display}). please run 'fsck'",
)));
}
};
@@ -43,7 +54,7 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
Some(total) => total * bsize,
None => {
return Err(Error::other(format!(
"detected reserved space ({reserved}) > blocks space ({blocks}), fs corruption at ({path_display}). please run 'fsck'"
"detected reserved space ({reserved}) > blocks space ({blocks}), fs corruption at ({path_display}). please run 'fsck'",
)));
}
};
@@ -58,17 +69,17 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
}
};
let st = stat(p.as_ref())?;
let st = rustix::fs::stat(p.as_ref())?;
Ok(DiskInfo {
total,
free,
used,
files: stat_fs.files(),
ffree: stat_fs.files_free(),
fstype: get_fs_type(stat_fs.filesystem_type()).to_string(),
major: stat::major(st.st_dev),
minor: stat::minor(st.st_dev),
files: stat.f_files as u64,
ffree: stat.f_ffree as u64,
fstype: get_fs_type(stat.f_type as u64).to_string(),
major: rustix::fs::major(st.st_dev) as u64,
minor: rustix::fs::minor(st.st_dev) as u64,
..Default::default()
})
}
@@ -85,23 +96,26 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
/// "2fc12fc1" => "zfs",
/// "ff534d42" => "cifs",
/// "53464846" => "wslfs",
fn get_fs_type(fs_type: FsType) -> &'static str {
fn get_fs_type(fs_type: u64) -> &'static str {
// Magic numbers for various filesystems
match fs_type {
statfs::TMPFS_MAGIC => "TMPFS",
statfs::MSDOS_SUPER_MAGIC => "MSDOS",
// statfs::XFS_SUPER_MAGIC => "XFS",
statfs::NFS_SUPER_MAGIC => "NFS",
statfs::EXT4_SUPER_MAGIC => "EXT4",
statfs::ECRYPTFS_SUPER_MAGIC => "ecryptfs",
statfs::OVERLAYFS_SUPER_MAGIC => "overlayfs",
statfs::REISERFS_SUPER_MAGIC => "REISERFS",
0x01021994 => "TMPFS",
0x4d44 => "MSDOS",
0x6969 => "NFS",
0xEF53 => "EXT4",
0xf15f => "ecryptfs",
0x794c7630 => "overlayfs",
0x52654973 => "REISERFS",
// Additional common ones can be added here:
// 0x58465342 => "XFS",
// 0x9123683E => "BTRFS",
_ => "UNKNOWN",
}
}
pub fn same_disk(disk1: &str, disk2: &str) -> std::io::Result<bool> {
let stat1 = stat(disk1)?;
let stat2 = stat(disk2)?;
let stat1 = rustix::fs::stat(disk1)?;
let stat2 = rustix::fs::stat(disk2)?;
Ok(stat1.st_dev == stat2.st_dev)
}
@@ -166,18 +180,3 @@ fn read_stat(file_name: &str) -> std::io::Result<Vec<u64>> {
Ok(stats)
}
#[cfg(test)]
mod test {
use super::get_drive_stats;
use tracing::debug;
#[ignore] // FIXME: failed in github actions
#[test]
fn test_stats() {
let major = 7;
let minor = 11;
let s = get_drive_stats(major, minor).unwrap();
debug!("Drive stats for major: {}, minor: {} - {:?}", major, minor, s);
}
}
+13 -5
View File
@@ -22,10 +22,10 @@ mod windows;
#[cfg(target_os = "linux")]
pub use linux::{get_drive_stats, get_info, same_disk};
// pub use linux::same_disk;
#[cfg(all(unix, not(target_os = "linux")))]
pub use unix::{get_drive_stats, get_info, same_disk};
#[cfg(target_os = "windows")]
pub use windows::{get_drive_stats, get_info, same_disk};
@@ -79,14 +79,19 @@ mod tests {
assert!(info.total > 0);
assert!(info.free > 0);
assert!(info.used > 0);
assert!(info.files > 0);
assert!(info.ffree > 0);
// Files count might be 0 on some systems/filesystems or if empty
// assert!(info.files > 0);
// assert!(info.ffree > 0);
assert!(info.total >= info.free);
}
#[test]
fn test_get_info_invalid_path() {
#[cfg(unix)]
let invalid_path = PathBuf::from("/invalid/path");
#[cfg(windows)]
let invalid_path = PathBuf::from("Z:\\invalid\\path");
let result = get_info(&invalid_path);
assert!(result.is_err());
@@ -118,7 +123,10 @@ mod tests {
#[ignore] // FIXME: failed in github actions
#[test]
fn test_get_drive_stats_default() {
let stats = get_drive_stats(0, 0).unwrap();
assert_eq!(stats, IOStats::default());
#[cfg(not(target_os = "linux"))]
{
let stats = get_drive_stats(0, 0).unwrap();
assert_eq!(stats, IOStats::default());
}
}
}
+31 -10
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use super::{DiskInfo, IOStats};
use nix::sys::{stat::stat, statvfs::statvfs};
use rustix::fs::{StatVfs, statvfs};
use std::io::Error;
use std::path::Path;
@@ -22,10 +22,22 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
let path_display = p.as_ref().display();
let stat = statvfs(p.as_ref())?;
let bsize = stat.block_size();
let bfree = stat.blocks_free() as u64;
let bavail = stat.blocks_available() as u64;
let blocks = stat.blocks() as u64;
// According to POSIX statvfs definition:
// f_bsize: File system block size.
// f_frsize: Fundamental file system block size.
// f_blocks: Total number of blocks on file system in units of f_frsize.
//
// We should use f_frsize to calculate the size in bytes.
// If f_frsize is 0 (which shouldn't happen on compliant systems), fallback to f_bsize.
let bsize = if stat.f_frsize > 0 {
stat.f_frsize as u64
} else {
stat.f_bsize as u64
};
let bfree = stat.f_bfree as u64;
let bavail = stat.f_bavail as u64;
let blocks = stat.f_blocks as u64;
let reserved = match bfree.checked_sub(bavail) {
Some(reserved) => reserved,
@@ -55,24 +67,33 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
}
};
let st = rustix::fs::stat(p.as_ref())?;
Ok(DiskInfo {
total,
free,
used,
files: stat.files() as u64,
ffree: stat.files_free() as u64,
// Statvfs does not provide a way to return the filesystem as name.
files: stat.f_files,
ffree: stat.f_ffree,
fstype: get_fs_type(&stat).to_string(),
major: rustix::fs::major(st.st_dev) as u64,
minor: rustix::fs::minor(st.st_dev) as u64,
..Default::default()
})
}
fn get_fs_type(_stat: &StatVfs) -> &'static str {
"UNKNOWN"
}
pub fn same_disk(disk1: &str, disk2: &str) -> std::io::Result<bool> {
let stat1 = stat(disk1)?;
let stat2 = stat(disk2)?;
let stat1 = rustix::fs::stat(disk1)?;
let stat2 = rustix::fs::stat(disk2)?;
Ok(stat1.st_dev == stat2.st_dev)
}
#[cfg(not(target_os = "linux"))]
pub fn get_drive_stats(_major: u32, _minor: u32) -> std::io::Result<IOStats> {
Ok(IOStats::default())
}
+23 -127
View File
@@ -12,9 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(unsafe_code)] // TODO: audit unsafe code
use crate::os::{DiskInfo, IOStats};
use super::{DiskInfo, IOStats};
use std::io::Error;
use std::path::Path;
use windows::Win32::Foundation::MAX_PATH;
@@ -75,51 +73,12 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
used: total - free,
files: total_number_of_clusters as u64,
ffree: number_of_free_clusters as u64,
fstype: get_fs_type(&path_wide).unwrap_or_default(),
fstype: get_windows_fs_type(&path_wide).unwrap_or_default(),
..Default::default()
})
}
/// Returns leading volume name.
///
/// # Arguments
/// * `v` - A slice of u16 representing the path in UTF-16 encoding
///
/// # Returns
/// * `Ok(Vec<u16>)` containing the volume name in UTF-16 encoding.
/// * `Err` if an error occurs during the operation.
#[allow(dead_code)]
fn get_volume_name(v: &[u16]) -> std::io::Result<Vec<u16>> {
let mut volume_name_buffer = [0u16; MAX_PATH as usize];
unsafe {
GetVolumePathNameW(windows::core::PCWSTR::from_raw(v.as_ptr()), &mut volume_name_buffer)
.map_err(|e| Error::from_raw_os_error(e.code().0 as i32))?;
}
let len = volume_name_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(volume_name_buffer.len());
Ok(volume_name_buffer[..len].to_vec())
}
#[allow(dead_code)]
fn utf16_to_string(v: &[u16]) -> String {
let len = v.iter().position(|&x| x == 0).unwrap_or(v.len());
String::from_utf16_lossy(&v[..len])
}
/// Returns the filesystem type of the underlying mounted filesystem
///
/// # Arguments
/// * `p` - A slice of u16 representing the path in UTF-16 encoding
///
/// # Returns
/// * `Ok(String)` containing the filesystem type (e.g., "NTFS", "FAT32").
/// * `Err` if an error occurs during the operation.
#[allow(dead_code)]
fn get_fs_type(p: &[u16]) -> std::io::Result<String> {
fn get_windows_fs_type(p: &[u16]) -> std::io::Result<String> {
let path = get_volume_name(p)?;
let mut volume_serial_number = 0u32;
@@ -143,16 +102,26 @@ fn get_fs_type(p: &[u16]) -> std::io::Result<String> {
Ok(utf16_to_string(&file_system_name_buffer))
}
/// Determines if two paths are on the same disk.
///
/// # Arguments
/// * `disk1` - The first disk path as a string slice.
/// * `disk2` - The second disk path as a string slice.
///
/// # Returns
/// * `Ok(true)` if both paths are on the same disk.
/// * `Ok(false)` if both paths are on different disks.
/// * `Err` if an error occurs during the operation.
fn get_volume_name(v: &[u16]) -> std::io::Result<Vec<u16>> {
let mut volume_name_buffer = [0u16; MAX_PATH as usize];
unsafe {
GetVolumePathNameW(windows::core::PCWSTR::from_raw(v.as_ptr()), &mut volume_name_buffer)
.map_err(|e| Error::from_raw_os_error(e.code().0 as i32))?;
}
let len = volume_name_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(volume_name_buffer.len());
Ok(volume_name_buffer[..len].to_vec())
}
fn utf16_to_string(v: &[u16]) -> String {
let len = v.iter().position(|&x| x == 0).unwrap_or(v.len());
String::from_utf16_lossy(&v[..len])
}
pub fn same_disk(disk1: &str, disk2: &str) -> std::io::Result<bool> {
let path1_wide: Vec<u16> = disk1.encode_utf16().chain(std::iter::once(0)).collect();
let path2_wide: Vec<u16> = disk2.encode_utf16().chain(std::iter::once(0)).collect();
@@ -163,79 +132,6 @@ pub fn same_disk(disk1: &str, disk2: &str) -> std::io::Result<bool> {
Ok(volume1 == volume2)
}
/// Retrieves I/O statistics for a drive identified by its major and minor numbers.
///
/// # Arguments
/// * `major` - The major number of the drive.
/// * `minor` - The minor number of the drive.
///
/// # Returns
/// * `Ok(IOStats)` containing the I/O statistics.
/// * `Err` if an error occurs during the operation.
pub fn get_drive_stats(_major: u32, _minor: u32) -> std::io::Result<IOStats> {
// Windows does not provide direct IO stats via simple API; this is a stub
// For full implementation, consider using PDH or WMI, but that adds complexity
Ok(IOStats::default())
}
#[cfg(test)]
mod tests {
#[cfg(target_os = "windows")]
#[test]
fn test_get_info_valid_path() {
let temp_dir = tempfile::tempdir().unwrap();
let info = get_info(temp_dir.path()).unwrap();
// Verify disk info is valid
assert!(info.total > 0);
assert!(info.free > 0);
assert!(info.used > 0);
assert!(info.files > 0);
assert!(info.ffree > 0);
assert!(!info.fstype.is_empty());
}
#[cfg(target_os = "windows")]
#[test]
fn test_get_info_invalid_path() {
use std::path::PathBuf;
let invalid_path = PathBuf::from("Z:\\invalid\\path");
let result = get_info(&invalid_path);
assert!(result.is_err());
}
#[cfg(target_os = "windows")]
#[test]
fn test_same_disk_same_path() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().to_str().unwrap();
let result = same_disk(path, path).unwrap();
assert!(result);
}
#[cfg(target_os = "windows")]
#[test]
fn test_same_disk_different_paths() {
let temp_dir1 = tempfile::tempdir().unwrap();
let temp_dir2 = tempfile::tempdir().unwrap();
let path1 = temp_dir1.path().to_str().unwrap();
let path2 = temp_dir2.path().to_str().unwrap();
let _result = same_disk(path1, path2).unwrap();
// Since both temporary directories are created in the same file system,
// they should be on the same disk in most cases
// Test passes if the function doesn't panic - the actual result depends on test environment
}
#[cfg(target_os = "windows")]
#[test]
fn get_info_with_root_drive() {
let info = get_info("C:\\").unwrap();
assert!(info.total > 0);
assert!(info.free > 0);
assert!(info.used > 0);
assert!(info.files > 0);
assert!(info.ffree > 0);
assert!(!info.fstype.is_empty());
}
}