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 {
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
41pub fn dir_size(path: &Path) -> u64 {
43 dir_stats(path).size
44}
45
46pub fn dir_last_modified(path: &Path) -> Option<SystemTime> {
48 dir_stats(path).last_modified
49}
50
51pub 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); 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); 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}