1use cadi_core::{CadiError, CadiResult, sha256_bytes};
4use std::fs;
5use std::io::{Read, Write};
6use std::path::PathBuf;
7
8pub struct BuildCache {
10 cache_dir: PathBuf,
11}
12
13impl BuildCache {
14 pub fn new(cache_dir: PathBuf) -> Self {
16 Self { cache_dir }
17 }
18
19 pub fn has(&self, chunk_id: &str) -> CadiResult<bool> {
21 let path = self.chunk_path(chunk_id);
22 Ok(path.exists())
23 }
24
25 pub fn get(&self, chunk_id: &str) -> CadiResult<Option<Vec<u8>>> {
27 let path = self.chunk_path(chunk_id);
28 if !path.exists() {
29 return Ok(None);
30 }
31
32 let mut file = fs::File::open(&path)?;
33 let mut data = Vec::new();
34 file.read_to_end(&mut data)?;
35
36 let expected_hash = chunk_id.strip_prefix("chunk:sha256:")
38 .ok_or_else(|| CadiError::InvalidChunkId(chunk_id.to_string()))?;
39 let actual_hash = sha256_bytes(&data);
40
41 if expected_hash != actual_hash {
42 tracing::warn!("Cache corruption detected for {}", chunk_id);
43 fs::remove_file(&path)?;
44 return Ok(None);
45 }
46
47 Ok(Some(data))
48 }
49
50 pub fn store(&self, chunk_id: &str, data: &[u8]) -> CadiResult<()> {
52 let path = self.chunk_path(chunk_id);
53
54 if let Some(parent) = path.parent() {
56 fs::create_dir_all(parent)?;
57 }
58
59 let mut file = fs::File::create(&path)?;
60 file.write_all(data)?;
61
62 Ok(())
63 }
64
65 pub fn remove(&self, chunk_id: &str) -> CadiResult<bool> {
67 let path = self.chunk_path(chunk_id);
68 if path.exists() {
69 fs::remove_file(&path)?;
70 Ok(true)
71 } else {
72 Ok(false)
73 }
74 }
75
76 pub fn stats(&self) -> CadiResult<super::CacheStats> {
78 let mut total_entries = 0;
79 let mut total_size_bytes = 0u64;
80
81 if self.cache_dir.exists() {
82 for entry in fs::read_dir(&self.cache_dir)? {
83 let entry = entry?;
84 let path = entry.path();
85
86 if path.is_dir() {
87 for subentry in fs::read_dir(&path)? {
88 let subentry = subentry?;
89 if subentry.path().is_file() {
90 total_entries += 1;
91 total_size_bytes += subentry.metadata()?.len();
92 }
93 }
94 }
95 }
96 }
97
98 Ok(super::CacheStats {
99 total_entries,
100 total_size_bytes,
101 hit_rate: 0.0, })
103 }
104
105 pub fn gc(&self, aggressive: bool) -> CadiResult<GcResult> {
107 let mut removed = 0;
108 let mut freed_bytes = 0u64;
109
110 if !self.cache_dir.exists() {
111 return Ok(GcResult {
112 removed,
113 freed_bytes,
114 });
115 }
116
117 let max_age = if aggressive {
120 std::time::Duration::from_secs(60 * 60 * 24) } else {
122 std::time::Duration::from_secs(60 * 60 * 24 * 7) };
124
125 let now = std::time::SystemTime::now();
126
127 for entry in fs::read_dir(&self.cache_dir)? {
128 let entry = entry?;
129 let path = entry.path();
130
131 if path.is_dir() {
132 for subentry in fs::read_dir(&path)? {
133 let subentry = subentry?;
134 let subpath = subentry.path();
135
136 if subpath.is_file() {
137 if let Ok(metadata) = subentry.metadata() {
138 if let Ok(modified) = metadata.modified() {
139 if let Ok(age) = now.duration_since(modified) {
140 if age > max_age {
141 let size = metadata.len();
142 if fs::remove_file(&subpath).is_ok() {
143 removed += 1;
144 freed_bytes += size;
145 }
146 }
147 }
148 }
149 }
150 }
151 }
152 }
153 }
154
155 Ok(GcResult {
156 removed,
157 freed_bytes,
158 })
159 }
160
161 pub fn clear(&self) -> CadiResult<()> {
163 if self.cache_dir.exists() {
164 fs::remove_dir_all(&self.cache_dir)?;
165 }
166 Ok(())
167 }
168
169 pub fn get_path(&self, chunk_id: &str) -> PathBuf {
171 self.chunk_path(chunk_id)
172 }
173
174 fn chunk_path(&self, chunk_id: &str) -> PathBuf {
176 let hash = chunk_id.strip_prefix("chunk:sha256:")
178 .unwrap_or(chunk_id);
179
180 let prefix = if hash.len() >= 2 {
181 &hash[..2]
182 } else {
183 "00"
184 };
185
186 self.cache_dir.join("chunks").join(prefix).join(hash)
187 }
188}
189
190#[derive(Debug)]
192pub struct GcResult {
193 pub removed: usize,
194 pub freed_bytes: u64,
195}