remi_s3/
config.rs

1// ๐Ÿปโ€โ„๏ธ๐Ÿงถ remi-rs: Asynchronous Rust crate to handle communication between applications and object storage providers
2// Copyright (c) 2022-2025 Noelware, LLC. <team@noelware.org>
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in all
12// copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20// SOFTWARE.
21
22use aws_config::AppName;
23use aws_credential_types::{provider::SharedCredentialsProvider, Credentials};
24use aws_sdk_s3::{
25    config::Region,
26    types::{BucketCannedAcl, ObjectCannedAcl},
27};
28
29/// Represents the main configuration struct to configure a [`StorageService`][crate::StorageService].
30#[derive(Debug, Clone, Default)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct StorageConfig {
33    /// Whether if the S3 storage backend should enable AWSv4 signatures when requests
34    /// come in or not.
35    #[cfg_attr(feature = "serde", serde(default))]
36    pub enable_signer_v4_requests: bool,
37
38    /// Whether if path access style should be enabled or not. This is recommended
39    /// to be set to `true` on MinIO instances.
40    ///
41    /// - Enabled: `https://{host}/{bucket}/...`
42    /// - Disabled: `https://{bucket}.{host}/...`
43    #[cfg_attr(feature = "serde", serde(default))]
44    pub enforce_path_access_style: bool,
45
46    /// Default ACL for all new objects.
47    #[cfg_attr(
48        feature = "serde",
49        serde(default, with = "__serde::object_acl", skip_serializing_if = "Option::is_none")
50    )]
51    pub default_object_acl: Option<ObjectCannedAcl>,
52
53    /// Default ACL to use when a bucket doesn't exist and #init was called
54    /// from the backend.
55    #[cfg_attr(
56        feature = "serde",
57        serde(default, with = "__serde::bucket_acl", skip_serializing_if = "Option::is_none")
58    )]
59    pub default_bucket_acl: Option<BucketCannedAcl>,
60
61    /// The secret access key to authenticate with S3
62    pub secret_access_key: String,
63
64    /// The access key ID to authenticate with S3
65    pub access_key_id: String,
66
67    /// Application name. This is set to `remi-s3` if not provided.
68    #[cfg_attr(feature = "serde", serde(default))]
69    pub app_name: Option<String>,
70
71    /// AWS endpoint to reach.
72    #[cfg_attr(feature = "serde", serde(default))]
73    pub endpoint: Option<String>,
74
75    /// Prefix for querying and inserting new blobs into S3.
76    #[cfg_attr(feature = "serde", serde(default))]
77    pub prefix: Option<String>,
78
79    /// The region to use, this will default to `us-east-1`.
80    #[cfg_attr(
81        feature = "serde",
82        serde(default, with = "__serde::region", skip_serializing_if = "Option::is_none")
83    )]
84    pub region: Option<Region>,
85
86    /// Bucket to use for querying and inserting objects in.
87    pub bucket: String,
88}
89
90impl From<StorageConfig> for aws_sdk_s3::Config {
91    fn from(config: StorageConfig) -> aws_sdk_s3::Config {
92        let mut cfg = aws_sdk_s3::Config::builder();
93        cfg.set_credentials_provider(Some(SharedCredentialsProvider::new(Credentials::new(
94            &config.access_key_id,
95            &config.secret_access_key,
96            None,
97            None,
98            "remi-rs",
99        ))))
100        .set_endpoint_url(config.endpoint.clone())
101        .set_app_name(Some(
102            AppName::new(config.app_name.clone().unwrap_or(String::from("remi-rs"))).unwrap(),
103        ));
104
105        if config.enforce_path_access_style {
106            cfg.set_force_path_style(Some(true));
107        }
108
109        cfg.region(config.region).build()
110    }
111}
112
113// TODO(@auguwu): switch to `azalia_serde` once MSRV is 1.84
114#[cfg(feature = "serde")]
115mod __serde {
116    pub mod region {
117        use aws_sdk_s3::config::Region;
118        use serde::{de::Deserializer, ser::Serializer, Deserialize};
119        use std::borrow::Cow;
120
121        pub fn serialize<S: Serializer>(region: &Option<Region>, serializer: S) -> Result<S::Ok, S::Error> {
122            match region {
123                Some(region) => serializer.serialize_str(region.as_ref()),
124                None => unreachable!(), // it shouldn't serialize if it is Option<None>
125            }
126        }
127
128        pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Region>, D::Error>
129        where
130            D: Deserializer<'de>,
131        {
132            let s = String::deserialize(deserializer)?;
133            Ok(Some(Region::new(Cow::Owned(s))))
134        }
135    }
136
137    pub mod bucket_acl {
138        use aws_sdk_s3::types::BucketCannedAcl;
139        use serde::*;
140
141        pub fn serialize<S: Serializer>(acl: &Option<BucketCannedAcl>, serializer: S) -> Result<S::Ok, S::Error> {
142            match acl {
143                Some(acl) => serializer.serialize_str(acl.as_str()),
144                None => unreachable!(),
145            }
146        }
147
148        pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<BucketCannedAcl>, D::Error>
149        where
150            D: Deserializer<'de>,
151        {
152            let s = String::deserialize(deserializer)?;
153            Ok(Some(s.as_str().into()))
154        }
155    }
156
157    pub mod object_acl {
158        use aws_sdk_s3::types::ObjectCannedAcl;
159        use serde::*;
160
161        pub fn serialize<S: Serializer>(acl: &Option<ObjectCannedAcl>, serializer: S) -> Result<S::Ok, S::Error> {
162            match acl {
163                Some(acl) => serializer.serialize_str(acl.as_str()),
164                None => unreachable!(),
165            }
166        }
167
168        pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<ObjectCannedAcl>, D::Error>
169        where
170            D: Deserializer<'de>,
171        {
172            let s = String::deserialize(deserializer)?;
173            Ok(Some(s.as_str().into()))
174        }
175    }
176}