use crate::config::AppConfig; use crate::error::{AppError, AppResult}; use object_store::aws::{AmazonS3, AmazonS3Builder}; use object_store::path::Path; use object_store::signer::Signer; use object_store::{ObjectStoreExt, PutPayload}; use reqwest::Method; use std::sync::Arc; use std::time::Duration; #[derive(Clone)] pub struct AppS3Storage { client: Arc, #[allow(dead_code)] bucket: String, public_url: Option, presigned_url_expiry: Duration, } impl AppS3Storage { pub async fn from_config(config: &AppConfig) -> AppResult { let bucket = config .s3_bucket()? .ok_or_else(|| AppError::Config("APP_S3_BUCKET is not set".into()))?; let region = config.s3_region()?; let access_key = config.s3_access_key()?; let secret_key = config.s3_secret_key()?; let force_path_style = config.s3_force_path_style()?; let mut builder = AmazonS3Builder::new() .with_bucket_name(&bucket) .with_region(®ion) .with_virtual_hosted_style_request(!force_path_style); if let Some(endpoint) = config.s3_endpoint()? { builder = builder.with_endpoint(&endpoint); } if let (Some(ak), Some(sk)) = (access_key, secret_key) { builder = builder.with_access_key_id(&ak).with_secret_access_key(&sk); } let client = builder.build()?; Ok(Self { client: Arc::new(client), bucket, public_url: config.s3_public_url()?, presigned_url_expiry: Duration::from_secs(config.s3_presigned_url_expiry()?), }) } pub async fn put(&self, key: &str, data: Vec) -> AppResult<()> { let path = Path::from(key); let payload = PutPayload::from_bytes(data.into()); self.client.put(&path, payload).await?; Ok(()) } pub async fn get(&self, key: &str) -> AppResult> { let path = Path::from(key); let result = self.client.get(&path).await?; let bytes = result.bytes().await?; Ok(bytes.to_vec()) } pub async fn delete(&self, key: &str) -> AppResult<()> { let path = Path::from(key); self.client.delete(&path).await?; Ok(()) } pub async fn presigned_get_url( &self, key: &str, expiry: Option, ) -> AppResult { let path = Path::from(key); let expires = expiry.unwrap_or(self.presigned_url_expiry); let url = self.client.signed_url(Method::GET, &path, expires).await?; Ok(url.to_string()) } pub async fn presigned_put_url( &self, key: &str, expiry: Option, ) -> AppResult { let path = Path::from(key); let expires = expiry.unwrap_or(self.presigned_url_expiry); let url = self.client.signed_url(Method::PUT, &path, expires).await?; Ok(url.to_string()) } pub fn public_url(&self, key: &str) -> Option { self.public_url .as_ref() .map(|base| format!("{}/{}", base.trim_end_matches('/'), key)) } }