mod local; mod s3; pub use local::LocalStorage; pub use s3::S3Storage; use crate::validation::{validate_storage_key, ValidationError}; use async_trait::async_trait; use axum::body::Bytes; use std::sync::Arc; use thiserror::Error; /// File metadata #[derive(Debug, Clone)] pub struct FileMeta { pub size: u64, pub modified: u64, // Unix timestamp } #[derive(Debug, Error)] pub enum StorageError { #[error("Network error: {0}")] Network(String), #[error("Object not found")] NotFound, #[error("IO error: {0}")] Io(String), #[error("Validation error: {0}")] Validation(#[from] ValidationError), } pub type Result = std::result::Result; /// Storage backend trait #[async_trait] pub trait StorageBackend: Send + Sync { async fn put(&self, key: &str, data: &[u8]) -> Result<()>; async fn get(&self, key: &str) -> Result; async fn list(&self, prefix: &str) -> Vec; async fn stat(&self, key: &str) -> Option; async fn health_check(&self) -> bool; fn backend_name(&self) -> &'static str; } /// Storage wrapper for dynamic dispatch #[derive(Clone)] pub struct Storage { inner: Arc, } impl Storage { pub fn new_local(path: &str) -> Self { Self { inner: Arc::new(LocalStorage::new(path)), } } pub fn new_s3(s3_url: &str, bucket: &str) -> Self { Self { inner: Arc::new(S3Storage::new(s3_url, bucket)), } } pub async fn put(&self, key: &str, data: &[u8]) -> Result<()> { validate_storage_key(key)?; self.inner.put(key, data).await } pub async fn get(&self, key: &str) -> Result { validate_storage_key(key)?; self.inner.get(key).await } pub async fn list(&self, prefix: &str) -> Vec { // Empty prefix is valid for listing all if !prefix.is_empty() && validate_storage_key(prefix).is_err() { return Vec::new(); } self.inner.list(prefix).await } pub async fn stat(&self, key: &str) -> Option { if validate_storage_key(key).is_err() { return None; } self.inner.stat(key).await } pub async fn health_check(&self) -> bool { self.inner.health_check().await } pub fn backend_name(&self) -> &'static str { self.inner.backend_name() } }