1use std::path::{Path, PathBuf};
8
9pub struct Usage {
11 pub label: &'static str,
12 pub path: PathBuf,
13 pub bytes: u64,
14 pub files: u64,
15 pub rebuildable: bool,
19}
20
21pub fn dir_size(path: &Path) -> (u64, u64) {
26 if !path.exists() {
27 return (0, 0);
28 }
29 if path.is_file() {
30 return (path.metadata().map(|m| m.len()).unwrap_or(0), 1);
31 }
32 let (mut bytes, mut files) = (0u64, 0u64);
33 let mut stack = vec![path.to_path_buf()];
34 while let Some(dir) = stack.pop() {
35 let Ok(entries) = std::fs::read_dir(&dir) else {
36 continue;
37 };
38 for entry in entries.flatten() {
39 let Ok(md) = entry.metadata() else { continue };
42 if md.is_dir() {
43 stack.push(entry.path());
44 } else {
45 bytes += md.len();
46 files += 1;
47 }
48 }
49 }
50 (bytes, files)
51}
52
53pub fn usage(
73 home: &Path,
74 db: Option<&Path>,
75 thumbs: &Path,
76 lib_embeddings: Option<&Path>,
77) -> Vec<Usage> {
78 fn row(label: &'static str, path: PathBuf, rebuildable: bool) -> Option<Usage> {
84 let (bytes, files) = dir_size(&path);
85 (bytes > 0).then_some(Usage {
86 label,
87 path,
88 bytes,
89 files,
90 rebuildable,
91 })
92 }
93
94 let mut out: Vec<Usage> = Vec::new();
95
96 if let Some(db) = db {
97 out.extend(row("database", db.to_path_buf(), false));
98
99 let (mut bytes, mut files) = (0u64, 0u64);
103 let mut wal = db.as_os_str().to_owned();
104 for suffix in ["-wal", "-shm"] {
105 let mut p = db.as_os_str().to_owned();
106 p.push(suffix);
107 let (b, f) = dir_size(Path::new(&p));
108 bytes += b;
109 files += f;
110 }
111 if bytes > 0 {
112 wal.push("-wal");
113 out.push(Usage {
114 label: "database journal",
115 path: PathBuf::from(wal),
116 bytes,
117 files,
118 rebuildable: true,
119 });
120 }
121 }
122 let all_embeddings = dir_size(&home.join("embeddings")).0;
126 let mine = match lib_embeddings {
127 Some(dir) => {
128 let r = row("embeddings", dir.to_path_buf(), false);
129 let n = r.as_ref().map(|u| u.bytes).unwrap_or(0);
130 out.extend(r);
131 n
132 }
133 None => {
134 out.extend(row("embeddings", home.join("embeddings"), false));
135 all_embeddings
136 }
137 };
138 if all_embeddings > mine {
139 out.push(Usage {
140 label: "embeddings (other libraries)",
141 path: home.join("embeddings"),
142 bytes: all_embeddings - mine,
143 files: 0,
144 rebuildable: false,
145 });
146 }
147 out.extend(row("thumbnails", thumbs.to_path_buf(), true));
148 out.extend(row("place names", home.join("geo"), true));
149 out.extend(row("locks", home.join("locks"), true));
150
151 out.sort_by(|a, b| b.bytes.cmp(&a.bytes));
152 out
153}
154
155pub fn human_bytes(bytes: u64) -> String {
157 const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
158 if bytes < 1024 {
159 return format!("{bytes} B");
160 }
161 let mut v = bytes as f64;
162 let mut unit = 0;
163 while v >= 1024.0 && unit < UNITS.len() - 1 {
164 v /= 1024.0;
165 unit += 1;
166 }
167 format!("{v:.1} {}", UNITS[unit])
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn a_missing_path_is_zero_not_an_error() {
176 assert_eq!(dir_size(Path::new("/definitely/not/here")), (0, 0));
177 }
178
179 #[test]
180 fn sizes_a_tree_including_nested_files() {
181 let d = tempfile::tempdir().unwrap();
182 std::fs::write(d.path().join("a"), b"12345").unwrap();
183 std::fs::create_dir(d.path().join("sub")).unwrap();
184 std::fs::write(d.path().join("sub/b"), b"123").unwrap();
185 assert_eq!(dir_size(d.path()), (8, 2));
186 }
187
188 #[test]
189 fn a_single_file_sizes_as_itself() {
190 let d = tempfile::tempdir().unwrap();
191 let f = d.path().join("x");
192 std::fs::write(&f, b"1234").unwrap();
193 assert_eq!(dir_size(&f), (4, 1));
194 }
195
196 #[test]
197 fn bytes_read_the_way_a_person_says_them() {
198 assert_eq!(human_bytes(0), "0 B");
199 assert_eq!(human_bytes(512), "512 B");
200 assert_eq!(human_bytes(1024), "1.0 KB");
201 assert_eq!(human_bytes(1_048_576), "1.0 MB");
202 assert_eq!(human_bytes(1_503_238_553), "1.4 GB");
203 }
204
205 #[test]
206 fn usage_reports_the_database_and_marks_what_is_rebuildable() {
207 let d = tempfile::tempdir().unwrap();
208 let db = d.path().join("t.db");
209 std::fs::write(&db, vec![0u8; 4096]).unwrap();
210 std::fs::create_dir_all(d.path().join("embeddings")).unwrap();
211 std::fs::write(d.path().join("embeddings/m.db"), vec![0u8; 8192]).unwrap();
212
213 let u = usage(d.path(), Some(&db), &d.path().join("no-thumbs"), None);
214 let by = |l: &str| u.iter().find(|x| x.label == l);
215
216 assert_eq!(u[0].label, "embeddings");
218 assert_eq!(by("embeddings").unwrap().bytes, 8192);
219 assert_eq!(by("database").unwrap().bytes, 4096);
220 assert!(!by("database").unwrap().rebuildable, "a database is not");
221 assert!(
222 !by("embeddings").unwrap().rebuildable,
223 "embeddings cost hours; losing them is not free"
224 );
225 }
226
227 #[test]
228 fn another_librarys_embeddings_are_not_counted_as_this_ones() {
229 let d = tempfile::tempdir().unwrap();
234 let db = d.path().join("t.db");
235 std::fs::write(&db, vec![0u8; 16]).unwrap();
236 let mine = d.path().join("embeddings/mine-0000000000000001");
237 let theirs = d.path().join("embeddings/theirs-0000000000000002");
238 std::fs::create_dir_all(&mine).unwrap();
239 std::fs::create_dir_all(&theirs).unwrap();
240 std::fs::write(mine.join("m.db"), vec![0u8; 1000]).unwrap();
241 std::fs::write(theirs.join("m.db"), vec![0u8; 7000]).unwrap();
242
243 let u = usage(
244 d.path(),
245 Some(&db),
246 &d.path().join("no-thumbs"),
247 Some(&mine),
248 );
249 let by = |l: &str| u.iter().find(|x| x.label == l).map(|x| x.bytes);
250 assert_eq!(by("embeddings"), Some(1000), "only this library's vectors");
251 assert_eq!(
252 by("embeddings (other libraries)"),
253 Some(7000),
254 "the rest is still disk use, but it is not this library's"
255 );
256 }
257
258 #[test]
259 fn nothing_present_reports_nothing_rather_than_a_row_of_zeroes() {
260 let d = tempfile::tempdir().unwrap();
261 let u = usage(d.path(), None, &d.path().join("no-thumbs"), None);
262 assert!(
263 u.iter().all(|x| x.bytes > 0 || x.files > 0),
264 "empty locations must not be listed"
265 );
266 }
267}