1use std::path::Path;
2use std::time::SystemTime;
3
4use walkdir::WalkDir;
5
6pub struct DirStats {
8 pub size: u64,
9 pub last_modified: Option<SystemTime>,
10}
11
12pub 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
40pub fn dir_size(path: &Path) -> u64 {
42 dir_stats(path).size
43}
44
45pub fn dir_last_modified(path: &Path) -> Option<SystemTime> {
47 dir_stats(path).last_modified
48}
49
50pub 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); 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); 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}