1use std::collections::HashMap;
13use std::io;
14use std::sync::{Arc, RwLock};
15
16pub trait BlobStore: Send + Sync + 'static {
22 fn load(&self, index_name: &str, file_name: &str) -> io::Result<Vec<u8>>;
24
25 fn save(&self, index_name: &str, file_name: &str, data: &[u8]) -> io::Result<()>;
27
28 fn delete(&self, index_name: &str, file_name: &str) -> io::Result<()>;
30
31 fn exists(&self, index_name: &str, file_name: &str) -> io::Result<bool>;
33
34 fn list(&self, index_name: &str) -> io::Result<Vec<String>>;
36
37 fn blob_len(&self, _index_name: &str, _file_name: &str) -> io::Result<Option<u64>> {
42 Ok(None)
43 }
44
45 fn load_range(
51 &self,
52 _index_name: &str,
53 _file_name: &str,
54 _range: std::ops::Range<u64>,
55 ) -> io::Result<Option<Vec<u8>>> {
56 Ok(None)
57 }
58}
59
60impl<T: BlobStore + ?Sized> BlobStore for std::sync::Arc<T> {
64 fn load(&self, index_name: &str, file_name: &str) -> io::Result<Vec<u8>> {
65 (**self).load(index_name, file_name)
66 }
67 fn save(&self, index_name: &str, file_name: &str, data: &[u8]) -> io::Result<()> {
68 (**self).save(index_name, file_name, data)
69 }
70 fn delete(&self, index_name: &str, file_name: &str) -> io::Result<()> {
71 (**self).delete(index_name, file_name)
72 }
73 fn exists(&self, index_name: &str, file_name: &str) -> io::Result<bool> {
74 (**self).exists(index_name, file_name)
75 }
76 fn list(&self, index_name: &str) -> io::Result<Vec<String>> {
77 (**self).list(index_name)
78 }
79 fn blob_len(&self, index_name: &str, file_name: &str) -> io::Result<Option<u64>> {
80 (**self).blob_len(index_name, file_name)
81 }
82 fn load_range(
83 &self,
84 index_name: &str,
85 file_name: &str,
86 range: std::ops::Range<u64>,
87 ) -> io::Result<Option<Vec<u8>>> {
88 (**self).load_range(index_name, file_name, range)
89 }
90}
91
92type MemFiles = Arc<RwLock<HashMap<String, HashMap<String, Vec<u8>>>>>;
94
95#[derive(Debug, Clone)]
97pub struct MemBlobStore {
98 data: MemFiles,
99}
100
101impl MemBlobStore {
102 pub fn new() -> Self {
103 Self {
104 data: Arc::new(RwLock::new(HashMap::new())),
105 }
106 }
107}
108
109impl Default for MemBlobStore {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl BlobStore for MemBlobStore {
116 fn blob_len(&self, index_name: &str, file_name: &str) -> io::Result<Option<u64>> {
117 Ok(self.data.read().unwrap()
118 .get(index_name)
119 .and_then(|files| files.get(file_name))
120 .map(|b| b.len() as u64))
121 }
122
123 fn load_range(
124 &self,
125 index_name: &str,
126 file_name: &str,
127 range: std::ops::Range<u64>,
128 ) -> io::Result<Option<Vec<u8>>> {
129 Ok(self.data.read().unwrap()
130 .get(index_name)
131 .and_then(|files| files.get(file_name))
132 .map(|b| b[range.start as usize..(range.end as usize).min(b.len())].to_vec()))
133 }
134
135 fn load(&self, index_name: &str, file_name: &str) -> io::Result<Vec<u8>> {
136 let guard = self.data.read().map_err(|_| io::Error::other("lock poisoned"))?;
137 guard
138 .get(index_name)
139 .and_then(|files| files.get(file_name))
140 .cloned()
141 .ok_or_else(|| {
142 io::Error::new(
143 io::ErrorKind::NotFound,
144 format!("{index_name}/{file_name} not found"),
145 )
146 })
147 }
148
149 fn save(&self, index_name: &str, file_name: &str, data: &[u8]) -> io::Result<()> {
150 let mut guard = self.data.write().map_err(|_| io::Error::other("lock poisoned"))?;
151 guard
152 .entry(index_name.to_string())
153 .or_default()
154 .insert(file_name.to_string(), data.to_vec());
155 Ok(())
156 }
157
158 fn delete(&self, index_name: &str, file_name: &str) -> io::Result<()> {
159 let mut guard = self.data.write().map_err(|_| io::Error::other("lock poisoned"))?;
160 if let Some(files) = guard.get_mut(index_name) {
161 files.remove(file_name);
162 }
163 Ok(())
164 }
165
166 fn exists(&self, index_name: &str, file_name: &str) -> io::Result<bool> {
167 let guard = self.data.read().map_err(|_| io::Error::other("lock poisoned"))?;
168 Ok(guard
169 .get(index_name)
170 .is_some_and(|files| files.contains_key(file_name)))
171 }
172
173 fn list(&self, index_name: &str) -> io::Result<Vec<String>> {
174 let guard = self.data.read().map_err(|_| io::Error::other("lock poisoned"))?;
175 Ok(guard
176 .get(index_name)
177 .map(|files| files.keys().cloned().collect())
178 .unwrap_or_default())
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_mem_blob_store_roundtrip() {
188 let store = MemBlobStore::new();
189 store.save("idx1", "file.bin", b"hello world").unwrap();
190 assert!(store.exists("idx1", "file.bin").unwrap());
191 assert!(!store.exists("idx1", "other.bin").unwrap());
192 let loaded = store.load("idx1", "file.bin").unwrap();
193 assert_eq!(loaded, b"hello world");
194 store.delete("idx1", "file.bin").unwrap();
195 assert!(!store.exists("idx1", "file.bin").unwrap());
196 }
197
198 #[test]
199 fn test_mem_blob_store_multiple_indexes() {
200 let store = MemBlobStore::new();
201 store.save("idx1", "a.bin", b"aaa").unwrap();
202 store.save("idx2", "a.bin", b"xxx").unwrap();
203 assert_eq!(store.load("idx1", "a.bin").unwrap(), b"aaa");
204 assert_eq!(store.load("idx2", "a.bin").unwrap(), b"xxx");
205 }
206}