1use anyhow::Result;
2use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf};
3use tracing::{debug, error, trace};
4
5#[derive(Debug, Clone, Default)]
6pub struct HashDigest {
7 pub parts: HashMap<String, String>,
8 pub digest: Option<md5::Digest>,
9}
10
11pub trait CacheKey {
12 fn hexdigest(&self) -> String;
13}
14
15impl HashDigest {
16 pub fn new() -> Self {
17 Self::default()
18 }
19
20 pub fn add(&mut self, key: &str, value: &str) {
21 match self.digest {
22 Some(_) => panic!("Cannot add to finalized cache key"),
23 None => self.parts.insert(key.to_string(), value.to_string()),
24 };
25 }
26
27 pub fn finalize(&mut self) {
28 let mut context = md5::Context::new();
29
30 let sorted_keys = {
31 let mut keys: Vec<&String> = self.parts.keys().collect();
32 keys.sort();
33 keys
34 };
35
36 for key in sorted_keys {
37 let value = self.parts.get(key).unwrap();
38
39 context.consume(key.as_bytes());
40 context.consume(value.as_bytes());
41 }
42
43 self.digest = Some(context.compute());
44 }
45}
46
47impl CacheKey for HashDigest {
48 fn hexdigest(&self) -> String {
49 match &self.digest {
50 Some(digest) => format!("{:x}", digest),
51 None => panic!("Cannot get hexdigest of unfinalized cache key"),
52 }
53 }
54}
55
56pub trait Cache: Debug + Send + Sync + 'static {
57 fn read(&self, key: &dyn CacheKey) -> Result<Option<Vec<u8>>>;
58 fn write(&self, key: &dyn CacheKey, value: &[u8]) -> Result<()>;
59 fn path(&self, key: &dyn CacheKey) -> PathBuf;
60 fn clear(&self) -> Result<()>;
61 fn clone_box(&self) -> Box<dyn Cache>;
62}
63
64impl Clone for Box<dyn Cache> {
65 fn clone(&self) -> Box<dyn Cache> {
66 self.clone_box()
67 }
68}
69
70#[derive(Debug, Clone, Default)]
71pub struct NullCache {}
72
73impl Cache for NullCache {
74 fn read(&self, _key: &dyn CacheKey) -> Result<Option<Vec<u8>>> {
75 Ok(None)
76 }
77
78 fn write(&self, _key: &dyn CacheKey, _value: &[u8]) -> Result<()> {
79 Ok(())
80 }
81
82 fn path(&self, _key: &dyn CacheKey) -> PathBuf {
83 PathBuf::new()
84 }
85
86 fn clear(&self) -> Result<()> {
87 Ok(())
88 }
89
90 fn clone_box(&self) -> Box<dyn Cache> {
91 Box::new(self.clone())
92 }
93}
94
95impl NullCache {
96 pub fn new() -> Self {
97 Self::default()
98 }
99}
100
101#[derive(Debug, Clone)]
102pub struct FilesystemCache {
103 pub root: PathBuf,
104 pub extension: String,
105}
106
107impl FilesystemCache {
108 pub fn new(root: PathBuf, extension: &str) -> Self {
109 Self {
110 root,
111 extension: extension.to_string(),
112 }
113 }
114}
115
116impl Cache for FilesystemCache {
117 fn read(&self, key: &dyn CacheKey) -> Result<Option<Vec<u8>>> {
118 let path = self.path(key);
119
120 trace!("FilesystemCache read: {}", path.display());
121 let result = fs::read(path);
122
123 match result {
124 Ok(contents) => {
125 debug!("Cache hit: {:?}", &key.hexdigest());
126 Ok(Some(contents))
127 }
128 Err(error) => match error.kind() {
129 std::io::ErrorKind::NotFound => Ok(None),
130 _ => Err(error.into()),
131 },
132 }
133 }
134
135 fn write(&self, key: &dyn CacheKey, value: &[u8]) -> Result<()> {
136 let path = self.path(key);
137 let directory = path.parent();
138
139 if directory.is_some()
140 && !directory.unwrap().exists()
141 && std::fs::create_dir_all(directory.unwrap()).is_err()
142 {
143 error!(
144 "Failed to create directory: {}",
145 directory.unwrap().display()
146 );
147 }
148
149 trace!(
150 "FilesystemCache write: {} ({} bytes)",
151 path.display(),
152 value.len()
153 );
154 fs::write(path, value)?;
155 Ok(())
156 }
157
158 #[allow(unused)]
159 fn clear(&self) -> Result<()> {
160 fs::remove_dir_all(&self.root)?;
161 Ok(())
162 }
163
164 fn path(&self, key: &dyn CacheKey) -> PathBuf {
165 let mut path = self.root.clone();
166 path.push(key.hexdigest());
167 path.set_extension(&self.extension);
168 path
169 }
170
171 fn clone_box(&self) -> Box<dyn Cache> {
172 Box::new(self.clone())
173 }
174}
175
176#[cfg(test)]
177mod test {
178 use super::*;
179
180 #[test]
181 fn cache_key_equal() {
182 let mut key_a = HashDigest::new();
183 key_a.add("foo", "bar");
184 key_a.finalize();
185
186 let mut key_b = HashDigest::new();
187 key_b.add("foo", "bar");
188 key_b.finalize();
189
190 assert_eq!(key_a.hexdigest(), key_b.hexdigest());
191 }
192
193 #[test]
194 fn cache_key_not_equal() {
195 let mut key_a = HashDigest::new();
196 key_a.add("foo", "bar");
197 key_a.finalize();
198
199 let mut key_b = HashDigest::new();
200 key_b.add("foo", "bar");
201 key_b.add("baz", "bop");
202 key_b.finalize();
203
204 assert_ne!(key_a.hexdigest(), key_b.hexdigest());
205 }
206
207 #[test]
208 fn filesystem_cache() {
209 let tmpdir = tempfile::tempdir().unwrap();
210 let cache = FilesystemCache::new(tmpdir.into_path(), "bytes");
211
212 let mut digest = HashDigest::new();
213 digest.add("foo", "bar");
214 digest.finalize();
215
216 cache.write(&digest, "Test 123".as_bytes()).unwrap();
217 assert_eq!("Test 123".as_bytes(), cache.read(&digest).unwrap().unwrap());
218 }
219}