style: cargo fmt --all
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use domain::{errors::DomainError, ports::TokenIssuer, value_objects::SystemId};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod db;
|
||||
pub mod user_repository;
|
||||
|
||||
pub use db::{connect, run_migrations, PgPool};
|
||||
pub use db::{PgPool, connect, run_migrations};
|
||||
pub use user_repository::PostgresUserRepository;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::db::PgPool;
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
entities::User,
|
||||
@@ -5,14 +6,15 @@ use domain::{
|
||||
ports::UserRepository,
|
||||
value_objects::{Email, PasswordHash, SystemId},
|
||||
};
|
||||
use crate::db::PgPool;
|
||||
|
||||
pub struct PostgresUserRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PostgresUserRepository {
|
||||
pub fn new(pool: PgPool) -> Self { Self { pool } }
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -26,12 +28,14 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
|
||||
row.map(|r| Ok(User {
|
||||
id: SystemId::from_uuid(r.id),
|
||||
email: Email::new(r.email)?,
|
||||
password_hash: PasswordHash::from_hash(r.password_hash),
|
||||
created_at: r.created_at,
|
||||
}))
|
||||
row.map(|r| {
|
||||
Ok(User {
|
||||
id: SystemId::from_uuid(r.id),
|
||||
email: Email::new(r.email)?,
|
||||
password_hash: PasswordHash::from_hash(r.password_hash),
|
||||
created_at: r.created_at,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
@@ -44,12 +48,14 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
|
||||
row.map(|r| Ok(User {
|
||||
id: SystemId::from_uuid(r.id),
|
||||
email: Email::new(r.email)?,
|
||||
password_hash: PasswordHash::from_hash(r.password_hash),
|
||||
created_at: r.created_at,
|
||||
}))
|
||||
row.map(|r| {
|
||||
Ok(User {
|
||||
id: SystemId::from_uuid(r.id),
|
||||
email: Email::new(r.email)?,
|
||||
password_hash: PasswordHash::from_hash(r.password_hash),
|
||||
created_at: r.created_at,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::StreamExt;
|
||||
use object_store::{ObjectStore, path::Path, Error as OsError};
|
||||
use domain::errors::DomainError;
|
||||
use domain::ports::{DataStream, StorageReader, StorageWriter};
|
||||
use futures::stream::StreamExt;
|
||||
use object_store::{Error as OsError, ObjectStore, path::Path};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ObjectStorageAdapter {
|
||||
store: Arc<dyn ObjectStore>,
|
||||
@@ -12,7 +12,10 @@ pub struct ObjectStorageAdapter {
|
||||
}
|
||||
|
||||
impl ObjectStorageAdapter {
|
||||
pub fn new(store: Arc<dyn ObjectStore>, prefix: impl Into<String>) -> Result<Self, DomainError> {
|
||||
pub fn new(
|
||||
store: Arc<dyn ObjectStore>,
|
||||
prefix: impl Into<String>,
|
||||
) -> Result<Self, DomainError> {
|
||||
let prefix = prefix.into();
|
||||
if !prefix.is_empty() {
|
||||
validate_key(&prefix)?;
|
||||
@@ -38,17 +41,19 @@ fn map_err(e: OsError, key: &str) -> DomainError {
|
||||
|
||||
fn validate_key(key: &str) -> Result<(), DomainError> {
|
||||
if key.is_empty() {
|
||||
return Err(DomainError::Validation("storage key must not be empty".into()));
|
||||
return Err(DomainError::Validation(
|
||||
"storage key must not be empty".into(),
|
||||
));
|
||||
}
|
||||
if key.starts_with('/') {
|
||||
return Err(DomainError::Validation(
|
||||
format!("storage key must not start with '/': {key}"),
|
||||
));
|
||||
return Err(DomainError::Validation(format!(
|
||||
"storage key must not start with '/': {key}"
|
||||
)));
|
||||
}
|
||||
if key.split('/').any(|seg| seg == ".." || seg == ".") {
|
||||
return Err(DomainError::Validation(
|
||||
format!("storage key contains invalid path segment: {key}"),
|
||||
));
|
||||
return Err(DomainError::Validation(format!(
|
||||
"storage key contains invalid path segment: {key}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -79,7 +84,10 @@ impl StorageWriter for ObjectStorageAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
upload.complete().await.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
upload
|
||||
.complete()
|
||||
.await
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -99,11 +107,7 @@ impl StorageReader for ObjectStorageAdapter {
|
||||
async fn get(&self, key: &str) -> Result<DataStream, DomainError> {
|
||||
validate_key(key)?;
|
||||
let path = self.path(key);
|
||||
let result = self
|
||||
.store
|
||||
.get(&path)
|
||||
.await
|
||||
.map_err(|e| map_err(e, key))?;
|
||||
let result = self.store.get(&path).await.map_err(|e| map_err(e, key))?;
|
||||
let s = result
|
||||
.into_stream()
|
||||
.map(|r| r.map_err(|e| DomainError::Internal(e.to_string())));
|
||||
@@ -128,10 +132,12 @@ impl StorageReader for ObjectStorageAdapter {
|
||||
let key = meta.location.to_string();
|
||||
let stripped = if !self.prefix.is_empty() {
|
||||
key.strip_prefix(&format!("{}/", self.prefix))
|
||||
.ok_or_else(|| DomainError::Internal(format!(
|
||||
"listed key '{key}' does not start with expected prefix '{}'",
|
||||
self.prefix
|
||||
)))?
|
||||
.ok_or_else(|| {
|
||||
DomainError::Internal(format!(
|
||||
"listed key '{key}' does not start with expected prefix '{}'",
|
||||
self.prefix
|
||||
))
|
||||
})?
|
||||
.to_string()
|
||||
} else {
|
||||
key
|
||||
@@ -172,7 +178,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn get_missing_is_not_found() {
|
||||
let a = make_adapter();
|
||||
assert!(matches!(a.get("nope.txt").await, Err(DomainError::NotFound(_))));
|
||||
assert!(matches!(
|
||||
a.get("nope.txt").await,
|
||||
Err(DomainError::NotFound(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -186,7 +195,10 @@ mod tests {
|
||||
let a = make_adapter();
|
||||
a.put("file.txt", one_shot(b"data")).await.unwrap();
|
||||
a.delete("file.txt").await.unwrap();
|
||||
assert!(matches!(a.get("file.txt").await, Err(DomainError::NotFound(_))));
|
||||
assert!(matches!(
|
||||
a.get("file.txt").await,
|
||||
Err(DomainError::NotFound(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -213,9 +225,15 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rejects_empty_key() {
|
||||
let a = make_adapter();
|
||||
assert!(matches!(a.put("", one_shot(b"x")).await, Err(DomainError::Validation(_))));
|
||||
assert!(matches!(
|
||||
a.put("", one_shot(b"x")).await,
|
||||
Err(DomainError::Validation(_))
|
||||
));
|
||||
assert!(matches!(a.get("").await, Err(DomainError::Validation(_))));
|
||||
assert!(matches!(a.delete("").await, Err(DomainError::Validation(_))));
|
||||
assert!(matches!(
|
||||
a.delete("").await,
|
||||
Err(DomainError::Validation(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -230,8 +248,14 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rejects_path_traversal() {
|
||||
let a = make_adapter();
|
||||
assert!(matches!(a.get("../escape").await, Err(DomainError::Validation(_))));
|
||||
assert!(matches!(a.get("a/../../../etc").await, Err(DomainError::Validation(_))));
|
||||
assert!(matches!(
|
||||
a.get("../escape").await,
|
||||
Err(DomainError::Validation(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
a.get("a/../../../etc").await,
|
||||
Err(DomainError::Validation(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -246,8 +270,14 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_list_prefix() {
|
||||
let a = make_adapter();
|
||||
assert!(matches!(a.list(Some("")).await, Err(DomainError::Validation(_))));
|
||||
assert!(matches!(a.list(Some("../escape")).await, Err(DomainError::Validation(_))));
|
||||
assert!(matches!(
|
||||
a.list(Some("")).await,
|
||||
Err(DomainError::Validation(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
a.list(Some("../escape")).await,
|
||||
Err(DomainError::Validation(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -275,7 +305,9 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn put_bytes_get_bytes_roundtrip() {
|
||||
let a = make_adapter();
|
||||
a.put_bytes("data.bin", Bytes::from("hello bytes")).await.unwrap();
|
||||
a.put_bytes("data.bin", Bytes::from("hello bytes"))
|
||||
.await
|
||||
.unwrap();
|
||||
let got = a.get_bytes("data.bin").await.unwrap();
|
||||
assert_eq!(got.as_ref(), b"hello bytes");
|
||||
}
|
||||
@@ -283,7 +315,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn get_bytes_missing_is_not_found() {
|
||||
let a = make_adapter();
|
||||
assert!(matches!(a.get_bytes("nope.bin").await, Err(DomainError::NotFound(_))));
|
||||
assert!(matches!(
|
||||
a.get_bytes("nope.bin").await,
|
||||
Err(DomainError::NotFound(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
use anyhow::{Context, Result};
|
||||
use object_store::ObjectStore;
|
||||
use object_store::local::LocalFileSystem;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// All storage configuration. Populate once via `from_env()` and pass
|
||||
/// explicitly to `build_store` and `ObjectStorageAdapter::new`.
|
||||
@@ -41,7 +41,9 @@ impl StorageConfig {
|
||||
pub fn build_store(config: &StorageConfig) -> Result<Arc<dyn ObjectStore>> {
|
||||
match config.backend.as_str() {
|
||||
"local" => {
|
||||
let path = config.local_path.as_deref()
|
||||
let path = config
|
||||
.local_path
|
||||
.as_deref()
|
||||
.context("STORAGE_PATH must be set when STORAGE_BACKEND=local")?;
|
||||
std::fs::create_dir_all(path)
|
||||
.with_context(|| format!("failed to create storage dir: {path}"))?;
|
||||
@@ -53,18 +55,28 @@ pub fn build_store(config: &StorageConfig) -> Result<Arc<dyn ObjectStore>> {
|
||||
use object_store::aws::AmazonS3Builder;
|
||||
let store = AmazonS3Builder::new()
|
||||
.with_endpoint(
|
||||
config.s3_endpoint.as_deref().context("S3_ENDPOINT must be set")?,
|
||||
config
|
||||
.s3_endpoint
|
||||
.as_deref()
|
||||
.context("S3_ENDPOINT must be set")?,
|
||||
)
|
||||
.with_access_key_id(
|
||||
config.s3_access_key_id.as_deref()
|
||||
config
|
||||
.s3_access_key_id
|
||||
.as_deref()
|
||||
.context("S3_ACCESS_KEY_ID must be set")?,
|
||||
)
|
||||
.with_secret_access_key(
|
||||
config.s3_secret_access_key.as_deref()
|
||||
config
|
||||
.s3_secret_access_key
|
||||
.as_deref()
|
||||
.context("S3_SECRET_ACCESS_KEY must be set")?,
|
||||
)
|
||||
.with_bucket_name(
|
||||
config.s3_bucket.as_deref().context("S3_BUCKET must be set")?,
|
||||
config
|
||||
.s3_bucket
|
||||
.as_deref()
|
||||
.context("S3_BUCKET must be set")?,
|
||||
)
|
||||
.with_region(config.s3_region.as_deref().unwrap_or("us-east-1"))
|
||||
.with_allow_http(true)
|
||||
@@ -76,7 +88,10 @@ pub fn build_store(config: &StorageConfig) -> Result<Arc<dyn ObjectStore>> {
|
||||
use object_store::gcp::GoogleCloudStorageBuilder;
|
||||
let store = GoogleCloudStorageBuilder::new()
|
||||
.with_bucket_name(
|
||||
config.gcs_bucket.as_deref().context("GCS_BUCKET must be set")?,
|
||||
config
|
||||
.gcs_bucket
|
||||
.as_deref()
|
||||
.context("GCS_BUCKET must be set")?,
|
||||
)
|
||||
.build()?;
|
||||
Ok(Arc::new(store))
|
||||
|
||||
@@ -2,4 +2,4 @@ pub mod adapter;
|
||||
pub mod config;
|
||||
|
||||
pub use adapter::ObjectStorageAdapter;
|
||||
pub use config::{build_store, StorageConfig};
|
||||
pub use config::{StorageConfig, build_store};
|
||||
|
||||
Reference in New Issue
Block a user