1use std::cmp::Reverse;
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7use std::time::SystemTime;
8
9use anyhow::Result;
10use walkdir::WalkDir;
11
12use crate::prune;
13
14pub struct DirStat {
16 pub name: String,
17 pub bytes: u64,
18 pub files: u64,
19}
20
21pub struct CacheSummary {
22 pub root: PathBuf,
23 pub total_bytes: u64,
24 pub total_files: u64,
25 pub dirs: Vec<DirStat>,
27 pub oldest: Option<SystemTime>,
29 pub top_units: Vec<(String, u64)>,
31}
32
33pub fn summarize(root: &Path) -> Result<CacheSummary> {
40 let mut summary = CacheSummary {
41 root: root.to_path_buf(),
42 total_bytes: 0,
43 total_files: 0,
44 dirs: Vec::new(),
45 oldest: None,
46 top_units: Vec::new(),
47 };
48
49 let mut buckets: BTreeMap<String, (u64, u64)> = BTreeMap::new();
51 for entry in WalkDir::new(root).min_depth(1) {
52 let entry = match entry {
53 Ok(entry) => entry,
54 Err(_) => continue, };
56 if !entry.file_type().is_file() {
57 continue;
58 }
59 let Ok(meta) = entry.metadata() else { continue };
60
61 summary.total_bytes += meta.len();
62 summary.total_files += 1;
63 if let Ok(mtime) = meta.modified() {
64 if summary.oldest.map_or(true, |oldest| mtime < oldest) {
65 summary.oldest = Some(mtime);
66 }
67 }
68
69 let rel: Vec<String> = entry
73 .path()
74 .strip_prefix(root)
75 .ok()
76 .map(|rel| {
77 rel.components()
78 .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
79 .collect()
80 })
81 .unwrap_or_default();
82 let bucket = if rel.len() > 1 {
83 rel[0].clone()
84 } else {
85 "(cache root)".to_string()
86 };
87 let bucket = buckets.entry(bucket).or_insert((0, 0));
88 bucket.0 += meta.len();
89 bucket.1 += 1;
90 }
91
92 summary.dirs = buckets
93 .into_iter()
94 .map(|(name, (bytes, files))| DirStat { name, bytes, files })
95 .collect();
96 summary.dirs.sort_by_key(|dir| Reverse(dir.bytes));
97
98 summary.top_units = prune::scan(root)?
99 .into_values()
100 .map(|group| (group.key, group.bytes))
101 .collect();
102 summary.top_units.sort_by_key(|(_, bytes)| Reverse(*bytes));
103 summary.top_units.truncate(10);
104
105 Ok(summary)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use std::fs;
112
113 #[test]
114 fn summarizes_sizes_and_buckets() -> Result<()> {
115 let root = tempfile::tempdir()?;
116 let debug = root.path().join("debug");
117 fs::create_dir_all(debug.join("deps"))?;
118 fs::write(debug.join("deps/liba-abc.rlib"), [0u8; 10])?;
119 fs::write(debug.join("a.d"), [0u8; 3])?;
120 fs::write(root.path().join("top.txt"), [0u8; 2])?;
121
122 let summary = summarize(root.path())?;
123 assert_eq!(summary.total_bytes, 15);
124 assert_eq!(summary.total_files, 3);
125
126 let debug_bucket = summary.dirs.iter().find(|d| d.name == "debug").unwrap();
127 assert_eq!(debug_bucket.bytes, 13);
128 assert_eq!(debug_bucket.files, 2);
129
130 let root_bucket = summary
131 .dirs
132 .iter()
133 .find(|d| d.name == "(cache root)")
134 .unwrap();
135 assert_eq!(root_bucket.bytes, 2);
136
137 assert_eq!(summary.top_units.len(), 1);
139 Ok(())
140 }
141}