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}