cadi_builder/
cache.rs

1//! Build cache for CADI
2
3use cadi_core::{CadiError, CadiResult, sha256_bytes};
4use std::fs;
5use std::io::{Read, Write};
6use std::path::PathBuf;
7
8/// Build cache for storing and retrieving built artifacts
9pub struct BuildCache {
10    cache_dir: PathBuf,
11}
12
13impl BuildCache {
14    /// Create a new build cache
15    pub fn new(cache_dir: PathBuf) -> Self {
16        Self { cache_dir }
17    }
18
19    /// Check if a chunk exists in cache
20    pub fn has(&self, chunk_id: &str) -> CadiResult<bool> {
21        let path = self.chunk_path(chunk_id);
22        Ok(path.exists())
23    }
24
25    /// Retrieve a chunk from cache
26    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        // Verify hash matches
37        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    /// Store a chunk in cache
51    pub fn store(&self, chunk_id: &str, data: &[u8]) -> CadiResult<()> {
52        let path = self.chunk_path(chunk_id);
53        
54        // Create parent directories
55        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    /// Remove a chunk from cache
66    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    /// Get cache statistics
77    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, // Would need tracking to compute
102        })
103    }
104
105    /// Run garbage collection on the cache
106    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        // Simple GC: remove old entries
118        // In aggressive mode, remove more entries
119        let max_age = if aggressive {
120            std::time::Duration::from_secs(60 * 60 * 24) // 1 day
121        } else {
122            std::time::Duration::from_secs(60 * 60 * 24 * 7) // 7 days
123        };
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    /// Clear all cached chunks
162    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    /// Get the path for a chunk
170    pub fn get_path(&self, chunk_id: &str) -> PathBuf {
171        self.chunk_path(chunk_id)
172    }
173
174    /// Internal helper to get the path for a chunk
175    fn chunk_path(&self, chunk_id: &str) -> PathBuf {
176        // Use first 2 chars of hash as subdirectory for better filesystem performance
177        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/// Result of garbage collection
191#[derive(Debug)]
192pub struct GcResult {
193    pub removed: usize,
194    pub freed_bytes: u64,
195}