liter_llm_proxy/
file_store.rs1use std::str::FromStr;
2
3use bytes::Bytes;
4use opendal::Operator;
5
6use crate::config::FileStorageConfig;
7
8pub struct FileStore {
13 operator: Operator,
14 prefix: String,
15}
16
17impl FileStore {
18 pub fn from_config(config: &FileStorageConfig) -> Result<Self, String> {
29 let scheme = opendal::Scheme::from_str(&config.backend)
30 .map_err(|e| format!("unknown storage backend '{}': {e}", config.backend))?;
31
32 let operator = Operator::via_iter(scheme, config.backend_config.clone())
33 .map_err(|e| format!("failed to build storage operator for '{}': {e}", config.backend))?;
34
35 Ok(Self {
36 operator,
37 prefix: config.prefix.clone(),
38 })
39 }
40
41 fn full_path(&self, key: &str) -> String {
43 format!("{}{key}", self.prefix)
44 }
45
46 pub async fn write(&self, key: &str, data: Bytes) -> Result<(), String> {
52 let path = self.full_path(key);
53 self.operator
54 .write(&path, data)
55 .await
56 .map(|_| ())
57 .map_err(|e| format!("failed to write '{path}': {e}"))
58 }
59
60 pub async fn read(&self, key: &str) -> Result<Bytes, String> {
66 let path = self.full_path(key);
67 let buf = self
68 .operator
69 .read(&path)
70 .await
71 .map_err(|e| format!("failed to read '{path}': {e}"))?;
72 Ok(buf.to_bytes())
73 }
74
75 pub async fn delete(&self, key: &str) -> Result<(), String> {
81 let path = self.full_path(key);
82 self.operator
83 .delete(&path)
84 .await
85 .map_err(|e| format!("failed to delete '{path}': {e}"))
86 }
87
88 pub async fn list(&self, prefix: Option<&str>) -> Result<Vec<String>, String> {
96 let scan_prefix = match prefix {
97 Some(p) => format!("{}{p}", self.prefix),
98 None => self.prefix.clone(),
99 };
100 let entries = self
101 .operator
102 .list(&scan_prefix)
103 .await
104 .map_err(|e| format!("failed to list '{scan_prefix}': {e}"))?;
105
106 let store_prefix_len = self.prefix.len();
107 let keys: Vec<String> = entries
108 .into_iter()
109 .filter(|entry| !entry.path().ends_with('/'))
110 .filter_map(|entry| {
111 let path = entry.path();
112 if path.len() > store_prefix_len {
113 Some(path[store_prefix_len..].to_string())
114 } else {
115 None
116 }
117 })
118 .collect();
119
120 Ok(keys)
121 }
122
123 pub async fn exists(&self, key: &str) -> Result<bool, String> {
129 let path = self.full_path(key);
130 self.operator
131 .exists(&path)
132 .await
133 .map_err(|e| format!("failed to check existence of '{path}': {e}"))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::config::FileStorageConfig;
141
142 fn memory_store() -> FileStore {
143 let config = FileStorageConfig::default();
144 FileStore::from_config(&config).expect("memory backend should build")
145 }
146
147 #[tokio::test]
148 async fn write_then_read_returns_same_data() {
149 let store = memory_store();
150 let data = Bytes::from_static(b"hello world");
151 store
152 .write("test.txt", data.clone())
153 .await
154 .expect("write should succeed");
155 let read_data = store.read("test.txt").await.expect("read should succeed");
156 assert_eq!(read_data, data);
157 }
158
159 #[tokio::test]
160 async fn read_nonexistent_key_returns_error() {
161 let store = memory_store();
162 let result = store.read("does-not-exist.txt").await;
163 assert!(result.is_err());
164 }
165
166 #[tokio::test]
167 async fn delete_then_exists_returns_false() {
168 let store = memory_store();
169 store
170 .write("to-delete.txt", Bytes::from_static(b"data"))
171 .await
172 .expect("write should succeed");
173 assert!(
174 store.exists("to-delete.txt").await.expect("exists check"),
175 "key should exist after write"
176 );
177 store.delete("to-delete.txt").await.expect("delete should succeed");
178 assert!(
179 !store.exists("to-delete.txt").await.expect("exists check"),
180 "key should not exist after delete"
181 );
182 }
183
184 #[tokio::test]
185 async fn list_returns_written_keys() {
186 let store = memory_store();
187 store.write("a.txt", Bytes::from_static(b"aaa")).await.expect("write a");
188 store.write("b.txt", Bytes::from_static(b"bbb")).await.expect("write b");
189
190 let mut keys = store.list(None).await.expect("list should succeed");
191 keys.sort();
192 assert_eq!(keys, vec!["a.txt", "b.txt"]);
193 }
194
195 #[tokio::test]
196 async fn exists_returns_false_for_missing_key() {
197 let store = memory_store();
198 let result = store.exists("nope.txt").await.expect("exists check");
199 assert!(!result);
200 }
201
202 #[tokio::test]
203 async fn exists_returns_true_after_write() {
204 let store = memory_store();
205 store
206 .write("present.txt", Bytes::from_static(b"here"))
207 .await
208 .expect("write");
209 let result = store.exists("present.txt").await.expect("exists check");
210 assert!(result);
211 }
212
213 #[tokio::test]
214 async fn overwrite_replaces_data() {
215 let store = memory_store();
216 store
217 .write("file.txt", Bytes::from_static(b"original"))
218 .await
219 .expect("write");
220 store
221 .write("file.txt", Bytes::from_static(b"replaced"))
222 .await
223 .expect("overwrite");
224 let data = store.read("file.txt").await.expect("read");
225 assert_eq!(data, Bytes::from_static(b"replaced"));
226 }
227
228 #[test]
229 fn from_config_rejects_unknown_backend() {
230 let config = FileStorageConfig {
231 backend: "nonexistent_xyz".to_string(),
232 prefix: "test/".to_string(),
233 backend_config: std::collections::HashMap::new(),
234 };
235 let result = FileStore::from_config(&config);
236 assert!(result.is_err());
237 }
238}