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}