#[cfg(feature = "http")]
pub mod http;
#[cfg(feature = "local")]
pub mod local;
pub mod memory;
#[cfg(feature = "s3")]
pub mod s3;
use bytes::Bytes;
#[cfg(feature = "http")]
pub use http::{HttpBackend, RangeHttpBackend};
#[cfg(feature = "local")]
pub use local::LocalBackend;
pub use memory::MemoryBackend;
#[cfg(feature = "s3")]
pub use s3::{CredentialSource, S3Backend};
use crate::error::Result;
pub trait StorageBackend: Send + Sync {
fn list(&self, prefix: &str) -> Result<Vec<String>>;
fn get(&self, key: &str) -> Result<Bytes>;
fn put(&self, key: &str, data: Bytes) -> Result<()>;
fn delete(&self, key: &str) -> Result<()>;
fn exists(&self, key: &str) -> Result<bool>;
fn size(&self, key: &str) -> Result<u64>;
}
#[derive(Debug, Clone)]
pub enum BackendConfig {
Local {
root: std::path::PathBuf,
},
Memory,
#[cfg(feature = "s3")]
S3 {
bucket: String,
region: String,
endpoint: Option<String>,
credentials: CredentialSource,
},
}
impl BackendConfig {
pub fn local(root: impl Into<std::path::PathBuf>) -> Self {
Self::Local { root: root.into() }
}
pub fn memory() -> Self {
Self::Memory
}
#[cfg(feature = "s3")]
pub fn s3_aws(bucket: impl Into<String>, region: impl Into<String>) -> Self {
Self::S3 {
bucket: bucket.into(),
region: region.into(),
endpoint: None,
credentials: CredentialSource::Environment,
}
}
#[cfg(feature = "s3")]
pub fn s3_custom(
bucket: impl Into<String>,
region: impl Into<String>,
endpoint: impl Into<String>,
credentials: CredentialSource,
) -> Self {
Self::S3 {
bucket: bucket.into(),
region: region.into(),
endpoint: Some(endpoint.into()),
credentials,
}
}
}
pub fn create_backend(config: BackendConfig) -> Result<Box<dyn StorageBackend>> {
match config {
#[cfg(feature = "local")]
BackendConfig::Local { root } => Ok(Box::new(LocalBackend::new(root)?)),
#[cfg(not(feature = "local"))]
BackendConfig::Local { .. } => Err(crate::error::Error::invalid_config(
"Local backend requires 'local' feature",
)),
BackendConfig::Memory => Ok(Box::new(MemoryBackend::new())),
#[cfg(feature = "s3")]
BackendConfig::S3 {
bucket,
region,
endpoint,
credentials,
} => Ok(Box::new(S3Backend::new(
bucket,
region,
endpoint,
credentials,
)?)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_config_local() {
let config = BackendConfig::local("/tmp/test");
if let BackendConfig::Local { root } = config {
assert_eq!(root, std::path::PathBuf::from("/tmp/test"));
} else {
panic!("Expected Local config");
}
}
#[test]
fn test_backend_config_memory() {
let config = BackendConfig::memory();
assert!(matches!(config, BackendConfig::Memory));
}
#[test]
fn test_create_memory_backend() {
let backend = create_backend(BackendConfig::Memory);
assert!(backend.is_ok());
}
#[cfg(feature = "local")]
#[test]
fn test_create_local_backend() {
let temp_dir = tempfile::tempdir()
.ok()
.unwrap_or_else(|| panic!("Should create temp dir"));
let backend = create_backend(BackendConfig::local(temp_dir.path()));
assert!(backend.is_ok());
}
#[test]
fn test_create_memory_backend_operations() {
let backend = create_backend(BackendConfig::Memory)
.ok()
.unwrap_or_else(|| panic!("Should create backend"));
backend
.put("test_key", bytes::Bytes::from("test_value"))
.ok()
.unwrap_or_else(|| panic!("Should put"));
let data = backend
.get("test_key")
.ok()
.unwrap_or_else(|| panic!("Should get"));
assert_eq!(data, bytes::Bytes::from("test_value"));
let exists = backend
.exists("test_key")
.ok()
.unwrap_or_else(|| panic!("Should check exists"));
assert!(exists);
let size = backend
.size("test_key")
.ok()
.unwrap_or_else(|| panic!("Should get size"));
assert_eq!(size, 10);
let list = backend
.list("")
.ok()
.unwrap_or_else(|| panic!("Should list"));
assert_eq!(list.len(), 1);
backend
.delete("test_key")
.ok()
.unwrap_or_else(|| panic!("Should delete"));
let exists_after = backend
.exists("test_key")
.ok()
.unwrap_or_else(|| panic!("Should check exists"));
assert!(!exists_after);
}
#[cfg(feature = "local")]
#[test]
fn test_create_local_backend_operations() {
let temp_dir = tempfile::tempdir()
.ok()
.unwrap_or_else(|| panic!("Should create temp dir"));
let backend = create_backend(BackendConfig::local(temp_dir.path()))
.ok()
.unwrap_or_else(|| panic!("Should create backend"));
backend
.put("data.txt", bytes::Bytes::from("content"))
.ok()
.unwrap_or_else(|| panic!("Should put"));
let exists = backend
.exists("data.txt")
.ok()
.unwrap_or_else(|| panic!("Should check exists"));
assert!(exists);
}
#[test]
fn test_backend_config_debug() {
let config = BackendConfig::local("/tmp/test");
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("Local"));
let config2 = BackendConfig::memory();
let debug_str2 = format!("{:?}", config2);
assert!(debug_str2.contains("Memory"));
}
#[test]
fn test_backend_config_clone() {
let config = BackendConfig::local("/tmp/test");
let cloned = config;
if let BackendConfig::Local { root } = cloned {
assert_eq!(root, std::path::PathBuf::from("/tmp/test"));
} else {
panic!("Expected Local config");
}
}
}