kellnr_storage/
s3_storage.rs1use std::time::Duration;
2
3use async_trait::async_trait;
4use bytes::Bytes;
5use kellnr_settings::Settings;
6use object_store::aws::{AmazonS3, AmazonS3Builder};
7use object_store::path::Path;
8use object_store::{ClientOptions, ObjectStore, ObjectStoreExt, PutMode};
9
10use crate::storage::Storage;
11use crate::storage_error::StorageError;
12
13pub struct S3Storage(AmazonS3);
14
15#[async_trait]
16impl Storage for S3Storage {
17 async fn get(&self, key: &str) -> Result<Bytes, StorageError> {
18 self.storage()
19 .get(&Self::try_path_from(key)?)
20 .await?
21 .bytes()
22 .await
23 .map_err(StorageError::from)
24 }
25
26 async fn put(&self, key: &str, object: Bytes) -> Result<(), StorageError> {
27 self.storage()
28 .put_opts(
29 &Self::try_path_from(key)?,
30 object.into(),
31 PutMode::Create.into(),
32 )
33 .await?;
34 Ok(())
35 }
36
37 async fn delete(&self, key: &str) -> Result<(), StorageError> {
38 let path = Self::try_path_from(key)?;
39 self.storage().delete(&path).await?;
40 Ok(())
41 }
42
43 async fn exists(&self, key: &str) -> Result<bool, StorageError> {
44 let path = Self::try_path_from(key)?;
45 self.storage()
46 .head(&path)
47 .await
48 .map(|_| true)
49 .or_else(|e| match e {
50 object_store::Error::NotFound { .. } => Ok(false),
51 _ => Err(StorageError::from(e)),
52 })
53 }
54}
55
56impl S3Storage {
57 fn try_path_from(key: &str) -> Result<Path, object_store::path::Error> {
58 Path::from_url_path(key)
59 }
60
61 fn storage(&self) -> &AmazonS3 {
62 &self.0
63 }
64}
65
66impl TryFrom<(&str, &Settings)> for S3Storage {
67 type Error = StorageError;
68
69 fn try_from((bucket, settings): (&str, &Settings)) -> Result<Self, Self::Error> {
70 let client_options = ClientOptions::new()
74 .with_connect_timeout(Duration::from_secs(settings.s3.connect_timeout_seconds))
75 .with_timeout(Duration::from_secs(settings.s3.request_timeout_seconds))
76 .with_allow_http(settings.s3.allow_http);
77
78 let mut s3 = AmazonS3Builder::from_env()
79 .with_bucket_name(bucket)
80 .with_client_options(client_options)
81 .with_conditional_put(object_store::aws::S3ConditionalPut::ETagMatch);
82 if let Some(endpoint) = &settings.s3.endpoint {
83 s3 = s3.with_endpoint(endpoint);
84 }
85 if let Some(region) = &settings.s3.region {
86 s3 = s3.with_region(region);
87 }
88 if let Some(access_key) = &settings.s3.access_key {
89 s3 = s3.with_access_key_id(access_key);
90 }
91 if let Some(secret_key) = &settings.s3.secret_key {
92 s3 = s3.with_secret_access_key(secret_key);
93 }
94 Ok(Self(s3.build()?))
96 }
97}