code refactor
Some checks failed
CI / Check / Test (push) Has been cancelled

Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2026-10-26 09:36:23 +00:00
parent 2ce7db12c2
commit 017b58d6fa
804 changed files with 33423 additions and 28955 deletions

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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(_))
));
}
}

View File

@@ -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());
}

View File

@@ -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 {

View 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(&section).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(&section).is_err());
}