1use anyhow::{Context, Result};
2use http::{HeaderMap, Uri};
3use octocrab::service::middleware::cache::{CacheKey, CacheStorage, CacheWriter, CachedResponse};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::sync::{Arc, Mutex};
7
8#[derive(Clone, Debug)]
10pub struct CacheConfig {
11 pub enabled: bool, }
13
14pub fn get_cache_path() -> PathBuf {
16 dirs::cache_dir()
17 .map(|p| p.join("pr-bro/http-cache"))
18 .unwrap_or_else(|| {
19 PathBuf::from(format!(
20 "{}/.cache/pr-bro/http-cache",
21 std::env::var("HOME").unwrap_or_default()
22 ))
23 })
24}
25
26pub fn clear_cache() -> Result<()> {
28 let cache_path = get_cache_path();
29 match std::fs::remove_dir_all(&cache_path) {
30 Ok(()) => Ok(()),
31 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
32 Err(e) => Err(e).context("Failed to remove cache directory"),
33 }
34}
35
36#[derive(Clone)]
41pub struct DiskCache {
42 inner: Arc<Mutex<CacheData>>,
43 cache_path: PathBuf,
44}
45
46struct CacheData {
47 keys: HashMap<String, CacheKey>, responses: HashMap<String, CachedResponse>, }
50
51#[derive(serde::Serialize, serde::Deserialize)]
53struct DiskCacheEntry {
54 etag: Option<String>,
55 last_modified: Option<String>,
56 headers: Vec<(String, Vec<u8>)>, body: Vec<u8>,
58}
59
60impl DiskCacheEntry {
61 fn from_parts(key: &CacheKey, response: &CachedResponse) -> Self {
63 let (etag, last_modified) = match key {
64 CacheKey::ETag(etag) => (Some(etag.clone()), None),
65 CacheKey::LastModified(lm) => (None, Some(lm.clone())),
66 _ => (None, None), };
68
69 let headers: Vec<(String, Vec<u8>)> = response
70 .headers
71 .iter()
72 .map(|(name, value)| (name.to_string(), value.as_bytes().to_vec()))
73 .collect();
74
75 Self {
76 etag,
77 last_modified,
78 headers,
79 body: response.body.clone(),
80 }
81 }
82
83 fn to_parts(&self) -> Result<(CacheKey, CachedResponse)> {
85 let key = if let Some(etag) = &self.etag {
86 CacheKey::ETag(etag.clone())
87 } else if let Some(lm) = &self.last_modified {
88 CacheKey::LastModified(lm.clone())
89 } else {
90 anyhow::bail!("Invalid cache entry: no ETag or Last-Modified");
91 };
92
93 let mut headers = HeaderMap::new();
94 for (name, value) in &self.headers {
95 let header_name: http::HeaderName = name.parse().context("Invalid header name")?;
96 let header_value =
97 http::HeaderValue::from_bytes(value).context("Invalid header value")?;
98 headers.insert(header_name, header_value);
99 }
100
101 let response = CachedResponse {
102 headers,
103 body: self.body.clone(),
104 };
105
106 Ok((key, response))
107 }
108}
109
110impl DiskCache {
111 pub fn new(cache_path: PathBuf) -> Self {
112 Self {
114 inner: Arc::new(Mutex::new(CacheData {
115 keys: HashMap::new(),
116 responses: HashMap::new(),
117 })),
118 cache_path,
119 }
120 }
121
122 pub fn clear_memory(&self) {
124 let mut data = self.inner.lock().unwrap();
125 data.keys.clear();
126 data.responses.clear();
127 }
128
129 fn load_from_disk(&self, uri_key: &str) -> Option<CacheKey> {
131 let bytes = cacache::read_sync(&self.cache_path, uri_key).ok()?;
133
134 let entry: DiskCacheEntry = serde_json::from_slice(&bytes).ok()?;
136
137 let (key, response) = entry.to_parts().ok()?;
139
140 let mut data = self.inner.lock().unwrap();
142 data.keys.insert(uri_key.to_string(), key.clone());
143 data.responses.insert(uri_key.to_string(), response);
144
145 Some(key)
146 }
147}
148
149impl CacheStorage for DiskCache {
150 fn try_hit(&self, uri: &Uri) -> Option<CacheKey> {
151 let uri_key = uri.to_string();
152
153 {
155 let data = self.inner.lock().unwrap();
156 if let Some(cache_key) = data.keys.get(&uri_key) {
157 return Some(cache_key.clone());
158 }
159 }
160
161 self.load_from_disk(&uri_key)
163 }
164
165 fn load(&self, uri: &Uri) -> Option<CachedResponse> {
166 let data = self.inner.lock().unwrap();
167 data.responses.get(&uri.to_string()).cloned()
168 }
169
170 fn writer(&self, uri: &Uri, key: CacheKey, headers: HeaderMap) -> Box<dyn CacheWriter> {
171 Box::new(DiskCacheWriter {
172 cache: self.inner.clone(),
173 cache_path: self.cache_path.clone(),
174 uri_key: uri.to_string(),
175 key,
176 response: CachedResponse {
177 body: Vec::new(),
178 headers,
179 },
180 })
181 }
182}
183
184struct DiskCacheWriter {
186 cache: Arc<Mutex<CacheData>>,
187 cache_path: PathBuf,
188 uri_key: String,
189 key: CacheKey,
190 response: CachedResponse,
191}
192
193impl CacheWriter for DiskCacheWriter {
194 fn write_body(&mut self, data: &[u8]) {
195 self.response.body.extend_from_slice(data);
196 }
197}
198
199impl Drop for DiskCacheWriter {
200 fn drop(&mut self) {
201 let uri_key = self.uri_key.clone();
202 let key = self.key.clone();
203 let response = CachedResponse {
204 body: std::mem::take(&mut self.response.body),
205 headers: self.response.headers.clone(),
206 };
207
208 {
210 let mut data = self.cache.lock().unwrap();
211 data.keys.insert(uri_key.clone(), key.clone());
212 data.responses.insert(uri_key.clone(), response.clone());
213 }
214
215 let entry = DiskCacheEntry::from_parts(&key, &response);
217 if let Ok(serialized) = serde_json::to_vec(&entry) {
218 let _ = cacache::write_sync(&self.cache_path, &uri_key, &serialized);
219 }
220 }
221}