Skip to main content

githem_core/
cache.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CacheEntry {
11    pub repo_url: String,
12    pub branch: String,
13    pub commit_hash: String,
14    pub files: Vec<CachedFile>,
15    pub metadata: CacheMetadata,
16    pub created_at: u64,
17    pub last_accessed: u64,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct CachedFile {
22    pub path: PathBuf,
23    pub content: Vec<u8>,
24    pub size: u64,
25    pub is_binary: bool,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct CacheMetadata {
30    pub total_files: usize,
31    pub total_size: u64,
32    pub tree_hash: String,
33    pub cache_version: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CacheStats {
38    pub total_entries: usize,
39    pub total_size: u64,
40    pub max_size: u64,
41    pub expired_entries: usize,
42    pub cache_dir: PathBuf,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46struct CacheIndex {
47    pub entries: HashMap<String, CacheEntryInfo>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51struct CacheEntryInfo {
52    pub key: String,
53    pub path: PathBuf,
54    pub size: u64,
55    pub created_at: u64,
56    pub last_accessed: u64,
57    pub commit_hash: String,
58}
59
60pub struct RepositoryCache {
61    cache_dir: PathBuf,
62    index: HashMap<String, CacheEntryInfo>,
63    max_cache_size: u64,
64    max_age_seconds: u64,
65}
66
67impl RepositoryCache {
68    pub fn new() -> Result<Self> {
69        Self::with_config(5 * 1024 * 1024 * 1024, 7 * 24 * 3600)
70    }
71
72    pub fn with_config(max_size: u64, max_age_seconds: u64) -> Result<Self> {
73        let cache_dir = Self::get_cache_dir()?;
74        fs::create_dir_all(&cache_dir)?;
75
76        let index = Self::load_index(&cache_dir).unwrap_or_default();
77
78        Ok(Self {
79            cache_dir,
80            index,
81            max_cache_size: max_size,
82            max_age_seconds,
83        })
84    }
85
86    fn get_cache_dir() -> Result<PathBuf> {
87        let cache_dir = if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
88            PathBuf::from(xdg_cache).join("githem")
89        } else if let Ok(home) = std::env::var("HOME") {
90            PathBuf::from(home).join(".cache").join("githem")
91        } else {
92            PathBuf::from("/tmp/githem-cache")
93        };
94        Ok(cache_dir)
95    }
96
97    pub fn generate_cache_key(url: &str, branch: Option<&str>) -> String {
98        let mut hasher = Sha256::new();
99        hasher.update(url.as_bytes());
100        if let Some(branch) = branch {
101            hasher.update(b":");
102            hasher.update(branch.as_bytes());
103        }
104        format!("{:x}", hasher.finalize())
105    }
106
107    pub fn get(&mut self, key: &str) -> Result<Option<CacheEntry>> {
108        if let Some(info) = self.index.get_mut(key) {
109            let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
110
111            if now - info.created_at > self.max_age_seconds {
112                self.remove(key)?;
113                return Ok(None);
114            }
115
116            info.last_accessed = now;
117
118            let cache_path = &info.path;
119            if cache_path.exists() {
120                let data = fs::read(cache_path)?;
121                let entry: CacheEntry = bincode::deserialize(&data)?;
122                self.save_index()?;
123                return Ok(Some(entry));
124            }
125        }
126        Ok(None)
127    }
128
129    pub fn put(&mut self, key: String, entry: CacheEntry) -> Result<()> {
130        let serialized = bincode::serialize(&entry)?;
131        let entry_size = serialized.len() as u64;
132
133        self.evict_if_needed(entry_size)?;
134
135        let cache_file = self.cache_dir.join(format!("{}.cache", key));
136        fs::write(&cache_file, serialized)?;
137
138        let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
139        self.index.insert(
140            key.clone(),
141            CacheEntryInfo {
142                key,
143                path: cache_file,
144                size: entry_size,
145                created_at: now,
146                last_accessed: now,
147                commit_hash: entry.commit_hash.clone(),
148            },
149        );
150
151        self.save_index()?;
152        Ok(())
153    }
154
155    pub fn check_commit(&self, key: &str, current_commit: &str) -> CacheCommitStatus {
156        if let Some(info) = self.index.get(key) {
157            if info.commit_hash == current_commit {
158                CacheCommitStatus::Match
159            } else {
160                CacheCommitStatus::Outdated
161            }
162        } else {
163            CacheCommitStatus::NotCached
164        }
165    }
166
167    pub fn remove(&mut self, key: &str) -> Result<()> {
168        if let Some(info) = self.index.remove(key) {
169            if info.path.exists() {
170                fs::remove_file(info.path)?;
171            }
172            self.save_index()?;
173        }
174        Ok(())
175    }
176
177    fn evict_if_needed(&mut self, new_entry_size: u64) -> Result<()> {
178        let total_size: u64 = self.index.values().map(|e| e.size).sum();
179
180        if total_size + new_entry_size <= self.max_cache_size {
181            return Ok(());
182        }
183
184        let mut entries: Vec<_> = self.index.values().cloned().collect();
185        entries.sort_by_key(|e| e.last_accessed);
186
187        let mut freed_space = 0u64;
188        for entry in entries {
189            if total_size - freed_space + new_entry_size <= self.max_cache_size {
190                break;
191            }
192            freed_space += entry.size;
193            self.remove(&entry.key)?;
194        }
195
196        Ok(())
197    }
198
199    fn load_index(cache_dir: &Path) -> Result<HashMap<String, CacheEntryInfo>> {
200        let index_path = cache_dir.join("index.json");
201        if index_path.exists() {
202            let data = fs::read_to_string(index_path)?;
203            let index: CacheIndex = serde_json::from_str(&data)?;
204            Ok(index.entries)
205        } else {
206            Ok(HashMap::new())
207        }
208    }
209
210    fn save_index(&self) -> Result<()> {
211        let index_path = self.cache_dir.join("index.json");
212        let index = CacheIndex {
213            entries: self.index.clone(),
214        };
215        let data = serde_json::to_string_pretty(&index)?;
216        fs::write(index_path, data)?;
217        Ok(())
218    }
219
220    pub fn clear_all(&mut self) -> Result<()> {
221        for key in self.index.keys().cloned().collect::<Vec<_>>() {
222            self.remove(&key)?;
223        }
224        Ok(())
225    }
226
227    pub fn get_stats(&self) -> CacheStats {
228        let total_size: u64 = self.index.values().map(|e| e.size).sum();
229        let now = SystemTime::now()
230            .duration_since(UNIX_EPOCH)
231            .unwrap()
232            .as_secs();
233
234        let expired_count = self
235            .index
236            .values()
237            .filter(|e| now - e.created_at > self.max_age_seconds)
238            .count();
239
240        CacheStats {
241            total_entries: self.index.len(),
242            total_size,
243            max_size: self.max_cache_size,
244            expired_entries: expired_count,
245            cache_dir: self.cache_dir.clone(),
246        }
247    }
248}
249
250#[derive(Debug, PartialEq)]
251pub enum CacheCommitStatus {
252    Match,
253    Outdated,
254    NotCached,
255}
256
257pub struct CacheManager;
258
259impl CacheManager {
260    pub fn clear_cache() -> Result<()> {
261        let mut cache = RepositoryCache::new()?;
262        cache.clear_all()?;
263        Ok(())
264    }
265
266    pub fn get_stats() -> Result<CacheStats> {
267        let cache = RepositoryCache::new()?;
268        Ok(cache.get_stats())
269    }
270}