use std::time::Duration; use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use aws_sdk_s3::presigning::PresigningConfig; use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::Client as S3Client; use crate::models::Tenant; #[async_trait] pub trait ObjectStorage: Send + Sync + 'static { async fn put_object( &self, key: &str, bytes: Vec, content_type: Option, content_disposition: Option, ) -> Result<()>; async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result; async fn get_object(&self, key: &str) -> Result>; async fn delete_object(&self, key: &str) -> Result<()>; } pub struct S3Storage { client: S3Client, bucket: String, } impl S3Storage { pub fn new(client: S3Client, bucket: impl Into) -> Self { Self { client, bucket: bucket.into(), } } } #[async_trait] impl ObjectStorage for S3Storage { async fn put_object( &self, key: &str, bytes: Vec, content_type: Option, content_disposition: Option, ) -> Result<()> { let mut request = self .client .put_object() .bucket(&self.bucket) .key(key) .body(ByteStream::from(bytes)); if let Some(content_type) = content_type { request = request.content_type(content_type); } if let Some(content_disposition) = content_disposition { request = request.content_disposition(content_disposition); } request .send() .await .context("failed to upload object to S3")?; Ok(()) } async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result { let presign_config = PresigningConfig::builder() .expires_in(expires_in) .build() .context("failed to build S3 presigning config")?; let presigned = self .client .get_object() .bucket(&self.bucket) .key(key) .presigned(presign_config) .await .context("failed to generate presigned download URL")?; Ok(presigned.uri().to_string()) } async fn get_object(&self, key: &str) -> Result> { let response = self .client .get_object() .bucket(&self.bucket) .key(key) .send() .await .context("failed to download object from S3")?; 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<()> { self.client .delete_object() .bucket(&self.bucket) .key(key) .send() .await .context("failed to delete object from S3")?; Ok(()) } } #[derive(Clone)] pub struct TenantStorage { inner: Arc, root: String, } impl TenantStorage { pub fn new(inner: Arc, tenant: &Tenant) -> Result { let root = tenant .storage_root .as_ref() .ok_or_else(|| anyhow!("tenant {} missing storage_root", tenant.id))? .to_owned(); Ok(Self { inner, root }) } fn qualify(&self, key: &str) -> String { format!("{}{}", self.root, key) } pub async fn put_object( &self, key: &str, bytes: Vec, content_type: Option, content_disposition: Option, ) -> Result<()> { let qualified = self.qualify(key); self.inner .put_object(&qualified, bytes, content_type, content_disposition) .await } pub async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result { let qualified = self.qualify(key); self.inner.presign_get_object(&qualified, expires_in).await } pub async fn get_object(&self, key: &str) -> Result> { let qualified = self.qualify(key); self.inner.get_object(&qualified).await } pub async fn delete_object(&self, key: &str) -> Result<()> { let qualified = self.qualify(key); self.inner.delete_object(&qualified).await } }