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