use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use bytes::Bytes;
use thiserror::Error;
use url::Url;
pub mod disk;
pub mod gcs;
pub mod memory;
pub mod mirror;
pub mod s3;
pub use disk::DiskService;
pub use gcs::GcsService;
pub use memory::MemoryService;
pub use mirror::MirrorService;
pub use s3::S3Service;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("storage key not found: {0}")]
NotFound(String),
#[error("storage key already exists: {0}")]
DuplicateKey(String),
#[error("i/o failure for {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("object store failure for {path}: {message}")]
ObjectStore {
path: String,
message: String,
},
#[error("invalid storage url: {0}")]
InvalidUrl(String),
}
#[async_trait]
pub trait StorageService: Send + Sync {
fn name(&self) -> &str;
async fn upload(&self, key: &str, data: Bytes) -> Result<(), StorageError>;
async fn download(&self, key: &str) -> Result<Bytes, StorageError>;
async fn delete(&self, key: &str) -> Result<(), StorageError>;
async fn exists(&self, key: &str) -> Result<bool, StorageError>;
async fn url(&self, key: &str, expires_in: Duration) -> Result<Url, StorageError>;
}
pub type DynStorageService = Arc<dyn StorageService>;
pub(crate) fn checked_key(key: &str) -> Result<&str, StorageError> {
let trimmed = key.trim();
if trimmed.is_empty() {
return Err(StorageError::InvalidUrl(
"storage key must not be empty".to_owned(),
));
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checked_key_rejects_empty_values() {
assert!(checked_key(" ").is_err());
}
#[test]
fn test_checked_key_returns_trimmed_input() {
assert_eq!(checked_key("abc").expect("key should be valid"), "abc");
}
}