1use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::sync::Mutex;
7
8use bytes::Bytes;
9use sha2::{Digest, Sha256};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct CachedResponse {
15 pub etag: String,
17 pub body: Bytes,
19}
20
21pub trait ResponseCache: Send + Sync {
23 fn get(&self, key: &str) -> Option<CachedResponse>;
25 fn set(&self, key: &str, response: CachedResponse);
27 fn invalidate(&self, key: &str);
29 fn clear(&self);
31}
32
33#[derive(Debug, Default)]
35pub struct InMemoryCache {
36 entries: Mutex<HashMap<String, CachedResponse>>,
37}
38
39impl InMemoryCache {
40 pub fn new() -> InMemoryCache {
42 InMemoryCache::default()
43 }
44}
45
46impl ResponseCache for InMemoryCache {
47 fn get(&self, key: &str) -> Option<CachedResponse> {
48 self.entries
49 .lock()
50 .unwrap_or_else(std::sync::PoisonError::into_inner)
51 .get(key)
52 .cloned()
53 }
54
55 fn set(&self, key: &str, response: CachedResponse) {
56 self.entries
57 .lock()
58 .unwrap_or_else(std::sync::PoisonError::into_inner)
59 .insert(key.to_string(), response);
60 }
61
62 fn invalidate(&self, key: &str) {
63 self.entries
64 .lock()
65 .unwrap_or_else(std::sync::PoisonError::into_inner)
66 .remove(key);
67 }
68
69 fn clear(&self) {
70 self.entries
71 .lock()
72 .unwrap_or_else(std::sync::PoisonError::into_inner)
73 .clear();
74 }
75}
76
77#[derive(Debug)]
81pub struct FileCache {
82 directory: PathBuf,
83 lock: Mutex<()>,
84}
85
86impl FileCache {
87 pub fn new(directory: impl Into<PathBuf>) -> FileCache {
89 FileCache {
90 directory: directory.into(),
91 lock: Mutex::new(()),
92 }
93 }
94
95 fn etags(&self) -> HashMap<String, String> {
96 fs::read(self.directory.join("etags.json"))
97 .ok()
98 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
99 .unwrap_or_default()
100 }
101
102 fn write_etags(&self, etags: &HashMap<String, String>) {
103 if let Ok(json) = serde_json::to_vec_pretty(etags) {
104 write_private(&self.directory.join("etags.json"), &json);
105 }
106 }
107
108 fn body_path(&self, key: &str) -> PathBuf {
109 self.directory.join("responses").join(format!("{key}.body"))
110 }
111}
112
113impl ResponseCache for FileCache {
114 fn get(&self, key: &str) -> Option<CachedResponse> {
115 let _guard = self
116 .lock
117 .lock()
118 .unwrap_or_else(std::sync::PoisonError::into_inner);
119 let etag = self.etags().get(key).cloned()?;
120 let body = fs::read(self.body_path(key)).ok()?;
121 Some(CachedResponse {
122 etag,
123 body: Bytes::from(body),
124 })
125 }
126
127 fn set(&self, key: &str, response: CachedResponse) {
128 let _guard = self
129 .lock
130 .lock()
131 .unwrap_or_else(std::sync::PoisonError::into_inner);
132 write_private(&self.body_path(key), &response.body);
133 let mut etags = self.etags();
134 etags.insert(key.to_string(), response.etag);
135 self.write_etags(&etags);
136 }
137
138 fn invalidate(&self, key: &str) {
139 let _guard = self
140 .lock
141 .lock()
142 .unwrap_or_else(std::sync::PoisonError::into_inner);
143 let _ = fs::remove_file(self.body_path(key));
144 let mut etags = self.etags();
145 etags.remove(key);
146 self.write_etags(&etags);
147 }
148
149 fn clear(&self) {
153 let _guard = self
154 .lock
155 .lock()
156 .unwrap_or_else(std::sync::PoisonError::into_inner);
157 let _ = fs::remove_dir_all(self.directory.join("responses"));
158 let _ = fs::remove_file(self.directory.join("etags.json"));
159 }
160}
161
162fn write_private(path: &Path, bytes: &[u8]) {
163 let Some(parent) = path.parent() else { return };
164 if fs::create_dir_all(parent).is_err() {
165 return;
166 }
167 let temporary = path.with_extension("tmp");
168 if fs::write(&temporary, bytes).is_ok() {
169 restrict_permissions(parent, &temporary);
170 let _ = fs::rename(&temporary, path);
171 }
172}
173
174#[cfg(unix)]
175fn restrict_permissions(directory: &Path, file: &Path) {
176 use std::os::unix::fs::PermissionsExt;
177 let _ = fs::set_permissions(directory, fs::Permissions::from_mode(0o700));
178 let _ = fs::set_permissions(file, fs::Permissions::from_mode(0o600));
179}
180
181#[cfg(not(unix))]
182fn restrict_permissions(_directory: &Path, _file: &Path) {}
183
184pub fn cache_key(url: &str, credential: &str) -> String {
187 let mut credential_hash = String::new();
188 if !credential.is_empty() {
189 credential_hash = hex(&Sha256::digest(credential.as_bytes())[..8]);
190 }
191 hex(&Sha256::digest(
192 format!("{url}:{credential_hash}").as_bytes(),
193 ))
194}
195
196fn hex(bytes: &[u8]) -> String {
197 use std::fmt::Write;
198 bytes.iter().fold(String::new(), |mut out, byte| {
199 let _ = write!(out, "{byte:02x}");
200 out
201 })
202}