This commit is contained in:
2025-11-08 02:03:52 +01:00
parent 9bf1fc1983
commit d24d2c9249
6 changed files with 524 additions and 1027 deletions
+457 -928
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -18,9 +18,7 @@ uuid = { version = "1.6", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
# S3 # S3
aws-config = "1.1" rust-s3 = { version = "0.37", features = ["with-tokio", "tokio-rustls-tls"] }
aws-sdk-s3 = "1.14"
aws-credential-types = "1.2"
# Serialization # Serialization
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
+2 -3
View File
@@ -492,9 +492,8 @@ async fn delete_assets_for_tenant(
pool: &PgPool, pool: &PgPool,
tenant_id: Uuid, tenant_id: Uuid,
) -> Result<()> { ) -> Result<()> {
let s3_client = s3::build_client(config).await?; let bucket = s3::build_bucket(config)?;
let storage: Arc<dyn ObjectStorage> = let storage: Arc<dyn ObjectStorage> = Arc::new(S3Storage::new(bucket));
Arc::new(S3Storage::new(s3_client, config.s3_bucket.clone()));
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table let tenant: Tenant = tenants::table
+25 -29
View File
@@ -1,38 +1,34 @@
use anyhow::Result; use anyhow::{anyhow, Context, Result};
use aws_config::meta::region::RegionProviderChain; use s3::{bucket::Bucket, creds::Credentials, region::Region};
use aws_credential_types::Credentials;
use aws_sdk_s3::{
config::{Builder as S3ConfigBuilder, Region},
Client as S3Client,
};
use crate::config::AppConfig; use crate::config::AppConfig;
pub async fn build_client(config: &AppConfig) -> Result<S3Client> { pub fn build_bucket(config: &AppConfig) -> Result<Bucket> {
let region = Region::new(config.aws_region.clone()); let region = if let Some(endpoint) = &config.aws_endpoint_url {
let region_provider = RegionProviderChain::first_try(Some(region)) Region::Custom {
.or_default_provider() region: config.aws_region.clone(),
.or_else("us-east-1"); endpoint: endpoint.clone(),
#[allow(deprecated)]
let mut loader = aws_config::from_env().region(region_provider);
if let Some(endpoint) = &config.aws_endpoint_url {
loader = loader.endpoint_url(endpoint);
} }
} else {
config
.aws_region
.parse::<Region>()
.context("invalid AWS region")?
};
if let (Some(access_key), Some(secret_key)) = ( let credentials = if let (Some(access_key), Some(secret_key)) = (
config.aws_access_key_id.clone(), config.aws_access_key_id.as_deref(),
config.aws_secret_access_key.clone(), config.aws_secret_access_key.as_deref(),
) { ) {
let credentials = Credentials::new(access_key, secret_key, None, None, "static"); Credentials::new(Some(access_key), Some(secret_key), None, None, None)
loader = loader.credentials_provider(credentials); .context("failed to create static AWS credentials")?
} } else {
Credentials::default().context("failed to load AWS credentials")?
};
let base_config = loader.load().await; let bucket = Bucket::new(&config.s3_bucket, region, credentials)
let s3_config = S3ConfigBuilder::from(&base_config) .map_err(|err| anyhow!("failed to create S3 bucket client: {err}"))?;
.force_path_style(true) let bucket = bucket.with_path_style();
.build();
Ok(S3Client::from_conf(s3_config)) Ok(*bucket)
} }
+2 -5
View File
@@ -34,11 +34,8 @@ impl AppState {
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
let pool_size = pool_size_override.unwrap_or(config.database_max_pool_size); let pool_size = pool_size_override.unwrap_or(config.database_max_pool_size);
let pool = crate::db::init_pool_with_size(&config.database_url, pool_size)?; let pool = crate::db::init_pool_with_size(&config.database_url, pool_size)?;
let s3_client = crate::s3::build_client(&config).await?; let bucket = crate::s3::build_bucket(&config)?;
let storage = Arc::new(crate::storage::S3Storage::new( let storage = Arc::new(crate::storage::S3Storage::new(bucket));
s3_client,
config.s3_bucket.clone(),
));
let jwt = crate::auth::jwt::JwtService::from_config(&config)?; let jwt = crate::auth::jwt::JwtService::from_config(&config)?;
Ok(Self::new(pool, config, storage, jwt)) Ok(Self::new(pool, config, storage, jwt))
+35 -57
View File
@@ -1,12 +1,10 @@
use std::time::Duration; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use aws_sdk_s3::presigning::PresigningConfig; use s3::bucket::Bucket;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::Client as S3Client;
use crate::models::Tenant; use crate::models::Tenant;
@@ -33,16 +31,16 @@ pub trait ObjectStorage: Send + Sync + 'static {
} }
pub struct S3Storage { pub struct S3Storage {
client: S3Client, bucket: Bucket,
bucket: String,
} }
impl S3Storage { impl S3Storage {
pub fn new(client: S3Client, bucket: impl Into<String>) -> Self { pub fn new(bucket: Bucket) -> Self {
Self { Self { bucket }
client,
bucket: bucket.into(),
} }
fn default_content_type(content_type: Option<String>) -> String {
content_type.unwrap_or_else(|| "application/octet-stream".to_string())
} }
} }
@@ -55,23 +53,19 @@ impl ObjectStorage for S3Storage {
content_type: Option<String>, content_type: Option<String>,
content_disposition: Option<String>, content_disposition: Option<String>,
) -> Result<()> { ) -> Result<()> {
let mut request = self let mut builder = self
.client .bucket
.put_object() .put_object_builder(key, &bytes)
.bucket(&self.bucket) .with_content_type(Self::default_content_type(content_type));
.key(key)
.body(ByteStream::from(bytes));
if let Some(content_type) = content_type { if let Some(disposition) = content_disposition {
request = request.content_type(content_type); builder = builder
.with_content_disposition(disposition)
.context("invalid content disposition header")?;
} }
if let Some(content_disposition) = content_disposition { builder
request = request.content_disposition(content_disposition); .execute()
}
request
.send()
.await .await
.context("failed to upload object to S3")?; .context("failed to upload object to S3")?;
@@ -84,51 +78,35 @@ impl ObjectStorage for S3Storage {
expires_in: Duration, expires_in: Duration,
response_content_disposition: Option<&str>, response_content_disposition: Option<&str>,
) -> Result<String> { ) -> Result<String> {
let presign_config = PresigningConfig::builder() let expiry_secs = u32::try_from(expires_in.as_secs())
.expires_in(expires_in) .context("presign expiry exceeds u32 range")?;
.build()
.context("failed to build S3 presigning config")?;
let mut request = self.client.get_object().bucket(&self.bucket).key(key); let mut queries = HashMap::new();
if let Some(value) = response_content_disposition { if let Some(value) = response_content_disposition {
request = request.response_content_disposition(value); queries.insert(
"response-content-disposition".to_string(),
value.to_string(),
);
} }
let presigned = request self.bucket
.presigned(presign_config) .presign_get(key, expiry_secs, (!queries.is_empty()).then_some(queries))
.await .await
.context("failed to generate presigned download URL")?; .context("failed to generate presigned download URL")
Ok(presigned.uri().to_string())
} }
async fn get_object(&self, key: &str) -> Result<Vec<u8>> { async fn get_object(&self, key: &str) -> Result<Vec<u8>> {
let response = self let data = self
.client .bucket
.get_object() .get_object(key)
.bucket(&self.bucket)
.key(key)
.send()
.await .await
.context("failed to download object from S3")?; .context("failed to download object from S3")?;
Ok(data.into_bytes().to_vec())
let bytes = response
.body
.collect()
.await
.context("failed to read object stream")?
.into_bytes()
.to_vec();
Ok(bytes)
} }
async fn delete_object(&self, key: &str) -> Result<()> { async fn delete_object(&self, key: &str) -> Result<()> {
self.client self.bucket
.delete_object() .delete_object(key)
.bucket(&self.bucket)
.key(key)
.send()
.await .await
.context("failed to delete object from S3")?; .context("failed to delete object from S3")?;
Ok(()) Ok(())