Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "object-storage"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
@@ -11,8 +12,7 @@ tracing = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
object_store = { workspace = true }
|
||||
infer = "0.19.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -5,21 +5,25 @@ use std::sync::Arc;
|
||||
pub struct StorageConfig(Arc<dyn ObjectStore>);
|
||||
|
||||
impl StorageConfig {
|
||||
pub fn from_env() -> anyhow::Result<Self> {
|
||||
let backend = std::env::var("IMAGE_STORAGE_BACKEND").unwrap_or_else(|_| "local".into());
|
||||
|
||||
let store: Arc<dyn ObjectStore> = match backend.as_str() {
|
||||
pub fn from_config(config: &::config::StorageConfig) -> anyhow::Result<Self> {
|
||||
let store: Arc<dyn ObjectStore> = match config.backend.as_str() {
|
||||
"s3" => build_s3_store(
|
||||
&std::env::var("MINIO_ENDPOINT").context("MINIO_ENDPOINT required")?,
|
||||
&std::env::var("MINIO_ACCESS_KEY_ID").context("MINIO_ACCESS_KEY_ID required")?,
|
||||
&std::env::var("MINIO_SECRET_ACCESS_KEY")
|
||||
config
|
||||
.endpoint
|
||||
.as_deref()
|
||||
.context("MINIO_ENDPOINT required")?,
|
||||
config
|
||||
.access_key_id
|
||||
.as_deref()
|
||||
.context("MINIO_ACCESS_KEY_ID required")?,
|
||||
config
|
||||
.secret_access_key
|
||||
.as_deref()
|
||||
.context("MINIO_SECRET_ACCESS_KEY required")?,
|
||||
&std::env::var("MINIO_BUCKET").context("MINIO_BUCKET required")?,
|
||||
&std::env::var("MINIO_REGION").unwrap_or_else(|_| "minio".to_string()),
|
||||
)?,
|
||||
"local" => build_local_store(
|
||||
&std::env::var("IMAGE_STORAGE_PATH").unwrap_or_else(|_| "./images".into()),
|
||||
config.bucket.as_deref().context("MINIO_BUCKET required")?,
|
||||
&config.region,
|
||||
)?,
|
||||
"local" => build_local_store(&config.path)?,
|
||||
other => {
|
||||
anyhow::bail!("Unknown IMAGE_STORAGE_BACKEND: {other:?}. Valid values: s3, local")
|
||||
}
|
||||
@@ -58,7 +62,3 @@ fn build_local_store(path: &str) -> anyhow::Result<Arc<dyn ObjectStore>> {
|
||||
.context("Failed to initialise local file system store")?;
|
||||
Ok(Arc::new(store))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/config.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -82,6 +82,14 @@ impl ObjectStorage for ObjectStorageAdapter {
|
||||
Ok(Box::pin(stream))
|
||||
}
|
||||
|
||||
async fn exists(&self, key: &str) -> Result<bool, DomainError> {
|
||||
match self.store.head(&Path::from(key)).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(object_store::Error::NotFound { .. }) => Ok(false),
|
||||
Err(e) => Err(DomainError::InfrastructureError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete(&self, key: &str) -> Result<(), DomainError> {
|
||||
let path = Path::from(key);
|
||||
match self.store.delete(&path).await {
|
||||
@@ -112,6 +120,10 @@ impl ImageCleanupHandler {
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for ImageCleanupHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"image_cleanup"
|
||||
}
|
||||
|
||||
async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError> {
|
||||
let poster_path = match event {
|
||||
DomainEvent::MovieDeleted { poster_path, .. } => poster_path,
|
||||
@@ -127,12 +139,34 @@ impl EventHandler for ImageCleanupHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create() -> anyhow::Result<Arc<dyn ObjectStorage>> {
|
||||
pub fn create(config: &::config::StorageConfig) -> anyhow::Result<Arc<dyn ObjectStorage>> {
|
||||
Ok(Arc::new(ObjectStorageAdapter::from_config(
|
||||
StorageConfig::from_env()?,
|
||||
StorageConfig::from_config(config)?,
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/lib.rs"]
|
||||
mod tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use domain::value_objects::{MovieId, PosterPath};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
#[tokio::test]
|
||||
async fn image_cleanup_is_idempotent_when_run_twice() {
|
||||
let storage = Arc::new(ObjectStorageAdapter::new(Arc::new(InMemory::new())));
|
||||
storage.store("posters/m1.webp", b"img").await.unwrap();
|
||||
let handler = ImageCleanupHandler::new(storage.clone());
|
||||
let event = DomainEvent::MovieDeleted {
|
||||
movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()),
|
||||
poster_path: Some(PosterPath::new("posters/m1.webp".into()).unwrap()),
|
||||
};
|
||||
|
||||
handler.handle(&event).await.unwrap();
|
||||
handler.handle(&event).await.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
storage.get("posters/m1.webp").await,
|
||||
Err(DomainError::NotFound(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_store_creates_dir_and_succeeds() {
|
||||
let dir = std::env::temp_dir().join(format!("image_test_{}", uuid::Uuid::new_v4()));
|
||||
let result = build_local_store(dir.to_str().unwrap());
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
|
||||
assert!(dir.exists(), "directory should have been created");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_store_succeeds_if_dir_already_exists() {
|
||||
let dir = std::env::temp_dir().join(format!("image_test_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let result = build_local_store(dir.to_str().unwrap());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::{EventHandler, ObjectStorage},
|
||||
};
|
||||
use object_storage::{ImageCleanupHandler, ObjectStorageAdapter};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
fn adapter() -> ObjectStorageAdapter {
|
||||
44
crates/adapters/object-storage/tests/storage_config_test.rs
Normal file
44
crates/adapters/object-storage/tests/storage_config_test.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use object_storage::StorageConfig;
|
||||
|
||||
fn local_section(dir: &std::path::Path) -> config::StorageConfig {
|
||||
config::StorageConfig {
|
||||
backend: "local".into(),
|
||||
path: dir.to_str().unwrap().into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_store_creates_dir_and_succeeds() {
|
||||
let dir = std::env::temp_dir().join(format!("image_test_{}", uuid::Uuid::new_v4()));
|
||||
let result = StorageConfig::from_config(&local_section(&dir));
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
|
||||
assert!(dir.exists(), "directory should have been created");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_store_succeeds_if_dir_already_exists() {
|
||||
let dir = std::env::temp_dir().join(format!("image_test_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let result = StorageConfig::from_config(&local_section(&dir));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_backend_requires_endpoint() {
|
||||
let section = config::StorageConfig {
|
||||
backend: "s3".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let err = StorageConfig::from_config(§ion).err().unwrap();
|
||||
assert!(err.to_string().contains("MINIO_ENDPOINT required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_backend_is_error() {
|
||||
let section = config::StorageConfig {
|
||||
backend: "ftp".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(StorageConfig::from_config(§ion).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user