Skip to main content

kellnr_storage/
s3_storage.rs

1use 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        // NOTE: `with_client_options` replaces the builder's internal ClientOptions entirely,
71        // so `allow_http` must be set here rather than via `AmazonS3Builder::with_allow_http`,
72        // which would be overwritten by the subsequent `with_client_options` call.
73        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        // S3-compatible (RustFS, MinIO, etc.)
95        Ok(Self(s3.build()?))
96    }
97}