1#[cfg(feature = "http")]
8pub mod http;
9#[cfg(feature = "local")]
10pub mod local;
11pub mod memory;
12#[cfg(feature = "s3")]
13pub mod s3;
14
15use bytes::Bytes;
16#[cfg(feature = "http")]
17pub use http::{HttpBackend, RangeHttpBackend};
18#[cfg(feature = "local")]
19pub use local::LocalBackend;
20pub use memory::MemoryBackend;
21#[cfg(feature = "s3")]
22pub use s3::{CredentialSource, S3Backend};
23
24use crate::error::Result;
25
26pub trait StorageBackend: Send + Sync {
37 fn list(&self, prefix: &str) -> Result<Vec<String>>;
45
46 fn get(&self, key: &str) -> Result<Bytes>;
52
53 fn put(&self, key: &str, data: Bytes) -> Result<()>;
61
62 fn delete(&self, key: &str) -> Result<()>;
68
69 fn exists(&self, key: &str) -> Result<bool>;
75
76 fn size(&self, key: &str) -> Result<u64>;
82}
83
84#[derive(Debug, Clone)]
86pub enum BackendConfig {
87 Local {
89 root: std::path::PathBuf,
91 },
92 Memory,
94 #[cfg(feature = "s3")]
96 S3 {
97 bucket: String,
99 region: String,
101 endpoint: Option<String>,
103 credentials: CredentialSource,
105 },
106}
107
108impl BackendConfig {
109 pub fn local(root: impl Into<std::path::PathBuf>) -> Self {
111 Self::Local { root: root.into() }
112 }
113
114 pub fn memory() -> Self {
116 Self::Memory
117 }
118
119 #[cfg(feature = "s3")]
121 pub fn s3_aws(bucket: impl Into<String>, region: impl Into<String>) -> Self {
122 Self::S3 {
123 bucket: bucket.into(),
124 region: region.into(),
125 endpoint: None,
126 credentials: CredentialSource::Environment,
127 }
128 }
129
130 #[cfg(feature = "s3")]
132 pub fn s3_custom(
133 bucket: impl Into<String>,
134 region: impl Into<String>,
135 endpoint: impl Into<String>,
136 credentials: CredentialSource,
137 ) -> Self {
138 Self::S3 {
139 bucket: bucket.into(),
140 region: region.into(),
141 endpoint: Some(endpoint.into()),
142 credentials,
143 }
144 }
145}
146
147pub fn create_backend(config: BackendConfig) -> Result<Box<dyn StorageBackend>> {
153 match config {
154 #[cfg(feature = "local")]
155 BackendConfig::Local { root } => Ok(Box::new(LocalBackend::new(root)?)),
156 #[cfg(not(feature = "local"))]
157 BackendConfig::Local { .. } => Err(crate::error::Error::invalid_config(
158 "Local backend requires 'local' feature",
159 )),
160 BackendConfig::Memory => Ok(Box::new(MemoryBackend::new())),
161 #[cfg(feature = "s3")]
162 BackendConfig::S3 {
163 bucket,
164 region,
165 endpoint,
166 credentials,
167 } => Ok(Box::new(S3Backend::new(
168 bucket,
169 region,
170 endpoint,
171 credentials,
172 )?)),
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_backend_config_local() {
182 let config = BackendConfig::local("/tmp/test");
183 if let BackendConfig::Local { root } = config {
184 assert_eq!(root, std::path::PathBuf::from("/tmp/test"));
185 } else {
186 panic!("Expected Local config");
187 }
188 }
189
190 #[test]
191 fn test_backend_config_memory() {
192 let config = BackendConfig::memory();
193 assert!(matches!(config, BackendConfig::Memory));
194 }
195
196 #[test]
197 fn test_create_memory_backend() {
198 let backend = create_backend(BackendConfig::Memory);
199 assert!(backend.is_ok());
200 }
201
202 #[cfg(feature = "local")]
203 #[test]
204 fn test_create_local_backend() {
205 let temp_dir = tempfile::tempdir()
206 .ok()
207 .unwrap_or_else(|| panic!("Should create temp dir"));
208 let backend = create_backend(BackendConfig::local(temp_dir.path()));
209 assert!(backend.is_ok());
210 }
211
212 #[test]
213 fn test_create_memory_backend_operations() {
214 let backend = create_backend(BackendConfig::Memory)
215 .ok()
216 .unwrap_or_else(|| panic!("Should create backend"));
217
218 backend
220 .put("test_key", bytes::Bytes::from("test_value"))
221 .ok()
222 .unwrap_or_else(|| panic!("Should put"));
223
224 let data = backend
225 .get("test_key")
226 .ok()
227 .unwrap_or_else(|| panic!("Should get"));
228 assert_eq!(data, bytes::Bytes::from("test_value"));
229
230 let exists = backend
232 .exists("test_key")
233 .ok()
234 .unwrap_or_else(|| panic!("Should check exists"));
235 assert!(exists);
236
237 let size = backend
239 .size("test_key")
240 .ok()
241 .unwrap_or_else(|| panic!("Should get size"));
242 assert_eq!(size, 10);
243
244 let list = backend
246 .list("")
247 .ok()
248 .unwrap_or_else(|| panic!("Should list"));
249 assert_eq!(list.len(), 1);
250
251 backend
253 .delete("test_key")
254 .ok()
255 .unwrap_or_else(|| panic!("Should delete"));
256
257 let exists_after = backend
258 .exists("test_key")
259 .ok()
260 .unwrap_or_else(|| panic!("Should check exists"));
261 assert!(!exists_after);
262 }
263
264 #[cfg(feature = "local")]
265 #[test]
266 fn test_create_local_backend_operations() {
267 let temp_dir = tempfile::tempdir()
268 .ok()
269 .unwrap_or_else(|| panic!("Should create temp dir"));
270 let backend = create_backend(BackendConfig::local(temp_dir.path()))
271 .ok()
272 .unwrap_or_else(|| panic!("Should create backend"));
273
274 backend
276 .put("data.txt", bytes::Bytes::from("content"))
277 .ok()
278 .unwrap_or_else(|| panic!("Should put"));
279
280 let exists = backend
281 .exists("data.txt")
282 .ok()
283 .unwrap_or_else(|| panic!("Should check exists"));
284 assert!(exists);
285 }
286
287 #[test]
288 fn test_backend_config_debug() {
289 let config = BackendConfig::local("/tmp/test");
290 let debug_str = format!("{:?}", config);
291 assert!(debug_str.contains("Local"));
292
293 let config2 = BackendConfig::memory();
294 let debug_str2 = format!("{:?}", config2);
295 assert!(debug_str2.contains("Memory"));
296 }
297
298 #[test]
299 fn test_backend_config_clone() {
300 let config = BackendConfig::local("/tmp/test");
301 let cloned = config;
302
303 if let BackendConfig::Local { root } = cloned {
304 assert_eq!(root, std::path::PathBuf::from("/tmp/test"));
305 } else {
306 panic!("Expected Local config");
307 }
308 }
309}