Skip to main content

alimentar/backend/
mod.rs

1//! Storage backends for alimentar.
2//!
3//! Backends provide abstracted storage operations for datasets and registries.
4//! The [`StorageBackend`] trait defines the interface, with implementations
5//! for local filesystem, S3-compatible storage, and in-memory storage.
6
7#[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
26/// A storage backend for reading and writing data.
27///
28/// Backends abstract the underlying storage mechanism, allowing datasets
29/// and registries to work with local files, cloud storage, or in-memory
30/// buffers using the same interface.
31///
32/// # Async Design
33///
34/// All operations are synchronous for now (v0.1). Future versions will
35/// add async variants behind the `tokio-runtime` feature flag.
36pub trait StorageBackend: Send + Sync {
37    /// Lists all keys with the given prefix.
38    ///
39    /// Returns a vector of key names (relative to the backend root).
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if the listing operation fails.
44    fn list(&self, prefix: &str) -> Result<Vec<String>>;
45
46    /// Reads data from the given key.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the key does not exist or cannot be read.
51    fn get(&self, key: &str) -> Result<Bytes>;
52
53    /// Writes data to the given key.
54    ///
55    /// Creates parent directories/prefixes as needed.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the write fails.
60    fn put(&self, key: &str, data: Bytes) -> Result<()>;
61
62    /// Deletes the given key.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if the key cannot be deleted.
67    fn delete(&self, key: &str) -> Result<()>;
68
69    /// Checks if the given key exists.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the existence check fails.
74    fn exists(&self, key: &str) -> Result<bool>;
75
76    /// Returns the size of the data at the given key in bytes.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the key does not exist.
81    fn size(&self, key: &str) -> Result<u64>;
82}
83
84/// Configuration for storage backends.
85#[derive(Debug, Clone)]
86pub enum BackendConfig {
87    /// Local filesystem backend.
88    Local {
89        /// Root directory for storage.
90        root: std::path::PathBuf,
91    },
92    /// In-memory backend (for testing or WASM).
93    Memory,
94    /// S3-compatible backend (requires `s3` feature).
95    #[cfg(feature = "s3")]
96    S3 {
97        /// Bucket name.
98        bucket: String,
99        /// AWS region.
100        region: String,
101        /// Custom endpoint URL (None = AWS, Some = MinIO/Ceph/etc).
102        endpoint: Option<String>,
103        /// Credential source for authentication.
104        credentials: CredentialSource,
105    },
106}
107
108impl BackendConfig {
109    /// Creates a local backend configuration.
110    pub fn local(root: impl Into<std::path::PathBuf>) -> Self {
111        Self::Local { root: root.into() }
112    }
113
114    /// Creates an in-memory backend configuration.
115    pub fn memory() -> Self {
116        Self::Memory
117    }
118
119    /// Creates an S3 backend configuration for AWS.
120    #[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    /// Creates an S3 backend configuration for a custom endpoint (MinIO, etc.).
131    #[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
147/// Creates a storage backend from configuration.
148///
149/// # Errors
150///
151/// Returns an error if the backend cannot be created.
152pub 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        // Test put and get
219        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        // Test exists
231        let exists = backend
232            .exists("test_key")
233            .ok()
234            .unwrap_or_else(|| panic!("Should check exists"));
235        assert!(exists);
236
237        // Test size
238        let size = backend
239            .size("test_key")
240            .ok()
241            .unwrap_or_else(|| panic!("Should get size"));
242        assert_eq!(size, 10);
243
244        // Test list
245        let list = backend
246            .list("")
247            .ok()
248            .unwrap_or_else(|| panic!("Should list"));
249        assert_eq!(list.len(), 1);
250
251        // Test delete
252        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        // Basic operations
275        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}