use std::sync::Arc;
use aws_sdk_s3::Client as S3Client;
use aws_sdk_s3::config::{Credentials, Region, SharedCredentialsProvider};
use aws_sdk_s3::error::SdkError;
use aws_sdk_s3::operation::head_object::HeadObjectError;
use aws_sdk_s3::primitives::ByteStream;
use bytes::Bytes;
use crate::error::Error;
use crate::traits::StorageBackend;
use crate::types::{PutOptions, RawDownloadResult};
pub struct S3Backend {
client: Arc<S3Client>,
bucket: String,
public_base_url: String,
}
pub struct S3BackendBuilder {
region: String,
bucket: Option<String>,
access_key: Option<String>,
secret_key: Option<String>,
endpoint: Option<String>,
public_base_url: Option<String>,
}
impl S3Backend {
pub fn builder() -> S3BackendBuilder {
S3BackendBuilder {
region: "us-east-1".to_string(),
bucket: None,
access_key: None,
secret_key: None,
endpoint: None,
public_base_url: None,
}
}
}
impl S3BackendBuilder {
pub fn region(mut self, region: &str) -> Self {
self.region = region.to_string();
self
}
pub fn bucket(mut self, bucket: &str) -> Self {
self.bucket = Some(bucket.to_string());
self
}
pub fn credentials(mut self, access_key: &str, secret_key: &str) -> Self {
self.access_key = Some(access_key.to_string());
self.secret_key = Some(secret_key.to_string());
self
}
pub fn endpoint(mut self, endpoint: &str) -> Self {
self.endpoint = Some(endpoint.to_string());
self
}
pub fn public_base_url(mut self, url: &str) -> Self {
self.public_base_url = Some(url.trim_end_matches('/').to_string());
self
}
pub fn build(self) -> Result<S3Backend, Error> {
let bucket = self
.bucket
.ok_or_else(|| Error::ConfigError("bucket is required".into()))?;
let access_key = self
.access_key
.ok_or_else(|| Error::ConfigError("access_key is required".into()))?;
let secret_key = self
.secret_key
.ok_or_else(|| Error::ConfigError("secret_key is required".into()))?;
let region = Region::new(self.region.clone());
let credentials = Credentials::new(&access_key, &secret_key, None, None, "crypsol_storage");
let creds_provider = SharedCredentialsProvider::new(credentials);
let mut cfg = aws_sdk_s3::Config::builder()
.region(region)
.credentials_provider(creds_provider)
.behavior_version_latest()
.force_path_style(self.endpoint.is_some());
if let Some(ref endpoint) = self.endpoint {
cfg = cfg.endpoint_url(endpoint);
}
let client = S3Client::from_conf(cfg.build());
let public_base_url = self.public_base_url.unwrap_or_else(|| {
if let Some(ref ep) = self.endpoint {
format!("{}/{bucket}", ep.trim_end_matches('/'))
} else {
format!("https://{bucket}.s3.{}.amazonaws.com", self.region)
}
});
Ok(S3Backend {
client: Arc::new(client),
bucket,
public_base_url,
})
}
}
impl StorageBackend for S3Backend {
async fn put_object(
&self,
key: &str,
data: Bytes,
content_type: &str,
options: &PutOptions,
) -> Result<(), Error> {
let body = ByteStream::from(data);
let mut builder = self
.client
.put_object()
.bucket(&self.bucket)
.key(key)
.body(body)
.content_type(content_type);
if let Some(ref cc) = options.cache_control {
builder = builder.cache_control(cc);
}
if let Some(ref cd) = options.content_disposition {
builder = builder.content_disposition(cd);
}
builder
.send()
.await
.map_err(|e| Error::Backend(format!("S3 upload failed: {e}")))?;
Ok(())
}
async fn get_object(&self, key: &str) -> Result<RawDownloadResult, Error> {
let response = self
.client
.get_object()
.bucket(&self.bucket)
.key(key)
.send()
.await
.map_err(|e| Error::Backend(format!("S3 download failed: {e}")))?;
let content_type = response
.content_type()
.unwrap_or("application/octet-stream")
.to_string();
let content_length = response.content_length();
let body = response
.body
.collect()
.await
.map_err(|e| Error::Backend(format!("S3 read body failed: {e}")))?;
Ok(RawDownloadResult {
data: body.into_bytes(),
content_type,
content_length,
})
}
async fn delete_object(&self, key: &str) -> Result<(), Error> {
self.client
.delete_object()
.bucket(&self.bucket)
.key(key)
.send()
.await
.map_err(|e| Error::Backend(format!("S3 delete failed: {e}")))?;
Ok(())
}
async fn exists(&self, key: &str) -> Result<bool, Error> {
match self
.client
.head_object()
.bucket(&self.bucket)
.key(key)
.send()
.await
{
Ok(_) => Ok(true),
Err(sdk_error) => match &sdk_error {
SdkError::ServiceError(service_err) => match service_err.err() {
HeadObjectError::NotFound(_) => Ok(false),
_ => Err(Error::Backend(format!(
"S3 exists check failed: {sdk_error}"
))),
},
_ => Err(Error::Backend(format!(
"S3 exists check failed: {sdk_error}"
))),
},
}
}
async fn presigned_get_url(&self, key: &str, expires_in_secs: u64) -> Result<String, Error> {
let presigning_config = aws_sdk_s3::presigning::PresigningConfig::builder()
.expires_in(std::time::Duration::from_secs(expires_in_secs))
.build()
.map_err(|e| Error::Backend(format!("S3 presign config failed: {e}")))?;
let presigned = self
.client
.get_object()
.bucket(&self.bucket)
.key(key)
.presigned(presigning_config)
.await
.map_err(|e| Error::Backend(format!("S3 presigned GET failed: {e}")))?;
Ok(presigned.uri().to_string())
}
async fn presigned_put_url(
&self,
key: &str,
content_type: &str,
expires_in_secs: u64,
) -> Result<String, Error> {
let presigning_config = aws_sdk_s3::presigning::PresigningConfig::builder()
.expires_in(std::time::Duration::from_secs(expires_in_secs))
.build()
.map_err(|e| Error::Backend(format!("S3 presign config failed: {e}")))?;
let presigned = self
.client
.put_object()
.bucket(&self.bucket)
.key(key)
.content_type(content_type)
.presigned(presigning_config)
.await
.map_err(|e| Error::Backend(format!("S3 presigned PUT failed: {e}")))?;
Ok(presigned.uri().to_string())
}
fn public_url(&self, key: &str) -> String {
format!("{}/{key}", self.public_base_url)
}
async fn test_connection(&self) -> Result<(), Error> {
self.client
.head_bucket()
.bucket(&self.bucket)
.send()
.await
.map_err(|e| Error::Backend(format!("S3 connection test failed: {e}")))?;
Ok(())
}
}