gitai/remote/cache/
metadata.rs1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use serde::{Deserialize, Serialize};
7
8use crate::remote::RepositoryConfiguration;
9
10type CacheKey = String;
12
13fn current_timestamp() -> u64 {
14 SystemTime::now()
15 .duration_since(UNIX_EPOCH)
16 .expect("SystemTime::now() should always be after UNIX_EPOCH")
17 .as_secs()
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct CacheMetadata {
22 pub repo_url: String,
24 pub branch: String,
26 pub commit_hash: String,
28 pub created_at: u64,
30 pub last_accessed: u64,
32 pub size_bytes: u64,
34 pub cache_path: String,
36}
37
38impl CacheMetadata {
39 pub fn new(config: &RepositoryConfiguration, cache_path: &str, commit_hash: &str) -> Self {
40 let now = current_timestamp();
41
42 let size = get_directory_size(cache_path);
44
45 Self {
46 repo_url: config.url.clone(),
47 branch: config.branch.clone(),
48 commit_hash: commit_hash.to_string(),
49 created_at: now,
50 last_accessed: now,
51 size_bytes: size,
52 cache_path: cache_path.to_string(),
53 }
54 }
55
56 pub fn update_access_time(&mut self) {
58 self.last_accessed = current_timestamp();
59 }
60}
61
62pub struct CacheMetadataManager {
63 metadata: HashMap<CacheKey, CacheMetadata>,
65 metadata_file_path: String,
67}
68
69impl CacheMetadataManager {
70 pub fn new(metadata_file_path: String) -> Self {
71 let mut manager = Self {
72 metadata: HashMap::new(),
73 metadata_file_path,
74 };
75
76 manager.load_from_disk().ok();
78 manager
79 }
80
81 pub fn store_metadata(&mut self, key: &str, metadata: CacheMetadata) -> Result<(), String> {
83 self.metadata.insert(key.to_string(), metadata);
84 self.save_to_disk()
85 }
86
87 pub fn get_metadata(&self, key: &str) -> Option<&CacheMetadata> {
89 self.metadata.get(key)
90 }
91
92 pub fn update_access_time(&mut self, key: &str) -> Result<(), String> {
94 if let Some(metadata) = self.metadata.get_mut(key) {
95 metadata.update_access_time();
96 self.save_to_disk()
97 } else {
98 Err(format!("Metadata not found for key: {key}"))
99 }
100 }
101
102 pub fn is_cache_valid(&self, key: &str) -> bool {
104 self.metadata.contains_key(key)
105 }
106
107 pub fn remove_metadata(&mut self, key: &str) -> Result<(), String> {
109 self.metadata.remove(key);
110 self.save_to_disk()
111 }
112
113 pub fn get_all_keys(&self) -> Vec<String> {
115 self.metadata.keys().cloned().collect()
116 }
117
118 fn save_to_disk(&self) -> Result<(), String> {
120 if let Some(parent) = Path::new(&self.metadata_file_path).parent() {
122 fs::create_dir_all(parent)
123 .map_err(|e| format!("Failed to create metadata directory: {e}"))?;
124 }
125
126 let json = serde_json::to_string_pretty(&self.metadata)
127 .map_err(|e| format!("Failed to serialize metadata: {e}"))?;
128
129 fs::write(&self.metadata_file_path, json)
130 .map_err(|e| format!("Failed to write metadata file: {e}"))?;
131
132 Ok(())
133 }
134
135 fn load_from_disk(&mut self) -> Result<(), String> {
137 if !Path::new(&self.metadata_file_path).exists() {
138 return Ok(());
140 }
141
142 let json = fs::read_to_string(&self.metadata_file_path)
143 .map_err(|e| format!("Failed to read metadata file: {e}"))?;
144
145 self.metadata = serde_json::from_str(&json)
146 .map_err(|e| format!("Failed to deserialize metadata: {e}"))?;
147
148 Ok(())
149 }
150
151 pub fn cleanup_old_entries(&mut self, max_age_seconds: u64) -> Result<Vec<String>, String> {
153 let now = current_timestamp();
154
155 let mut to_remove = Vec::new();
156 for (key, metadata) in &self.metadata {
157 if now - metadata.last_accessed > max_age_seconds {
158 to_remove.push(key.clone());
159 }
160 }
161
162 for key in &to_remove {
164 self.metadata.remove(key);
165 }
168
169 if !to_remove.is_empty() {
170 self.save_to_disk()?;
171 }
172
173 Ok(to_remove)
174 }
175}
176
177fn get_directory_size(path: &str) -> u64 {
179 let mut size = 0;
180 if let Ok(entries) = fs::read_dir(path) {
181 for entry in entries {
182 if let Ok(entry) = entry
183 && let Ok(metadata) = entry.metadata()
184 {
185 if metadata.is_file() {
186 size += metadata.len();
187 } else if metadata.is_dir() {
188 size += 1024; }
191 }
192 }
193 }
194 size
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use tempfile::TempDir;
201
202 #[test]
203 fn test_cache_metadata_creation() {
204 let config = RepositoryConfiguration::new(
205 "https://github.com/example/repo.git".to_string(),
206 "main".to_string(),
207 "./src/module1".to_string(),
208 vec!["src/".to_string()],
209 None,
210 None,
211 );
212
213 let temp_dir = TempDir::new().expect("Failed to create temporary directory for test");
214 let cache_path = temp_dir
215 .path()
216 .to_str()
217 .expect("Failed to convert temporary directory path to string");
218
219 let metadata = CacheMetadata::new(&config, cache_path, "abc123");
220
221 assert_eq!(metadata.repo_url, "https://github.com/example/repo.git");
222 assert_eq!(metadata.branch, "main");
223 assert_eq!(metadata.commit_hash, "abc123");
224 assert_eq!(metadata.cache_path, cache_path);
225 }
226
227 #[test]
228 fn test_cache_metadata_manager() {
229 let temp_dir = TempDir::new().expect("Failed to create temporary directory for test");
230 let metadata_file = temp_dir
231 .path()
232 .join("metadata.json")
233 .to_str()
234 .expect("Failed to convert path to string")
235 .to_string();
236
237 let mut manager = CacheMetadataManager::new(metadata_file);
238
239 let config = RepositoryConfiguration::new(
241 "https://github.com/example/repo.git".to_string(),
242 "main".to_string(),
243 "./src/module1".to_string(),
244 vec!["src/".to_string()],
245 None,
246 None,
247 );
248
249 let cache_path_binding = temp_dir.path().join("cache");
250 let test_cache_path = cache_path_binding
251 .to_str()
252 .expect("Failed to convert cache path to string");
253
254 let metadata = CacheMetadata::new(&config, test_cache_path, "abc123");
255 let key = "test-key";
256
257 manager
259 .store_metadata(key, metadata.clone())
260 .expect("Failed to store metadata");
261
262 let retrieved = manager
264 .get_metadata(key)
265 .expect("Failed to retrieve stored metadata");
266 assert_eq!(retrieved.repo_url, metadata.repo_url);
267
268 manager
270 .update_access_time(key)
271 .expect("Failed to update access time");
272
273 assert!(manager.is_cache_valid(key));
275
276 let keys = manager.get_all_keys();
278 assert_eq!(keys, vec!["test-key".to_string()]);
279 }
280}