fob_bundler/cache/
storage.rs1use std::path::Path;
7
8use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
9
10use super::key::CacheKey;
11use super::serialize::CachedBuild;
12
13const CACHE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("cache");
15
16const METADATA_TABLE: TableDefinition<&str, &str> = TableDefinition::new("metadata");
18
19#[derive(Debug, thiserror::Error)]
21pub enum CacheError {
22 #[error("cache miss")]
24 CacheMiss,
25
26 #[error("cache database error: {0}")]
28 DatabaseError(String),
29
30 #[error("serialization error: {0}")]
32 SerializationError(String),
33
34 #[error("deserialization error: {0}")]
36 DeserializationError(String),
37
38 #[error("io error: {0}")]
40 IoError(#[from] std::io::Error),
41
42 #[error("cache version mismatch: expected {expected}, found {found}")]
44 VersionMismatch { expected: u32, found: u32 },
45
46 #[error("cache corrupted: {0}")]
48 Corrupted(String),
49
50 #[error("invalid data: {0}")]
52 InvalidData(String),
53}
54
55impl From<redb::Error> for CacheError {
56 fn from(err: redb::Error) -> Self {
57 CacheError::DatabaseError(err.to_string())
58 }
59}
60
61impl From<redb::DatabaseError> for CacheError {
62 fn from(err: redb::DatabaseError) -> Self {
63 CacheError::DatabaseError(err.to_string())
64 }
65}
66
67impl From<redb::TableError> for CacheError {
68 fn from(err: redb::TableError) -> Self {
69 CacheError::DatabaseError(err.to_string())
70 }
71}
72
73impl From<redb::TransactionError> for CacheError {
74 fn from(err: redb::TransactionError) -> Self {
75 CacheError::DatabaseError(err.to_string())
76 }
77}
78
79impl From<redb::StorageError> for CacheError {
80 fn from(err: redb::StorageError) -> Self {
81 CacheError::DatabaseError(err.to_string())
82 }
83}
84
85impl From<redb::CommitError> for CacheError {
86 fn from(err: redb::CommitError) -> Self {
87 CacheError::DatabaseError(err.to_string())
88 }
89}
90
91pub struct CacheStore {
93 db: Database,
94}
95
96impl CacheStore {
97 pub fn open(cache_dir: &Path) -> Result<Self, CacheError> {
102 std::fs::create_dir_all(cache_dir)?;
104
105 let db_path = cache_dir.join("cache.redb");
106
107 let db = Database::create(&db_path)?;
109
110 let write_txn = db.begin_write()?;
112 {
113 let _ = write_txn.open_table(CACHE_TABLE)?;
115 let _ = write_txn.open_table(METADATA_TABLE)?;
116 }
117 write_txn.commit()?;
118
119 Ok(Self { db })
120 }
121
122 pub fn get(&self, key: &CacheKey) -> Result<CachedBuild, CacheError> {
126 let read_txn = self.db.begin_read()?;
127 let table = read_txn.open_table(CACHE_TABLE)?;
128
129 let value = table.get(key.as_hex())?.ok_or(CacheError::CacheMiss)?;
130
131 let bytes = value.value();
132
133 let cached: CachedBuild = bincode::deserialize(bytes)
135 .map_err(|e| CacheError::DeserializationError(e.to_string()))?;
136
137 if !cached.metadata.is_compatible() {
139 return Err(CacheError::VersionMismatch {
140 expected: super::serialize::CACHE_FORMAT_VERSION,
141 found: cached.metadata.format_version,
142 });
143 }
144
145 Ok(cached)
146 }
147
148 pub fn put(&self, key: &CacheKey, build: &CachedBuild) -> Result<(), CacheError> {
150 let bytes =
152 bincode::serialize(build).map_err(|e| CacheError::SerializationError(e.to_string()))?;
153
154 let write_txn = self.db.begin_write()?;
156 {
157 let mut table = write_txn.open_table(CACHE_TABLE)?;
158 table.insert(key.as_hex(), bytes.as_slice())?;
159 }
160 write_txn.commit()?;
161
162 Ok(())
163 }
164
165 pub fn remove(&self, key: &CacheKey) -> Result<(), CacheError> {
167 let write_txn = self.db.begin_write()?;
168 {
169 let mut table = write_txn.open_table(CACHE_TABLE)?;
170 table.remove(key.as_hex())?;
171 }
172 write_txn.commit()?;
173
174 Ok(())
175 }
176
177 pub fn contains(&self, key: &CacheKey) -> Result<bool, CacheError> {
179 let read_txn = self.db.begin_read()?;
180 let table = read_txn.open_table(CACHE_TABLE)?;
181
182 Ok(table.get(key.as_hex())?.is_some())
183 }
184
185 pub fn clear(&self) -> Result<(), CacheError> {
187 let write_txn = self.db.begin_write()?;
188 {
189 write_txn.delete_table(CACHE_TABLE)?;
191 let _ = write_txn.open_table(CACHE_TABLE)?;
192 }
193 write_txn.commit()?;
194
195 Ok(())
196 }
197
198 pub fn len(&self) -> Result<usize, CacheError> {
200 let read_txn = self.db.begin_read()?;
201 let table = read_txn.open_table(CACHE_TABLE)?;
202
203 let count = table.iter()?.count();
205 Ok(count)
206 }
207
208 pub fn is_empty(&self) -> Result<bool, CacheError> {
210 Ok(self.len()? == 0)
211 }
212
213 pub fn set_metadata(&self, key: &str, value: &str) -> Result<(), CacheError> {
215 let write_txn = self.db.begin_write()?;
216 {
217 let mut table = write_txn.open_table(METADATA_TABLE)?;
218 table.insert(key, value)?;
219 }
220 write_txn.commit()?;
221
222 Ok(())
223 }
224
225 pub fn get_metadata(&self, key: &str) -> Result<Option<String>, CacheError> {
227 let read_txn = self.db.begin_read()?;
228 let table = read_txn.open_table(METADATA_TABLE)?;
229
230 Ok(table.get(key)?.map(|v| v.value().to_string()))
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use tempfile::TempDir;
238
239 fn create_test_store() -> (CacheStore, TempDir) {
240 let dir = TempDir::new().unwrap();
241 let store = CacheStore::open(dir.path()).unwrap();
242 (store, dir)
243 }
244
245 #[test]
246 fn test_store_open_creates_directory() {
247 let dir = TempDir::new().unwrap();
248 let cache_dir = dir.path().join("new_cache");
249
250 let _store = CacheStore::open(&cache_dir).unwrap();
251
252 assert!(cache_dir.exists());
253 assert!(cache_dir.join("cache.redb").exists());
254 }
255
256 #[test]
257 fn test_cache_miss() {
258 let (store, _dir) = create_test_store();
259
260 let key = CacheKey::from_hex("nonexistent");
261 let result = store.get(&key);
262
263 assert!(matches!(result, Err(CacheError::CacheMiss)));
264 }
265
266 #[test]
267 fn test_contains() {
268 let (store, _dir) = create_test_store();
269
270 let key = CacheKey::from_hex("test_key");
271
272 assert!(!store.contains(&key).unwrap());
273 }
274
275 #[test]
276 fn test_metadata() {
277 let (store, _dir) = create_test_store();
278
279 store.set_metadata("version", "1.0").unwrap();
280 let value = store.get_metadata("version").unwrap();
281
282 assert_eq!(value, Some("1.0".to_string()));
283 }
284
285 #[test]
286 fn test_clear() {
287 let (store, _dir) = create_test_store();
288
289 assert!(store.is_empty().unwrap());
291
292 store.clear().unwrap();
294 assert!(store.is_empty().unwrap());
295 }
296}