feat(poster-storage): implement S3/Minio storage adapter and configuration
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -4,3 +4,7 @@ JWT_SECRET=
|
|||||||
JWT_TTL_SECONDS=
|
JWT_TTL_SECONDS=
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
OMDB_API_KEY=
|
OMDB_API_KEY=
|
||||||
|
MINIO_ENDPOINT=
|
||||||
|
MINIO_ACCESS_KEY_ID=
|
||||||
|
MINIO_SECRET_ACCESS_KEY=
|
||||||
|
MINIO_BUCKET=
|
||||||
176
Cargo.lock
generated
176
Cargo.lock
generated
@@ -637,6 +637,21 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -681,6 +696,17 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -699,8 +725,10 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -899,6 +927,12 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -931,6 +965,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
"rustls-native-certs",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -1122,6 +1157,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -1328,7 +1372,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"domain",
|
"domain",
|
||||||
"reqwest",
|
"reqwest 0.13.3",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1430,6 +1474,36 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object_store"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3cfccb68961a56facde1163f9319e0d15743352344e7808a11795fb99698dcaf"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"humantime",
|
||||||
|
"hyper",
|
||||||
|
"itertools",
|
||||||
|
"md-5",
|
||||||
|
"parking_lot",
|
||||||
|
"percent-encoding",
|
||||||
|
"quick-xml",
|
||||||
|
"rand 0.8.6",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"snafu",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -1546,6 +1620,19 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poster-storage"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"domain",
|
||||||
|
"object_store",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1584,6 +1671,7 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"metadata",
|
"metadata",
|
||||||
|
"poster-storage",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlite",
|
"sqlite",
|
||||||
@@ -1617,6 +1705,16 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.37.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@@ -1788,6 +1886,48 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.3"
|
version = "0.13.3"
|
||||||
@@ -2189,6 +2329,27 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2"
|
||||||
|
dependencies = [
|
||||||
|
"snafu-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu-derive"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@@ -3006,6 +3167,19 @@ dependencies = [
|
|||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasmparser"
|
name = "wasmparser"
|
||||||
version = "0.244.0"
|
version = "0.244.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/adapters/auth",
|
"crates/adapters/auth",
|
||||||
"crates/adapters/metadata",
|
"crates/adapters/metadata", "crates/adapters/poster-storage",
|
||||||
"crates/adapters/rss",
|
"crates/adapters/rss",
|
||||||
"crates/adapters/sqlite",
|
"crates/adapters/sqlite",
|
||||||
"crates/adapters/template-askama",
|
"crates/adapters/template-askama",
|
||||||
@@ -31,6 +31,7 @@ sqlx = { version = "0.8.6", features = [
|
|||||||
"macros",
|
"macros",
|
||||||
] }
|
] }
|
||||||
reqwest = { version = "0.13", features = ["json", "query"] }
|
reqwest = { version = "0.13", features = ["json", "query"] }
|
||||||
|
object_store = { version = "0.11", features = ["aws"] }
|
||||||
|
|
||||||
domain = { path = "crates/domain" }
|
domain = { path = "crates/domain" }
|
||||||
common = { path = "crates/common" }
|
common = { path = "crates/common" }
|
||||||
@@ -38,6 +39,7 @@ application = { path = "crates/application" }
|
|||||||
presentation = { path = "crates/presentation" }
|
presentation = { path = "crates/presentation" }
|
||||||
auth = { path = "crates/adapters/auth" }
|
auth = { path = "crates/adapters/auth" }
|
||||||
metadata = { path = "crates/adapters/metadata" }
|
metadata = { path = "crates/adapters/metadata" }
|
||||||
|
poster-storage = { path = "crates/adapters/poster-storage" }
|
||||||
rss = { path = "crates/adapters/rss" }
|
rss = { path = "crates/adapters/rss" }
|
||||||
sqlite = { path = "crates/adapters/sqlite" }
|
sqlite = { path = "crates/adapters/sqlite" }
|
||||||
template-askama = { path = "crates/adapters/template-askama" }
|
template-askama = { path = "crates/adapters/template-askama" }
|
||||||
|
|||||||
15
crates/adapters/poster-storage/Cargo.toml
Normal file
15
crates/adapters/poster-storage/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "poster-storage"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
domain = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
object_store = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
38
crates/adapters/poster-storage/src/config.rs
Normal file
38
crates/adapters/poster-storage/src/config.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use object_store::{aws::AmazonS3Builder, ObjectStore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct StorageConfig {
|
||||||
|
endpoint: String,
|
||||||
|
access_key_id: String,
|
||||||
|
secret_access_key: String,
|
||||||
|
bucket: String,
|
||||||
|
region: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageConfig {
|
||||||
|
pub fn from_env() -> anyhow::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
endpoint: std::env::var("MINIO_ENDPOINT").context("MINIO_ENDPOINT required")?,
|
||||||
|
access_key_id: std::env::var("MINIO_ACCESS_KEY_ID")
|
||||||
|
.context("MINIO_ACCESS_KEY_ID required")?,
|
||||||
|
secret_access_key: std::env::var("MINIO_SECRET_ACCESS_KEY")
|
||||||
|
.context("MINIO_SECRET_ACCESS_KEY required")?,
|
||||||
|
bucket: std::env::var("MINIO_BUCKET").context("MINIO_BUCKET required")?,
|
||||||
|
region: std::env::var("MINIO_REGION").unwrap_or_else(|_| "minio".to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_store(self) -> anyhow::Result<Arc<dyn ObjectStore>> {
|
||||||
|
let store = AmazonS3Builder::new()
|
||||||
|
.with_endpoint(self.endpoint)
|
||||||
|
.with_access_key_id(self.access_key_id)
|
||||||
|
.with_secret_access_key(self.secret_access_key)
|
||||||
|
.with_bucket_name(self.bucket)
|
||||||
|
.with_region(self.region)
|
||||||
|
.with_allow_http(true)
|
||||||
|
.build()
|
||||||
|
.context("Failed to build S3/Minio store")?;
|
||||||
|
Ok(Arc::new(store))
|
||||||
|
}
|
||||||
|
}
|
||||||
85
crates/adapters/poster-storage/src/lib.rs
Normal file
85
crates/adapters/poster-storage/src/lib.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
mod config;
|
||||||
|
pub use config::StorageConfig;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
ports::PosterStorage,
|
||||||
|
value_objects::{MovieId, PosterPath},
|
||||||
|
};
|
||||||
|
use object_store::{path::Path, ObjectStore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct PosterStorageAdapter {
|
||||||
|
store: Arc<dyn ObjectStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PosterStorageAdapter {
|
||||||
|
pub fn new(store: Arc<dyn ObjectStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_config(config: StorageConfig) -> anyhow::Result<Self> {
|
||||||
|
Ok(Self::new(config.build_store()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PosterStorage for PosterStorageAdapter {
|
||||||
|
async fn store_poster(
|
||||||
|
&self,
|
||||||
|
movie_id: &MovieId,
|
||||||
|
image_bytes: &[u8],
|
||||||
|
) -> Result<PosterPath, DomainError> {
|
||||||
|
let path = Path::from(movie_id.value().to_string());
|
||||||
|
self.store
|
||||||
|
.put(&path, image_bytes.to_vec().into())
|
||||||
|
.await
|
||||||
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||||
|
PosterPath::new(path.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_poster(&self, poster_path: &PosterPath) -> Result<Vec<u8>, DomainError> {
|
||||||
|
let path = Path::from(poster_path.value().to_string());
|
||||||
|
let result = self.store.get(&path).await.map_err(|e| match e {
|
||||||
|
object_store::Error::NotFound { .. } => DomainError::NotFound("Poster not found".into()),
|
||||||
|
_ => DomainError::InfrastructureError(e.to_string()),
|
||||||
|
})?;
|
||||||
|
result
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map(|b| b.to_vec())
|
||||||
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use object_store::memory::InMemory;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn adapter() -> PosterStorageAdapter {
|
||||||
|
PosterStorageAdapter::new(Arc::new(InMemory::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn store_and_retrieve_round_trip() {
|
||||||
|
let adapter = adapter();
|
||||||
|
let movie_id = MovieId::from_uuid(Uuid::new_v4());
|
||||||
|
let bytes = b"fake-image-bytes";
|
||||||
|
|
||||||
|
let path = adapter.store_poster(&movie_id, bytes).await.unwrap();
|
||||||
|
let retrieved = adapter.get_poster(&path).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(retrieved, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_missing_returns_not_found() {
|
||||||
|
let adapter = adapter();
|
||||||
|
let path = PosterPath::new("nonexistent".into()).unwrap();
|
||||||
|
let result = adapter.get_poster(&path).await;
|
||||||
|
assert!(matches!(result, Err(DomainError::NotFound(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ domain = { workspace = true }
|
|||||||
application = { workspace = true }
|
application = { workspace = true }
|
||||||
auth = { workspace = true }
|
auth = { workspace = true }
|
||||||
metadata = { workspace = true }
|
metadata = { workspace = true }
|
||||||
|
poster-storage = { workspace = true }
|
||||||
sqlite = { workspace = true }
|
sqlite = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
template-askama = { workspace = true }
|
template-askama = { workspace = true }
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
ports::{EventPublisher, PosterFetcherClient, PosterStorage},
|
ports::{EventPublisher, PosterFetcherClient},
|
||||||
value_objects::{MovieId, PosterPath, PosterUrl},
|
value_objects::PosterUrl,
|
||||||
};
|
};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
@@ -15,6 +15,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|||||||
use application::{config::AppConfig, context::AppContext};
|
use application::{config::AppConfig, context::AppContext};
|
||||||
use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
|
use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
|
||||||
use metadata::MetadataClientImpl;
|
use metadata::MetadataClientImpl;
|
||||||
|
use poster_storage::{PosterStorageAdapter, StorageConfig};
|
||||||
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
|
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
|
||||||
use template_askama::AskamaHtmlRenderer;
|
use template_askama::AskamaHtmlRenderer;
|
||||||
|
|
||||||
@@ -31,27 +32,6 @@ impl PosterFetcherClient for StubPosterFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StubPosterStorage;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl PosterStorage for StubPosterStorage {
|
|
||||||
async fn store_poster(
|
|
||||||
&self,
|
|
||||||
_movie_id: &MovieId,
|
|
||||||
_bytes: &[u8],
|
|
||||||
) -> Result<PosterPath, DomainError> {
|
|
||||||
Err(DomainError::InfrastructureError(
|
|
||||||
"poster storage not implemented".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_poster(&self, _path: &PosterPath) -> Result<Vec<u8>, DomainError> {
|
|
||||||
Err(DomainError::InfrastructureError(
|
|
||||||
"poster storage not implemented".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StubEventPublisher;
|
struct StubEventPublisher;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -81,6 +61,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
async fn wire_dependencies() -> anyhow::Result<AppState> {
|
async fn wire_dependencies() -> anyhow::Result<AppState> {
|
||||||
let auth_config = AuthConfig::from_env()?;
|
let auth_config = AuthConfig::from_env()?;
|
||||||
|
let storage_config = StorageConfig::from_env()?;
|
||||||
let app_config = AppConfig::from_env();
|
let app_config = AppConfig::from_env();
|
||||||
let omdb_api_key = std::env::var("OMDB_API_KEY").context("OMDB_API_KEY must be set")?;
|
let omdb_api_key = std::env::var("OMDB_API_KEY").context("OMDB_API_KEY must be set")?;
|
||||||
|
|
||||||
@@ -101,7 +82,7 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
|
|||||||
repository: Arc::new(movie_repo),
|
repository: Arc::new(movie_repo),
|
||||||
metadata_client: Arc::new(MetadataClientImpl::new_omdb(omdb_api_key)),
|
metadata_client: Arc::new(MetadataClientImpl::new_omdb(omdb_api_key)),
|
||||||
poster_fetcher: Arc::new(StubPosterFetcher),
|
poster_fetcher: Arc::new(StubPosterFetcher),
|
||||||
poster_storage: Arc::new(StubPosterStorage),
|
poster_storage: Arc::new(PosterStorageAdapter::from_config(storage_config)?),
|
||||||
event_publisher: Arc::new(StubEventPublisher),
|
event_publisher: Arc::new(StubEventPublisher),
|
||||||
auth_service: Arc::new(JwtAuthService::new(auth_config)),
|
auth_service: Arc::new(JwtAuthService::new(auth_config)),
|
||||||
password_hasher: Arc::new(Argon2PasswordHasher),
|
password_hasher: Arc::new(Argon2PasswordHasher),
|
||||||
|
|||||||
Reference in New Issue
Block a user