1use std::collections::HashSet;
7
8use crate::CacheError;
9use crate::StorageBackend;
10
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct GcResult {
14 pub paths_deleted: usize,
16 pub bytes_freed: u64,
18}
19
20impl std::fmt::Display for GcResult {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(
23 f,
24 "GC: {} paths deleted, {} bytes freed",
25 self.paths_deleted, self.bytes_freed
26 )
27 }
28}
29
30pub async fn collect_garbage(
37 storage: &dyn StorageBackend,
38 roots: &[String],
39) -> Result<GcResult, CacheError> {
40 let all = storage.list_narinfos().await?;
41 let keep: HashSet<&str> = roots.iter().map(String::as_str).collect();
42
43 let mut result = GcResult::default();
44
45 for hash in &all {
46 if !keep.contains(hash.as_str()) {
47 if let Ok(Some(content)) = storage.get_narinfo(hash).await {
49 if let Ok(info) = sui_compat::narinfo::NarInfo::parse(&content) {
50 result.bytes_freed += info.file_size;
51 }
52 result.bytes_freed += content.len() as u64;
54 }
55 storage.delete(hash).await?;
56 result.paths_deleted += 1;
57 }
58 }
59
60 Ok(result)
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::LocalStorage;
67
68 fn make_narinfo(hash: &str, file_size: u64) -> String {
69 format!(
70 "StorePath: /nix/store/{hash}-pkg\n\
71 URL: nar/{hash}.nar.xz\n\
72 Compression: xz\n\
73 FileHash: sha256:aaaa\n\
74 FileSize: {file_size}\n\
75 NarHash: sha256:bbbb\n\
76 NarSize: 5000\n\
77 References: \n"
78 )
79 }
80
81 #[tokio::test]
82 async fn gc_empty_cache() {
83 let dir = tempfile::tempdir().unwrap();
84 let storage = LocalStorage::new(dir.path());
85 let result = collect_garbage(&storage, &[]).await.unwrap();
86 assert_eq!(result.paths_deleted, 0);
87 assert_eq!(result.bytes_freed, 0);
88 }
89
90 #[tokio::test]
91 async fn gc_keeps_roots() {
92 let dir = tempfile::tempdir().unwrap();
93 let storage = LocalStorage::new(dir.path());
94
95 storage
96 .put_narinfo("aaa", &make_narinfo("aaa", 100))
97 .await
98 .unwrap();
99 storage
100 .put_narinfo("bbb", &make_narinfo("bbb", 200))
101 .await
102 .unwrap();
103
104 let roots = vec!["aaa".to_string(), "bbb".to_string()];
105 let result = collect_garbage(&storage, &roots).await.unwrap();
106
107 assert_eq!(result.paths_deleted, 0);
108 assert!(storage.get_narinfo("aaa").await.unwrap().is_some());
110 assert!(storage.get_narinfo("bbb").await.unwrap().is_some());
111 }
112
113 #[tokio::test]
114 async fn gc_deletes_non_roots() {
115 let dir = tempfile::tempdir().unwrap();
116 let storage = LocalStorage::new(dir.path());
117
118 storage
119 .put_narinfo("keep", &make_narinfo("keep", 100))
120 .await
121 .unwrap();
122 storage
123 .put_narinfo("drop", &make_narinfo("drop", 500))
124 .await
125 .unwrap();
126 storage
127 .put_nar("nar/drop.nar.xz", b"nar data")
128 .await
129 .unwrap();
130
131 let roots = vec!["keep".to_string()];
132 let result = collect_garbage(&storage, &roots).await.unwrap();
133
134 assert_eq!(result.paths_deleted, 1);
135 assert!(result.bytes_freed > 0);
136
137 assert!(storage.get_narinfo("keep").await.unwrap().is_some());
139 assert!(storage.get_narinfo("drop").await.unwrap().is_none());
140 }
141
142 #[tokio::test]
143 async fn gc_deletes_all_when_no_roots() {
144 let dir = tempfile::tempdir().unwrap();
145 let storage = LocalStorage::new(dir.path());
146
147 storage
148 .put_narinfo("aaa", &make_narinfo("aaa", 100))
149 .await
150 .unwrap();
151 storage
152 .put_narinfo("bbb", &make_narinfo("bbb", 200))
153 .await
154 .unwrap();
155 storage
156 .put_narinfo("ccc", &make_narinfo("ccc", 300))
157 .await
158 .unwrap();
159
160 let result = collect_garbage(&storage, &[]).await.unwrap();
161
162 assert_eq!(result.paths_deleted, 3);
163 assert!(storage.list_narinfos().await.unwrap().is_empty());
164 }
165
166 #[tokio::test]
167 async fn gc_result_display() {
168 let result = GcResult {
169 paths_deleted: 5,
170 bytes_freed: 1024,
171 };
172 let s = format!("{result}");
173 assert!(s.contains("5"));
174 assert!(s.contains("1024"));
175 }
176
177 #[tokio::test]
178 async fn gc_accounts_for_file_size_and_narinfo_text() {
179 let dir = tempfile::tempdir().unwrap();
180 let storage = LocalStorage::new(dir.path());
181
182 let narinfo_text = make_narinfo("abc", 1000);
183 storage.put_narinfo("abc", &narinfo_text).await.unwrap();
184
185 let result = collect_garbage(&storage, &[]).await.unwrap();
186 assert_eq!(result.bytes_freed, 1000 + narinfo_text.len() as u64);
188 }
189}