1use std::collections::HashSet;
7
8use crate::storage::StorageBackend;
9use crate::CacheError;
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::storage::local::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.put_narinfo("aaa", &make_narinfo("aaa", 100)).await.unwrap();
96 storage.put_narinfo("bbb", &make_narinfo("bbb", 200)).await.unwrap();
97
98 let roots = vec!["aaa".to_string(), "bbb".to_string()];
99 let result = collect_garbage(&storage, &roots).await.unwrap();
100
101 assert_eq!(result.paths_deleted, 0);
102 assert!(storage.get_narinfo("aaa").await.unwrap().is_some());
104 assert!(storage.get_narinfo("bbb").await.unwrap().is_some());
105 }
106
107 #[tokio::test]
108 async fn gc_deletes_non_roots() {
109 let dir = tempfile::tempdir().unwrap();
110 let storage = LocalStorage::new(dir.path());
111
112 storage.put_narinfo("keep", &make_narinfo("keep", 100)).await.unwrap();
113 storage.put_narinfo("drop", &make_narinfo("drop", 500)).await.unwrap();
114 storage.put_nar("nar/drop.nar.xz", b"nar data").await.unwrap();
115
116 let roots = vec!["keep".to_string()];
117 let result = collect_garbage(&storage, &roots).await.unwrap();
118
119 assert_eq!(result.paths_deleted, 1);
120 assert!(result.bytes_freed > 0);
121
122 assert!(storage.get_narinfo("keep").await.unwrap().is_some());
124 assert!(storage.get_narinfo("drop").await.unwrap().is_none());
125 }
126
127 #[tokio::test]
128 async fn gc_deletes_all_when_no_roots() {
129 let dir = tempfile::tempdir().unwrap();
130 let storage = LocalStorage::new(dir.path());
131
132 storage.put_narinfo("aaa", &make_narinfo("aaa", 100)).await.unwrap();
133 storage.put_narinfo("bbb", &make_narinfo("bbb", 200)).await.unwrap();
134 storage.put_narinfo("ccc", &make_narinfo("ccc", 300)).await.unwrap();
135
136 let result = collect_garbage(&storage, &[]).await.unwrap();
137
138 assert_eq!(result.paths_deleted, 3);
139 assert!(storage.list_narinfos().await.unwrap().is_empty());
140 }
141
142 #[tokio::test]
143 async fn gc_result_display() {
144 let result = GcResult {
145 paths_deleted: 5,
146 bytes_freed: 1024,
147 };
148 let s = format!("{result}");
149 assert!(s.contains("5"));
150 assert!(s.contains("1024"));
151 }
152
153 #[tokio::test]
154 async fn gc_accounts_for_file_size_and_narinfo_text() {
155 let dir = tempfile::tempdir().unwrap();
156 let storage = LocalStorage::new(dir.path());
157
158 let narinfo_text = make_narinfo("abc", 1000);
159 storage.put_narinfo("abc", &narinfo_text).await.unwrap();
160
161 let result = collect_garbage(&storage, &[]).await.unwrap();
162 assert_eq!(result.bytes_freed, 1000 + narinfo_text.len() as u64);
164 }
165}