clnrm_core/cache/
file_cache.rs1use super::cache_trait::{Cache, CacheStats};
7use super::hash;
8use crate::error::{CleanroomError, Result};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex};
15use tracing::{debug, info, warn};
16
17const CACHE_VERSION: &str = "1.0.0";
19
20fn default_cache_dir() -> Result<PathBuf> {
22 let home = std::env::var("HOME")
23 .or_else(|_| std::env::var("USERPROFILE"))
24 .map_err(|_| CleanroomError::configuration_error("Cannot determine home directory"))?;
25
26 Ok(PathBuf::from(home).join(".clnrm").join("cache"))
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CacheFile {
32 pub version: String,
34 pub hashes: HashMap<String, String>,
36 pub last_updated: DateTime<Utc>,
38}
39
40impl CacheFile {
41 pub fn new() -> Self {
43 Self {
44 version: CACHE_VERSION.to_string(),
45 hashes: HashMap::new(),
46 last_updated: Utc::now(),
47 }
48 }
49
50 pub fn is_compatible(&self) -> bool {
52 self.version == CACHE_VERSION
53 }
54}
55
56impl Default for CacheFile {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62#[derive(Debug, Clone)]
89pub struct FileCache {
90 cache_path: PathBuf,
92 cache: Arc<Mutex<CacheFile>>,
94}
95
96impl FileCache {
97 pub fn new() -> Result<Self> {
99 let cache_dir = default_cache_dir()?;
100 let cache_path = cache_dir.join("hashes.json");
101 Self::with_path(cache_path)
102 }
103
104 pub fn with_path(cache_path: PathBuf) -> Result<Self> {
106 if let Some(parent) = cache_path.parent() {
108 if !parent.exists() {
109 fs::create_dir_all(parent).map_err(|e| {
110 CleanroomError::io_error(format!(
111 "Failed to create cache directory '{}': {}",
112 parent.display(),
113 e
114 ))
115 })?;
116 info!("Created cache directory: {}", parent.display());
117 }
118 }
119
120 let cache = if cache_path.exists() {
122 match Self::load_cache_file(&cache_path) {
123 Ok(mut cache_file) => {
124 if !cache_file.is_compatible() {
126 warn!(
127 "Cache version mismatch (expected {}, got {}). Creating new cache.",
128 CACHE_VERSION, cache_file.version
129 );
130 cache_file = CacheFile::new();
131 }
132 cache_file
133 }
134 Err(e) => {
135 warn!("Failed to load cache file: {}. Creating new cache.", e);
136 CacheFile::new()
137 }
138 }
139 } else {
140 debug!("Cache file not found. Creating new cache.");
141 CacheFile::new()
142 };
143
144 Ok(Self {
145 cache_path,
146 cache: Arc::new(Mutex::new(cache)),
147 })
148 }
149
150 fn load_cache_file(path: &Path) -> Result<CacheFile> {
152 let content = fs::read_to_string(path).map_err(|e| {
153 CleanroomError::io_error(format!(
154 "Failed to read cache file '{}': {}",
155 path.display(),
156 e
157 ))
158 })?;
159
160 serde_json::from_str(&content).map_err(|e| {
161 CleanroomError::serialization_error(format!(
162 "Failed to parse cache file '{}': {}",
163 path.display(),
164 e
165 ))
166 })
167 }
168
169 pub fn cache_path(&self) -> &Path {
171 &self.cache_path
172 }
173}
174
175impl Cache for FileCache {
176 fn has_changed(&self, file_path: &Path, rendered_content: &str) -> Result<bool> {
177 let file_key = file_path
178 .to_str()
179 .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
180 .to_string();
181
182 let current_hash = hash::hash_content(rendered_content)?;
184
185 let cache = self.cache.lock().map_err(|e| {
187 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
188 })?;
189
190 match cache.hashes.get(&file_key) {
191 Some(cached_hash) if cached_hash == ¤t_hash => {
192 debug!("Cache hit: {} (unchanged)", file_key);
193 Ok(false)
194 }
195 Some(_) => {
196 debug!("Cache miss: {} (changed)", file_key);
197 Ok(true)
198 }
199 None => {
200 debug!("Cache miss: {} (new file)", file_key);
201 Ok(true)
202 }
203 }
204 }
205
206 fn update(&self, file_path: &Path, rendered_content: &str) -> Result<()> {
207 let file_key = file_path
208 .to_str()
209 .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
210 .to_string();
211
212 let hash = hash::hash_content(rendered_content)?;
213
214 let mut cache = self.cache.lock().map_err(|e| {
215 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
216 })?;
217
218 cache.hashes.insert(file_key.clone(), hash);
219 debug!("Cache updated: {}", file_key);
220
221 Ok(())
222 }
223
224 fn remove(&self, file_path: &Path) -> Result<()> {
225 let file_key = file_path
226 .to_str()
227 .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
228 .to_string();
229
230 let mut cache = self.cache.lock().map_err(|e| {
231 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
232 })?;
233
234 if cache.hashes.remove(&file_key).is_some() {
235 debug!("Removed from cache: {}", file_key);
236 }
237
238 Ok(())
239 }
240
241 fn save(&self) -> Result<()> {
242 let cache = self.cache.lock().map_err(|e| {
243 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
244 })?;
245
246 let mut cache_to_save = cache.clone();
248 cache_to_save.last_updated = Utc::now();
249
250 let content = serde_json::to_string_pretty(&cache_to_save).map_err(|e| {
251 CleanroomError::serialization_error(format!("Failed to serialize cache: {}", e))
252 })?;
253
254 fs::write(&self.cache_path, content).map_err(|e| {
255 CleanroomError::io_error(format!(
256 "Failed to write cache file '{}': {}",
257 self.cache_path.display(),
258 e
259 ))
260 })?;
261
262 debug!("Cache saved to: {}", self.cache_path.display());
263 Ok(())
264 }
265
266 fn stats(&self) -> Result<CacheStats> {
267 let cache = self.cache.lock().map_err(|e| {
268 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
269 })?;
270
271 Ok(CacheStats {
272 total_files: cache.hashes.len(),
273 last_updated: cache.last_updated,
274 cache_path: Some(self.cache_path.clone()),
275 })
276 }
277
278 fn clear(&self) -> Result<()> {
279 let mut cache = self.cache.lock().map_err(|e| {
280 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
281 })?;
282
283 let count = cache.hashes.len();
284 cache.hashes.clear();
285 cache.last_updated = Utc::now();
286
287 info!("Cleared {} entries from cache", count);
288 Ok(())
289 }
290}
291
292