forge_guard/utils/
cache.rs1use crate::core::{ForgeGuardError, ProjectConfig};
4use serde::Serialize;
5use sha2::{Digest, Sha256};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::time::SystemTime;
9
10#[allow(dead_code)]
12pub struct Cache {
13 enabled: bool,
14 cache_dir: PathBuf,
15 memory_cache: HashMap<String, String>,
16 max_size: u64,
17 ttl_seconds: u64,
18}
19
20impl Cache {
21 pub fn new(config: &ProjectConfig) -> Result<Self, ForgeGuardError> {
23 let cache_dir = config.project_root.join(&config.cache.directory);
24 let enabled = config.cache.enabled;
25
26 if enabled {
27 std::fs::create_dir_all(&cache_dir).ok();
28 Self::clean_expired(&cache_dir, config.cache.ttl_seconds);
30 }
31
32 Ok(Self {
33 enabled,
34 cache_dir,
35 memory_cache: HashMap::new(),
36 max_size: config.cache.max_size_mb,
37 ttl_seconds: config.cache.ttl_seconds,
38 })
39 }
40
41 pub fn store<T: Serialize>(&self, key: &str, value: &T) -> Result<(), ForgeGuardError> {
43 if !self.enabled {
44 return Ok(());
45 }
46
47 let json = serde_json::to_string(value)?;
48 let cache_file = self.cache_path(key);
49
50 std::fs::write(&cache_file, &json)?;
55 let _ = filetime::set_file_mtime(&cache_file, filetime::FileTime::now());
57
58 Ok(())
59 }
60
61 pub fn load<T: serde::de::DeserializeOwned>(
63 &self,
64 key: &str,
65 ) -> Result<Option<T>, ForgeGuardError> {
66 if !self.enabled {
67 return Ok(None);
68 }
69
70 let cache_file = self.cache_path(key);
71 if !cache_file.exists() {
72 return Ok(None);
73 }
74
75 if let Ok(metadata) = std::fs::metadata(&cache_file) {
77 if let Ok(modified) = metadata.modified() {
78 let age = SystemTime::now()
79 .duration_since(modified)
80 .unwrap_or_default()
81 .as_secs();
82 if age > self.ttl_seconds {
83 std::fs::remove_file(&cache_file).ok();
84 return Ok(None);
85 }
86 }
87 }
88
89 let content = std::fs::read_to_string(&cache_file)?;
90 let value = serde_json::from_str(&content)?;
91 Ok(Some(value))
92 }
93
94 pub fn has(&self, key: &str) -> bool {
96 if !self.enabled {
97 return false;
98 }
99 let cache_file = self.cache_path(key);
100 if !cache_file.exists() {
101 return false;
102 }
103 if let Ok(metadata) = std::fs::metadata(&cache_file) {
104 if let Ok(modified) = metadata.modified() {
105 let age = SystemTime::now()
106 .duration_since(modified)
107 .unwrap_or_default()
108 .as_secs();
109 return age <= self.ttl_seconds;
110 }
111 }
112 false
113 }
114
115 pub fn clear(&self) -> Result<(), ForgeGuardError> {
117 if self.cache_dir.exists() {
118 std::fs::remove_dir_all(&self.cache_dir)?;
119 std::fs::create_dir_all(&self.cache_dir)?;
120 }
121 Ok(())
122 }
123
124 pub fn size(&self) -> u64 {
126 let mut total = 0;
127 if let Ok(entries) = std::fs::read_dir(&self.cache_dir) {
128 for entry in entries.flatten() {
129 if let Ok(metadata) = entry.metadata() {
130 total += metadata.len();
131 }
132 }
133 }
134 total
135 }
136
137 fn cache_path(&self, key: &str) -> PathBuf {
138 let sanitized: String = key
140 .chars()
141 .map(|c| {
142 if c.is_alphanumeric() || c == '-' || c == '_' {
143 c
144 } else {
145 '_'
146 }
147 })
148 .collect();
149 self.cache_dir.join(format!("{}.json", sanitized))
150 }
151
152 pub fn file_hash(path: &Path) -> Result<String, ForgeGuardError> {
156 let content = std::fs::read(path)?;
157 let hash = Sha256::digest(&content);
158 Ok(hex::encode(hash))
159 }
160
161 pub fn is_file_unchanged(&self, file_path: &Path) -> bool {
164 if !self.enabled {
165 return false;
166 }
167 let hash = match Self::file_hash(file_path) {
168 Ok(h) => h,
169 Err(_) => return false,
170 };
171 let key = &format!("file_hash_{}", file_path.to_string_lossy());
172 match self.load::<String>(key) {
173 Ok(Some(cached)) => cached == hash,
174 _ => false,
175 }
176 }
177
178 pub fn record_file_hash(&self, file_path: &Path) -> Result<(), ForgeGuardError> {
181 if !self.enabled {
182 return Ok(());
183 }
184 let hash = Self::file_hash(file_path)?;
185 let key = &format!("file_hash_{}", file_path.to_string_lossy());
186 self.store(key, &hash)
187 }
188
189 pub fn filter_changed_files(&self, files: &[PathBuf]) -> Vec<PathBuf> {
192 if !self.enabled {
193 return files.to_vec();
194 }
195 files
196 .iter()
197 .filter(|f| !self.is_file_unchanged(f))
198 .cloned()
199 .collect()
200 }
201
202 fn clean_expired(cache_dir: &PathBuf, ttl_seconds: u64) {
203 if let Ok(entries) = std::fs::read_dir(cache_dir) {
204 for entry in entries.flatten() {
205 if let Ok(metadata) = entry.metadata() {
206 if let Ok(modified) = metadata.modified() {
207 let age = SystemTime::now()
208 .duration_since(modified)
209 .unwrap_or_default()
210 .as_secs();
211 if age > ttl_seconds {
212 std::fs::remove_file(entry.path()).ok();
213 }
214 }
215 }
216 }
217 }
218 }
219}
220
221mod filetime {
223 use std::fs;
224 use std::time::SystemTime;
225
226 pub struct FileTime;
227 impl FileTime {
228 pub fn now() -> SystemTime {
229 SystemTime::now()
230 }
231 }
232
233 pub fn set_file_mtime(path: &std::path::Path, _time: SystemTime) -> std::io::Result<()> {
234 let _ = fs::File::options().write(true).open(path)?;
236 Ok(())
237 }
238}