Skip to main content

opendal_service_s3/
config.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::collections::HashMap;
19use std::fmt::Debug;
20
21use opendal_core::Configurator;
22use opendal_core::OperatorUri;
23use opendal_core::Result;
24use serde::Deserialize;
25use serde::Serialize;
26
27use crate::backend::S3Builder;
28
29/// Config for Aws S3 and compatible services (including minio, digitalocean space,
30/// Tencent Cloud Object Storage(COS) and so on) support.
31#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
32#[serde(default)]
33#[non_exhaustive]
34pub struct S3Config {
35    /// root of this backend.
36    ///
37    /// All operations will happen under this root.
38    ///
39    /// default to `/` if not set.
40    ///
41    /// <!-- @group General -->
42    /// <!-- @default / -->
43    pub root: Option<String>,
44    /// bucket name of this backend.
45    ///
46    /// required.
47    ///
48    /// <!-- @group General -->
49    /// <!-- @example my-bucket -->
50    #[serde(alias = "aws_bucket", alias = "aws_bucket_name", alias = "bucket_name")]
51    pub bucket: String,
52    /// Deprecated: S3 versioning capability is enabled by default.
53    ///
54    /// <!-- @group Deprecated -->
55    #[deprecated(
56        since = "0.57.0",
57        note = "S3 versioning capability is enabled by default and this option is no longer needed."
58    )]
59    pub enable_versioning: bool,
60    /// endpoint of this backend.
61    ///
62    /// Endpoint must be full uri, e.g.
63    ///
64    /// - AWS S3: `https://s3.amazonaws.com` or `https://s3.{region}.amazonaws.com`
65    /// - Cloudflare R2: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`
66    /// - Aliyun OSS: `https://{region}.aliyuncs.com`
67    /// - Tencent COS: `https://cos.{region}.myqcloud.com`
68    /// - Minio: `http://127.0.0.1:9000`
69    ///
70    /// If user inputs endpoint without scheme like "s3.amazonaws.com", we
71    /// will prepend "https://" before it.
72    ///
73    /// - If endpoint is set, we will take user's input first.
74    /// - If not, we will try to load it from environment.
75    /// - If still not set, default to `https://s3.amazonaws.com`.
76    ///
77    /// <!-- @group General -->
78    /// <!-- @default https://s3.amazonaws.com -->
79    #[serde(
80        alias = "aws_endpoint",
81        alias = "aws_endpoint_url",
82        alias = "endpoint_url"
83    )]
84    pub endpoint: Option<String>,
85    /// Region represent the signing region of this endpoint. This is required
86    /// if you are using the default AWS S3 endpoint.
87    ///
88    /// If using a custom endpoint,
89    /// - If region is set, we will take user's input first.
90    /// - If not, we will try to load it from environment.
91    ///
92    /// <!-- @group General -->
93    /// <!-- @example us-east-1 -->
94    #[serde(alias = "aws_region")]
95    pub region: Option<String>,
96
97    /// access_key_id of this backend.
98    ///
99    /// - If access_key_id is set, we will take user's input first.
100    /// - If not, we will try to load it from environment.
101    ///
102    /// <!-- @group Credentials -->
103    #[serde(alias = "aws_access_key_id")]
104    pub access_key_id: Option<String>,
105    /// secret_access_key of this backend.
106    ///
107    /// - If secret_access_key is set, we will take user's input first.
108    /// - If not, we will try to load it from environment.
109    ///
110    /// <!-- @group Credentials -->
111    #[serde(alias = "aws_secret_access_key")]
112    pub secret_access_key: Option<String>,
113    /// session_token (aka, security token) of this backend.
114    ///
115    /// This token will expire after sometime, it's recommended to set session_token
116    /// by hand.
117    ///
118    /// <!-- @group Credentials -->
119    #[serde(alias = "aws_session_token", alias = "aws_token", alias = "token")]
120    pub session_token: Option<String>,
121    /// role_arn for this backend.
122    ///
123    /// If `role_arn` is set, we will use already known config as source
124    /// credential to assume role with `role_arn`.
125    ///
126    /// <!-- @group Assume role -->
127    pub role_arn: Option<String>,
128    /// external_id for this backend.
129    ///
130    /// <!-- @group Assume role -->
131    pub external_id: Option<String>,
132    /// role_session_name for this backend.
133    ///
134    /// <!-- @group Assume role -->
135    pub role_session_name: Option<String>,
136    /// assume_role_duration_seconds for this backend.
137    ///
138    /// <!-- @group Assume role -->
139    pub assume_role_duration_seconds: Option<u32>,
140    /// assume_role_session_tags for this backend.
141    ///
142    /// <!-- @group Assume role -->
143    pub assume_role_session_tags: Option<HashMap<String, String>>,
144    /// Disable config load so that opendal will not load config from
145    /// environment.
146    ///
147    /// For examples:
148    ///
149    /// - envs like `AWS_ACCESS_KEY_ID`
150    /// - files like `~/.aws/config`
151    ///
152    /// <!-- @group Credentials -->
153    pub disable_config_load: bool,
154    /// Disable load credential from ec2 metadata.
155    ///
156    /// This option is used to disable the default behavior of opendal
157    /// to load credential from ec2 metadata, a.k.a., IMDSv2
158    ///
159    /// <!-- @group Credentials -->
160    pub disable_ec2_metadata: bool,
161    /// Skip signature will skip loading credentials and signing requests.
162    ///
163    /// <!-- @group Credentials -->
164    pub skip_signature: bool,
165    /// Allow anonymous will allow opendal to send request without signing
166    /// when credential is not loaded.
167    ///
168    /// <!-- @group Deprecated -->
169    #[deprecated(
170        since = "0.57.0",
171        note = "Please use `skip_signature` instead of `allow_anonymous`"
172    )]
173    pub allow_anonymous: bool,
174    /// server_side_encryption for this backend.
175    ///
176    /// Available values: `AES256`, `aws:kms`.
177    ///
178    /// <!-- @group Encryption -->
179    #[serde(alias = "aws_server_side_encryption")]
180    pub server_side_encryption: Option<String>,
181    /// server_side_encryption_aws_kms_key_id for this backend
182    ///
183    /// - If `server_side_encryption` set to `aws:kms`, and `server_side_encryption_aws_kms_key_id`
184    ///   is not set, S3 will use aws managed kms key to encrypt data.
185    /// - If `server_side_encryption` set to `aws:kms`, and `server_side_encryption_aws_kms_key_id`
186    ///   is a valid kms key id, S3 will use the provided kms key to encrypt data.
187    /// - If the `server_side_encryption_aws_kms_key_id` is invalid or not found, an error will be
188    ///   returned.
189    /// - If `server_side_encryption` is not `aws:kms`, setting `server_side_encryption_aws_kms_key_id`
190    ///   is a noop.
191    ///
192    /// <!-- @group Encryption -->
193    #[serde(alias = "aws_sse_kms_key_id")]
194    pub server_side_encryption_aws_kms_key_id: Option<String>,
195    /// server_side_encryption_customer_algorithm for this backend.
196    ///
197    /// Available values: `AES256`.
198    ///
199    /// <!-- @group Encryption -->
200    pub server_side_encryption_customer_algorithm: Option<String>,
201    /// server_side_encryption_customer_key for this backend.
202    ///
203    /// Value: BASE64-encoded key that matches algorithm specified in
204    /// `server_side_encryption_customer_algorithm`.
205    ///
206    /// <!-- @group Encryption -->
207    #[serde(alias = "aws_sse_customer_key_base64")]
208    pub server_side_encryption_customer_key: Option<String>,
209    /// Set server_side_encryption_customer_key_md5 for this backend.
210    ///
211    /// Value: MD5 digest of key specified in `server_side_encryption_customer_key`.
212    ///
213    /// <!-- @group Encryption -->
214    pub server_side_encryption_customer_key_md5: Option<String>,
215    /// default storage_class for this backend.
216    ///
217    /// Available values:
218    /// - `DEEP_ARCHIVE`
219    /// - `GLACIER`
220    /// - `GLACIER_IR`
221    /// - `INTELLIGENT_TIERING`
222    /// - `ONEZONE_IA`
223    /// - `EXPRESS_ONEZONE`
224    /// - `OUTPOSTS`
225    /// - `REDUCED_REDUNDANCY`
226    /// - `STANDARD`
227    /// - `STANDARD_IA`
228    ///
229    /// S3 compatible services don't support all of them
230    ///
231    /// <!-- @group Behavior -->
232    pub default_storage_class: Option<String>,
233    /// Enable virtual host style so that opendal will send API requests
234    /// in virtual host style instead of path style.
235    ///
236    /// - By default, opendal will send API to `https://s3.us-east-1.amazonaws.com/bucket_name`
237    /// - Enabled, opendal will send API to `https://bucket_name.s3.us-east-1.amazonaws.com`
238    ///
239    /// <!-- @group Behavior -->
240    #[serde(
241        alias = "aws_virtual_hosted_style_request",
242        alias = "virtual_hosted_style_request"
243    )]
244    pub enable_virtual_host_style: bool,
245    /// Deprecated: S3 delete batch capability is enabled by default.
246    ///
247    /// <!-- @group Deprecated -->
248    #[deprecated(
249        since = "0.57.0",
250        note = "S3 delete batch capability is enabled by default. Use CapabilityOverrideLayer to override delete_max_size for specific endpoints."
251    )]
252    pub batch_max_operations: Option<usize>,
253    /// Deprecated: S3 delete batch capability is enabled by default.
254    ///
255    /// <!-- @group Deprecated -->
256    #[deprecated(
257        since = "0.57.0",
258        note = "S3 delete batch capability is enabled by default. Use CapabilityOverrideLayer to override delete_max_size for specific endpoints."
259    )]
260    pub delete_max_size: Option<usize>,
261    /// Deprecated: S3 stat override capabilities are enabled by default.
262    ///
263    /// <!-- @group Deprecated -->
264    #[deprecated(
265        since = "0.57.0",
266        note = "S3 stat override capabilities are enabled by default. Use CapabilityOverrideLayer to override them for specific endpoints."
267    )]
268    pub disable_stat_with_override: bool,
269    /// Checksum Algorithm to use when sending checksums in HTTP headers.
270    /// This is necessary when writing to AWS S3 Buckets with Object Lock enabled for example.
271    ///
272    /// Available options:
273    /// - "crc32c"
274    /// - "md5"
275    ///
276    /// <!-- @group Behavior -->
277    #[serde(alias = "aws_checksum_algorithm")]
278    pub checksum_algorithm: Option<String>,
279    /// Deprecated: S3 write with If-Match capability is enabled by default.
280    ///
281    /// <!-- @group Deprecated -->
282    #[deprecated(
283        since = "0.57.0",
284        note = "S3 write with If-Match capability is enabled by default and this option is no longer needed."
285    )]
286    pub disable_write_with_if_match: bool,
287
288    /// Deprecated: S3 append capability is enabled by default.
289    ///
290    /// <!-- @group Deprecated -->
291    #[deprecated(
292        since = "0.57.0",
293        note = "S3 append capability is enabled by default and this option is no longer needed."
294    )]
295    pub enable_write_with_append: bool,
296
297    /// OpenDAL uses List Objects V2 by default to list objects.
298    /// However, some legacy services do not yet support V2.
299    /// This option allows users to switch back to the older List Objects V1.
300    ///
301    /// <!-- @group Behavior -->
302    pub disable_list_objects_v2: bool,
303
304    /// Indicates whether the client agrees to pay for the requests made to the S3 bucket.
305    ///
306    /// <!-- @group Behavior -->
307    #[serde(alias = "aws_request_payer", alias = "request_payer")]
308    pub enable_request_payer: bool,
309
310    /// Default ACL for new objects.
311    /// Note that some s3 services like minio do not support this option.
312    ///
313    /// <!-- @group Behavior -->
314    pub default_acl: Option<String>,
315}
316
317impl Debug for S3Config {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.debug_struct("S3Config")
320            .field("root", &self.root)
321            .field("bucket", &self.bucket)
322            .field("endpoint", &self.endpoint)
323            .field("region", &self.region)
324            .finish_non_exhaustive()
325    }
326}
327
328impl Configurator for S3Config {
329    type Builder = S3Builder;
330
331    fn from_uri(uri: &OperatorUri) -> Result<Self> {
332        let mut map = uri.options().clone();
333
334        if let Some(name) = uri.name() {
335            map.insert("bucket".to_string(), name.to_string());
336        }
337
338        if let Some(root) = uri.root() {
339            map.insert("root".to_string(), root.to_string());
340        }
341
342        Self::from_iter(map)
343    }
344
345    #[allow(deprecated)]
346    fn into_builder(self) -> Self::Builder {
347        S3Builder {
348            config: self,
349            credential_providers: None,
350        }
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use std::iter;
357
358    use super::*;
359    use opendal_core::Configurator;
360    use opendal_core::OperatorUri;
361
362    #[test]
363    fn test_s3_config_original_field_names() {
364        let json = r#"{
365            "bucket": "test-bucket",
366            "access_key_id": "test-key",
367            "secret_access_key": "test-secret",
368            "region": "us-west-2",
369            "endpoint": "https://s3.amazonaws.com",
370            "session_token": "test-token"
371        }"#;
372
373        let config: S3Config = serde_json::from_str(json).unwrap();
374        assert_eq!(config.bucket, "test-bucket");
375        assert_eq!(config.access_key_id, Some("test-key".to_string()));
376        assert_eq!(config.secret_access_key, Some("test-secret".to_string()));
377        assert_eq!(config.region, Some("us-west-2".to_string()));
378        assert_eq!(
379            config.endpoint,
380            Some("https://s3.amazonaws.com".to_string())
381        );
382        assert_eq!(config.session_token, Some("test-token".to_string()));
383    }
384
385    #[test]
386    fn test_s3_config_aws_prefixed_aliases() {
387        let json = r#"{
388            "aws_bucket": "test-bucket",
389            "aws_access_key_id": "test-key",
390            "aws_secret_access_key": "test-secret",
391            "aws_region": "us-west-2",
392            "aws_endpoint": "https://s3.amazonaws.com",
393            "aws_session_token": "test-token"
394        }"#;
395
396        let config: S3Config = serde_json::from_str(json).unwrap();
397        assert_eq!(config.bucket, "test-bucket");
398        assert_eq!(config.access_key_id, Some("test-key".to_string()));
399        assert_eq!(config.secret_access_key, Some("test-secret".to_string()));
400        assert_eq!(config.region, Some("us-west-2".to_string()));
401        assert_eq!(
402            config.endpoint,
403            Some("https://s3.amazonaws.com".to_string())
404        );
405        assert_eq!(config.session_token, Some("test-token".to_string()));
406    }
407
408    #[test]
409    fn test_s3_config_additional_aliases() {
410        let json = r#"{
411            "bucket_name": "test-bucket",
412            "token": "test-token",
413            "endpoint_url": "https://s3.amazonaws.com",
414            "virtual_hosted_style_request": true,
415            "aws_checksum_algorithm": "crc32c",
416            "request_payer": true
417        }"#;
418
419        let config: S3Config = serde_json::from_str(json).unwrap();
420        assert_eq!(config.bucket, "test-bucket");
421        assert_eq!(config.session_token, Some("test-token".to_string()));
422        assert_eq!(
423            config.endpoint,
424            Some("https://s3.amazonaws.com".to_string())
425        );
426        assert!(config.enable_virtual_host_style);
427        assert_eq!(config.checksum_algorithm, Some("crc32c".to_string()));
428        assert!(config.enable_request_payer);
429    }
430
431    #[test]
432    fn test_s3_config_encryption_aliases() {
433        let json = r#"{
434            "bucket": "test-bucket",
435            "aws_server_side_encryption": "aws:kms",
436            "aws_sse_kms_key_id": "test-kms-key",
437            "aws_sse_customer_key_base64": "dGVzdC1jdXN0b21lci1rZXk="
438        }"#;
439
440        let config: S3Config = serde_json::from_str(json).unwrap();
441        assert_eq!(config.bucket, "test-bucket");
442        assert_eq!(config.server_side_encryption, Some("aws:kms".to_string()));
443        assert_eq!(
444            config.server_side_encryption_aws_kms_key_id,
445            Some("test-kms-key".to_string())
446        );
447        assert_eq!(
448            config.server_side_encryption_customer_key,
449            Some("dGVzdC1jdXN0b21lci1rZXk=".to_string())
450        );
451    }
452
453    #[test]
454    fn from_uri_extracts_bucket_and_root() {
455        let uri = OperatorUri::new("s3://example-bucket/path/to/root", iter::empty()).unwrap();
456        let cfg = S3Config::from_uri(&uri).unwrap();
457        assert_eq!(cfg.bucket, "example-bucket");
458        assert_eq!(cfg.root.as_deref(), Some("path/to/root"));
459    }
460
461    #[test]
462    fn from_uri_extracts_endpoint() {
463        let uri = OperatorUri::new(
464            "s3://example-bucket/path/to/root?endpoint=https%3A%2F%2Fcustom-s3-endpoint.com",
465            iter::empty(),
466        )
467        .unwrap();
468        let cfg = S3Config::from_uri(&uri).unwrap();
469        assert_eq!(cfg.bucket, "example-bucket");
470        assert_eq!(cfg.root.as_deref(), Some("path/to/root"));
471        assert_eq!(
472            cfg.endpoint.as_deref(),
473            Some("https://custom-s3-endpoint.com")
474        );
475    }
476}