1use std::fmt;
4use std::sync::Arc;
5use std::time::Duration;
6
7use http::HeaderValue;
8
9use crate::credentials::{CredentialsProvider, EnvironmentCredentialsProvider};
10use crate::endpoint::Endpoint;
11use crate::error::S3Error;
12use crate::retry::RetryPolicy;
13
14pub use crate::endpoint::AddressingStyle;
15
16const MIN_MULTIPART_PART_SIZE: u64 = 5 * 1024 * 1024;
17const MAX_MULTIPART_PART_SIZE: u64 = 5 * 1024 * 1024 * 1024;
18const MAX_MULTIPART_CONCURRENCY: usize = 64;
19
20#[derive(Clone)]
22pub struct S3Config {
23 endpoint: Endpoint,
24 region: String,
25 bucket: String,
26 addressing_style: AddressingStyle,
27 connect_timeout: Duration,
28 attempt_timeout: Duration,
29 operation_timeout: Duration,
30 idle_body_timeout: Duration,
31 max_xml_response_size: usize,
32 max_error_response_size: usize,
33 retry_policy: RetryPolicy,
34 multipart_threshold: u64,
35 multipart_part_size: u64,
36 multipart_concurrency: usize,
37 max_multipart_in_flight_bytes: u64,
38 user_agent: String,
39 credentials_provider: Arc<dyn CredentialsProvider>,
40}
41
42impl S3Config {
43 pub fn builder() -> S3ConfigBuilder {
45 S3ConfigBuilder::default()
46 }
47
48 pub fn endpoint(&self) -> &Endpoint {
50 &self.endpoint
51 }
52
53 pub fn region(&self) -> &str {
55 &self.region
56 }
57
58 pub fn bucket(&self) -> &str {
60 &self.bucket
61 }
62
63 pub fn addressing_style(&self) -> AddressingStyle {
65 self.addressing_style
66 }
67
68 pub fn connect_timeout(&self) -> Duration {
70 self.connect_timeout
71 }
72
73 pub fn attempt_timeout(&self) -> Duration {
75 self.attempt_timeout
76 }
77
78 pub fn operation_timeout(&self) -> Duration {
80 self.operation_timeout
81 }
82
83 pub fn idle_body_timeout(&self) -> Duration {
85 self.idle_body_timeout
86 }
87
88 pub fn max_xml_response_size(&self) -> usize {
90 self.max_xml_response_size
91 }
92
93 pub fn max_error_response_size(&self) -> usize {
95 self.max_error_response_size
96 }
97
98 pub fn retry_policy(&self) -> &RetryPolicy {
100 &self.retry_policy
101 }
102
103 pub fn multipart_threshold(&self) -> u64 {
105 self.multipart_threshold
106 }
107
108 pub fn multipart_part_size(&self) -> u64 {
110 self.multipart_part_size
111 }
112
113 pub fn multipart_concurrency(&self) -> usize {
115 self.multipart_concurrency
116 }
117
118 pub fn max_multipart_in_flight_bytes(&self) -> u64 {
120 self.max_multipart_in_flight_bytes
121 }
122
123 pub fn user_agent(&self) -> &str {
125 &self.user_agent
126 }
127
128 pub fn credentials_provider(&self) -> &Arc<dyn CredentialsProvider> {
130 &self.credentials_provider
131 }
132}
133
134impl fmt::Debug for S3Config {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 formatter
137 .debug_struct("S3Config")
138 .field("endpoint", &self.endpoint)
139 .field("region", &self.region)
140 .field("bucket", &self.bucket)
141 .field("addressing_style", &self.addressing_style)
142 .field("connect_timeout", &self.connect_timeout)
143 .field("attempt_timeout", &self.attempt_timeout)
144 .field("operation_timeout", &self.operation_timeout)
145 .field("idle_body_timeout", &self.idle_body_timeout)
146 .field("max_xml_response_size", &self.max_xml_response_size)
147 .field("max_error_response_size", &self.max_error_response_size)
148 .field("retry_policy", &self.retry_policy)
149 .field("multipart_threshold", &self.multipart_threshold)
150 .field("multipart_part_size", &self.multipart_part_size)
151 .field("multipart_concurrency", &self.multipart_concurrency)
152 .field(
153 "max_multipart_in_flight_bytes",
154 &self.max_multipart_in_flight_bytes,
155 )
156 .field("user_agent", &self.user_agent)
157 .field("credentials_provider", &"[REDACTED]")
158 .finish()
159 }
160}
161
162pub struct S3ConfigBuilder {
164 endpoint: Endpoint,
165 region: String,
166 bucket: Option<String>,
167 addressing_style: AddressingStyle,
168 allow_http: bool,
169 connect_timeout: Duration,
170 attempt_timeout: Duration,
171 operation_timeout: Duration,
172 idle_body_timeout: Duration,
173 max_xml_response_size: usize,
174 max_error_response_size: usize,
175 retry_policy: RetryPolicy,
176 multipart_threshold: u64,
177 multipart_part_size: u64,
178 multipart_concurrency: usize,
179 max_multipart_in_flight_bytes: u64,
180 user_agent: String,
181 credentials_provider: Arc<dyn CredentialsProvider>,
182}
183
184impl S3ConfigBuilder {
185 pub fn endpoint(mut self, endpoint: Endpoint) -> Self {
187 self.endpoint = endpoint;
188 self
189 }
190
191 pub fn region(mut self, region: impl Into<String>) -> Self {
193 self.region = region.into();
194 self
195 }
196
197 pub fn bucket(mut self, bucket: impl Into<String>) -> Self {
199 self.bucket = Some(bucket.into());
200 self
201 }
202
203 pub fn addressing_style(mut self, style: AddressingStyle) -> Self {
205 self.addressing_style = style;
206 self
207 }
208
209 pub fn allow_http_for_local_testing(mut self) -> Self {
214 self.allow_http = true;
215 self
216 }
217
218 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
220 self.connect_timeout = timeout;
221 self
222 }
223
224 pub fn attempt_timeout(mut self, timeout: Duration) -> Self {
226 self.attempt_timeout = timeout;
227 self
228 }
229
230 pub fn operation_timeout(mut self, timeout: Duration) -> Self {
232 self.operation_timeout = timeout;
233 self
234 }
235
236 pub fn idle_body_timeout(mut self, timeout: Duration) -> Self {
238 self.idle_body_timeout = timeout;
239 self
240 }
241
242 pub fn max_xml_response_size(mut self, bytes: usize) -> Self {
244 self.max_xml_response_size = bytes;
245 self
246 }
247
248 pub fn max_error_response_size(mut self, bytes: usize) -> Self {
250 self.max_error_response_size = bytes;
251 self
252 }
253
254 pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
256 self.retry_policy = policy;
257 self
258 }
259
260 pub fn multipart_threshold(mut self, bytes: u64) -> Self {
262 self.multipart_threshold = bytes;
263 self
264 }
265
266 pub fn multipart_part_size(mut self, bytes: u64) -> Self {
268 self.multipart_part_size = bytes;
269 self
270 }
271
272 pub fn multipart_concurrency(mut self, concurrency: usize) -> Self {
274 self.multipart_concurrency = concurrency;
275 self
276 }
277
278 pub fn max_multipart_in_flight_bytes(mut self, bytes: u64) -> Self {
280 self.max_multipart_in_flight_bytes = bytes;
281 self
282 }
283
284 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
286 self.user_agent = user_agent.into();
287 self
288 }
289
290 pub fn credentials_provider(mut self, provider: Arc<dyn CredentialsProvider>) -> Self {
292 self.credentials_provider = provider;
293 self
294 }
295
296 pub fn build(self) -> Result<S3Config, S3Error> {
298 let bucket = self
299 .bucket
300 .ok_or_else(|| S3Error::configuration("bucket is required"))?;
301 if !self.endpoint.is_https() && !self.allow_http {
302 return Err(S3Error::configuration(
303 "plain HTTP requires allow_http_for_local_testing",
304 ));
305 }
306 validate_nonempty_token("region", &self.region)?;
307 if bucket.is_empty() {
308 return Err(S3Error::configuration("bucket must not be empty"));
309 }
310 for (name, timeout) in [
311 ("connect timeout", self.connect_timeout),
312 ("attempt timeout", self.attempt_timeout),
313 ("operation timeout", self.operation_timeout),
314 ("idle body timeout", self.idle_body_timeout),
315 ] {
316 if timeout.is_zero() {
317 return Err(S3Error::configuration(format!(
318 "{name} must be greater than zero"
319 )));
320 }
321 }
322 if self.max_xml_response_size == 0 || self.max_error_response_size == 0 {
323 return Err(S3Error::configuration(
324 "response body limits must be greater than zero",
325 ));
326 }
327 if !(MIN_MULTIPART_PART_SIZE..=MAX_MULTIPART_PART_SIZE).contains(&self.multipart_part_size)
328 {
329 return Err(S3Error::configuration(
330 "multipart part size must be between 5 MiB and 5 GiB",
331 ));
332 }
333 if self.multipart_threshold < self.multipart_part_size {
334 return Err(S3Error::configuration(
335 "multipart threshold must not be smaller than multipart part size",
336 ));
337 }
338 if !(1..=MAX_MULTIPART_CONCURRENCY).contains(&self.multipart_concurrency) {
339 return Err(S3Error::configuration(
340 "multipart concurrency must be between 1 and 64",
341 ));
342 }
343 let in_flight_bytes = self
344 .multipart_part_size
345 .checked_mul(
346 u64::try_from(self.multipart_concurrency).map_err(|_| {
347 S3Error::configuration("multipart concurrency does not fit in u64")
348 })?,
349 )
350 .ok_or_else(|| S3Error::configuration("multipart in-flight byte budget overflow"))?;
351 if in_flight_bytes > self.max_multipart_in_flight_bytes {
352 return Err(S3Error::configuration(
353 "multipart part size times concurrency exceeds the in-flight byte budget",
354 ));
355 }
356 HeaderValue::from_str(&self.user_agent)
357 .map_err(|_| S3Error::configuration("user agent is not a valid HTTP header value"))?;
358 if self.user_agent.is_empty() {
359 return Err(S3Error::configuration("user agent must not be empty"));
360 }
361
362 self.endpoint
364 .object_url(&bucket, None, self.addressing_style)?;
365
366 Ok(S3Config {
367 endpoint: self.endpoint,
368 region: self.region,
369 bucket,
370 addressing_style: self.addressing_style,
371 connect_timeout: self.connect_timeout,
372 attempt_timeout: self.attempt_timeout,
373 operation_timeout: self.operation_timeout,
374 idle_body_timeout: self.idle_body_timeout,
375 max_xml_response_size: self.max_xml_response_size,
376 max_error_response_size: self.max_error_response_size,
377 retry_policy: self.retry_policy,
378 multipart_threshold: self.multipart_threshold,
379 multipart_part_size: self.multipart_part_size,
380 multipart_concurrency: self.multipart_concurrency,
381 max_multipart_in_flight_bytes: self.max_multipart_in_flight_bytes,
382 user_agent: self.user_agent,
383 credentials_provider: self.credentials_provider,
384 })
385 }
386}
387
388impl Default for S3ConfigBuilder {
389 fn default() -> Self {
390 Self {
391 endpoint: Endpoint::default(),
392 region: "us-east-1".to_owned(),
393 bucket: None,
394 addressing_style: AddressingStyle::Path,
395 allow_http: false,
396 connect_timeout: Duration::from_secs(10),
397 attempt_timeout: Duration::from_secs(30),
398 operation_timeout: Duration::from_secs(5 * 60),
399 idle_body_timeout: Duration::from_secs(30),
400 max_xml_response_size: 1024 * 1024,
401 max_error_response_size: 64 * 1024,
402 retry_policy: RetryPolicy::default(),
403 multipart_threshold: 16 * 1024 * 1024,
404 multipart_part_size: 8 * 1024 * 1024,
405 multipart_concurrency: 4,
406 max_multipart_in_flight_bytes: 64 * 1024 * 1024,
407 user_agent: format!("s3-wire/{}", env!("CARGO_PKG_VERSION")),
408 credentials_provider: Arc::new(EnvironmentCredentialsProvider::new()),
409 }
410 }
411}
412
413fn validate_nonempty_token(name: &str, value: &str) -> Result<(), S3Error> {
414 if value.is_empty()
415 || !value
416 .bytes()
417 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
418 {
419 return Err(S3Error::configuration(format!("{name} is invalid")));
420 }
421 Ok(())
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn https_is_the_default_and_http_requires_opt_in() {
430 let default_config = S3Config::builder().bucket("bucket").build().unwrap();
431 assert!(default_config.endpoint().is_https());
432
433 let endpoint = Endpoint::new("http://127.0.0.1:9000").unwrap();
434 assert!(
435 S3Config::builder()
436 .endpoint(endpoint.clone())
437 .bucket("bucket")
438 .build()
439 .is_err()
440 );
441 assert!(
442 S3Config::builder()
443 .bucket("bucket")
444 .multipart_part_size(64 * 1024 * 1024)
445 .multipart_concurrency(2)
446 .build()
447 .is_err()
448 );
449 assert!(
450 S3Config::builder()
451 .endpoint(endpoint)
452 .allow_http_for_local_testing()
453 .bucket("bucket")
454 .build()
455 .is_ok()
456 );
457 }
458
459 #[test]
460 fn multipart_memory_bounds_are_validated() {
461 assert!(
462 S3Config::builder()
463 .bucket("bucket")
464 .multipart_part_size(MIN_MULTIPART_PART_SIZE - 1)
465 .build()
466 .is_err()
467 );
468 assert!(
469 S3Config::builder()
470 .bucket("bucket")
471 .multipart_concurrency(MAX_MULTIPART_CONCURRENCY + 1)
472 .build()
473 .is_err()
474 );
475 }
476}