Skip to main content

diskforge_core/
sizing.rs

1use std::path::Path;
2use std::time::SystemTime;
3
4use walkdir::WalkDir;
5
6/// Size and last-modified time computed in a single directory walk
7pub struct DirStats {
8    pub size: u64,
9    pub last_modified: Option<SystemTime>,
10}
11
12/// Walk a directory once and return both total size and last-modified time.
13pub fn dir_stats(path: &Path) -> DirStats {
14    if !path.exists() {
15        return DirStats {
16            size: 0,
17            last_modified: None,
18        };
19    }
20    let mut size = 0u64;
21    let mut last_modified: Option<SystemTime> = None;
22    for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
23        if entry.file_type().is_file()
24            && let Ok(meta) = entry.metadata()
25        {
26            size += meta.len();
27            if let Ok(mtime) = meta.modified() {
28                last_modified = Some(match last_modified {
29                    Some(cur) => cur.max(mtime),
30                    None => mtime,
31                });
32            }
33        }
34    }
35    DirStats {
36        size,
37        last_modified,
38    }
39}
40
41/// Calculate the total size of a directory (sum of all file sizes)
42pub fn dir_size(path: &Path) -> u64 {
43    dir_stats(path).size
44}
45
46/// Get the last modification time of any file in the directory
47pub fn dir_last_modified(path: &Path) -> Option<SystemTime> {
48    dir_stats(path).last_modified
49}
50
51/// Format bytes into human-readable string
52pub fn format_size(bytes: u64) -> String {
53    const KIB: u64 = 1024;
54    const MIB: u64 = 1024 * KIB;
55    const GIB: u64 = 1024 * MIB;
56
57    if bytes >= GIB {
58        format!("{:.1} GB", bytes as f64 / GIB as f64)
59    } else if bytes >= MIB {
60        format!("{:.0} MB", bytes as f64 / MIB as f64)
61    } else if bytes >= KIB {
62        format!("{:.0} KB", bytes as f64 / KIB as f64)
63    } else {
64        format!("{bytes} B")
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::fs;
72
73    #[test]
74    fn format_size_ranges() {
75        assert_eq!(format_size(0), "0 B");
76        assert_eq!(format_size(500), "500 B");
77        assert_eq!(format_size(1024), "1 KB");
78        assert_eq!(format_size(1536), "2 KB");
79        assert_eq!(format_size(1024 * 1024), "1 MB");
80        assert_eq!(format_size(1024 * 1024 * 1024), "1.0 GB");
81        assert_eq!(format_size(5 * 1024 * 1024 * 1024), "5.0 GB");
82    }
83
84    #[test]
85    fn dir_stats_with_files() {
86        let dir = std::env::temp_dir().join("diskforge_test_sizing");
87        let _ = fs::remove_dir_all(&dir);
88        fs::create_dir_all(&dir).unwrap();
89        fs::write(dir.join("a.txt"), "hello").unwrap();
90        fs::write(dir.join("b.txt"), "world!").unwrap();
91        let stats = dir_stats(&dir);
92        assert_eq!(stats.size, 11); // 5 + 6
93        assert!(stats.last_modified.is_some());
94        fs::remove_dir_all(&dir).ok();
95    }
96
97    #[test]
98    fn dir_stats_nested() {
99        let dir = std::env::temp_dir().join("diskforge_test_sizing_nested");
100        let _ = fs::remove_dir_all(&dir);
101        let sub = dir.join("sub");
102        fs::create_dir_all(&sub).unwrap();
103        fs::write(dir.join("root.txt"), "aaa").unwrap();
104        fs::write(sub.join("deep.txt"), "bbb").unwrap();
105        let stats = dir_stats(&dir);
106        assert_eq!(stats.size, 6); // 3 + 3
107        fs::remove_dir_all(&dir).ok();
108    }
109
110    #[test]
111    fn dir_stats_nonexistent() {
112        let stats = dir_stats(Path::new("/tmp/diskforge_nonexistent_xyz"));
113        assert_eq!(stats.size, 0);
114        assert!(stats.last_modified.is_none());
115    }
116
117    #[test]
118    fn dir_size_matches_stats() {
119        let dir = std::env::temp_dir().join("diskforge_test_size_match");
120        let _ = fs::remove_dir_all(&dir);
121        fs::create_dir_all(&dir).unwrap();
122        fs::write(dir.join("file.bin"), vec![0u8; 1024]).unwrap();
123        assert_eq!(dir_size(&dir), dir_stats(&dir).size);
124        assert_eq!(dir_size(&dir), 1024);
125        fs::remove_dir_all(&dir).ok();
126    }
127}