gitai/remote/cache/
metadata.rs

1use 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
10// Type alias for cache key
11type 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    /// The repository URL
23    pub repo_url: String,
24    /// The branch that was cached
25    pub branch: String,
26    /// The commit hash at the time of caching
27    pub commit_hash: String,
28    /// When this cache entry was created
29    pub created_at: u64,
30    /// When this cache entry was last accessed
31    pub last_accessed: u64,
32    /// Size of the cached repository in bytes
33    pub size_bytes: u64,
34    /// The path to the cached repository
35    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        // Get the directory size
43        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    /// Update the last accessed time
57    pub fn update_access_time(&mut self) {
58        self.last_accessed = current_timestamp();
59    }
60}
61
62pub struct CacheMetadataManager {
63    /// In-memory cache of metadata
64    metadata: HashMap<CacheKey, CacheMetadata>,
65    /// Path to store metadata on disk
66    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        // Load existing metadata from file
77        manager.load_from_disk().ok();
78        manager
79    }
80
81    /// Store metadata for a cache entry
82    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    /// Retrieve metadata for a cache key
88    pub fn get_metadata(&self, key: &str) -> Option<&CacheMetadata> {
89        self.metadata.get(key)
90    }
91
92    /// Update the access time for a cache entry
93    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    /// Check if a cache entry exists and is still valid
103    pub fn is_cache_valid(&self, key: &str) -> bool {
104        self.metadata.contains_key(key)
105    }
106
107    /// Remove metadata for a cache key
108    pub fn remove_metadata(&mut self, key: &str) -> Result<(), String> {
109        self.metadata.remove(key);
110        self.save_to_disk()
111    }
112
113    /// Get all cache keys
114    pub fn get_all_keys(&self) -> Vec<String> {
115        self.metadata.keys().cloned().collect()
116    }
117
118    /// Save metadata to disk
119    fn save_to_disk(&self) -> Result<(), String> {
120        // Create directory if it doesn't exist
121        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    /// Load metadata from disk
136    fn load_from_disk(&mut self) -> Result<(), String> {
137        if !Path::new(&self.metadata_file_path).exists() {
138            // File doesn't exist yet, that's OK
139            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    /// Clean up old cache entries based on last access time
152    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        // Remove the old entries
163        for key in &to_remove {
164            self.metadata.remove(key);
165            // Note: We're not actually deleting the cache directory here,
166            // that would be handled separately
167        }
168
169        if !to_remove.is_empty() {
170            self.save_to_disk()?;
171        }
172
173        Ok(to_remove)
174    }
175}
176
177/// Helper function to get directory size (simplified implementation)
178fn 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                    // For simplicity, we're not recursively calculating subdirectory sizes
189                    size += 1024; // Estimate 1KB for directory
190                }
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        // Create test metadata
240        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        // Store metadata
258        manager
259            .store_metadata(key, metadata.clone())
260            .expect("Failed to store metadata");
261
262        // Retrieve metadata
263        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        // Update access time
269        manager
270            .update_access_time(key)
271            .expect("Failed to update access time");
272
273        // Check if cache is valid
274        assert!(manager.is_cache_valid(key));
275
276        // Get all keys
277        let keys = manager.get_all_keys();
278        assert_eq!(keys, vec!["test-key".to_string()]);
279    }
280}