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::observer::RequestObserver;
13use crate::retry::RetryPolicy;
14
15pub use crate::endpoint::AddressingStyle;
16
17#[derive(Clone)]
19pub struct S3Config {
20 endpoint: Endpoint,
21 region: String,
22 bucket: String,
23 addressing_style: AddressingStyle,
24 connect_timeout: Duration,
25 attempt_timeout: Duration,
26 operation_timeout: Duration,
27 idle_body_timeout: Duration,
28 max_xml_response_size: usize,
29 max_error_response_size: usize,
30 retry_policy: RetryPolicy,
31 user_agent: String,
32 credentials_provider: Arc<dyn CredentialsProvider>,
33 observer: Option<Arc<dyn RequestObserver>>,
34}
35
36impl S3Config {
37 pub fn builder() -> S3ConfigBuilder {
39 S3ConfigBuilder::default()
40 }
41
42 pub fn endpoint(&self) -> &Endpoint {
44 &self.endpoint
45 }
46
47 pub fn region(&self) -> &str {
49 &self.region
50 }
51
52 pub fn bucket(&self) -> &str {
54 &self.bucket
55 }
56
57 pub fn addressing_style(&self) -> AddressingStyle {
59 self.addressing_style
60 }
61
62 pub fn connect_timeout(&self) -> Duration {
64 self.connect_timeout
65 }
66
67 pub fn attempt_timeout(&self) -> Duration {
69 self.attempt_timeout
70 }
71
72 pub fn operation_timeout(&self) -> Duration {
74 self.operation_timeout
75 }
76
77 pub fn idle_body_timeout(&self) -> Duration {
79 self.idle_body_timeout
80 }
81
82 pub fn max_xml_response_size(&self) -> usize {
84 self.max_xml_response_size
85 }
86
87 pub fn max_error_response_size(&self) -> usize {
89 self.max_error_response_size
90 }
91
92 pub fn retry_policy(&self) -> &RetryPolicy {
94 &self.retry_policy
95 }
96
97 pub fn user_agent(&self) -> &str {
99 &self.user_agent
100 }
101
102 pub fn credentials_provider(&self) -> &Arc<dyn CredentialsProvider> {
104 &self.credentials_provider
105 }
106
107 pub fn observer(&self) -> Option<&Arc<dyn RequestObserver>> {
109 self.observer.as_ref()
110 }
111
112 pub fn for_bucket(&self, bucket: impl Into<String>) -> Result<Self, S3Error> {
123 let bucket = bucket.into();
124 if bucket.is_empty() {
125 return Err(S3Error::configuration("bucket must not be empty"));
126 }
127 self.endpoint
128 .object_url(&bucket, None, self.addressing_style)?;
129 let mut config = self.clone();
130 config.bucket = bucket;
131 Ok(config)
132 }
133}
134
135impl fmt::Debug for S3Config {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter
138 .debug_struct("S3Config")
139 .field("endpoint", &self.endpoint)
140 .field("region", &self.region)
141 .field("bucket", &self.bucket)
142 .field("addressing_style", &self.addressing_style)
143 .field("connect_timeout", &self.connect_timeout)
144 .field("attempt_timeout", &self.attempt_timeout)
145 .field("operation_timeout", &self.operation_timeout)
146 .field("idle_body_timeout", &self.idle_body_timeout)
147 .field("max_xml_response_size", &self.max_xml_response_size)
148 .field("max_error_response_size", &self.max_error_response_size)
149 .field("retry_policy", &self.retry_policy)
150 .field("user_agent", &self.user_agent)
151 .field("credentials_provider", &"[REDACTED]")
152 .field("observer", &self.observer.as_ref().map(|_| "configured"))
153 .finish()
154 }
155}
156
157pub struct S3ConfigBuilder {
159 endpoint: Endpoint,
160 region: String,
161 bucket: Option<String>,
162 addressing_style: AddressingStyle,
163 allow_http: bool,
164 connect_timeout: Duration,
165 attempt_timeout: Duration,
166 operation_timeout: Duration,
167 idle_body_timeout: Duration,
168 max_xml_response_size: usize,
169 max_error_response_size: usize,
170 retry_policy: RetryPolicy,
171 user_agent: String,
172 credentials_provider: Arc<dyn CredentialsProvider>,
173 observer: Option<Arc<dyn RequestObserver>>,
174}
175
176impl S3ConfigBuilder {
177 pub fn endpoint(mut self, endpoint: Endpoint) -> Self {
179 self.endpoint = endpoint;
180 self
181 }
182
183 pub fn region(mut self, region: impl Into<String>) -> Self {
185 self.region = region.into();
186 self
187 }
188
189 pub fn bucket(mut self, bucket: impl Into<String>) -> Self {
191 self.bucket = Some(bucket.into());
192 self
193 }
194
195 pub fn addressing_style(mut self, style: AddressingStyle) -> Self {
197 self.addressing_style = style;
198 self
199 }
200
201 pub fn allow_http_for_local_testing(mut self) -> Self {
206 self.allow_http = true;
207 self
208 }
209
210 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
212 self.connect_timeout = timeout;
213 self
214 }
215
216 pub fn attempt_timeout(mut self, timeout: Duration) -> Self {
218 self.attempt_timeout = timeout;
219 self
220 }
221
222 pub fn operation_timeout(mut self, timeout: Duration) -> Self {
227 self.operation_timeout = timeout;
228 self
229 }
230
231 pub fn idle_body_timeout(mut self, timeout: Duration) -> Self {
233 self.idle_body_timeout = timeout;
234 self
235 }
236
237 pub fn max_xml_response_size(mut self, bytes: usize) -> Self {
239 self.max_xml_response_size = bytes;
240 self
241 }
242
243 pub fn max_error_response_size(mut self, bytes: usize) -> Self {
245 self.max_error_response_size = bytes;
246 self
247 }
248
249 pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
251 self.retry_policy = policy;
252 self
253 }
254
255 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
257 self.user_agent = user_agent.into();
258 self
259 }
260
261 pub fn credentials_provider(mut self, provider: Arc<dyn CredentialsProvider>) -> Self {
263 self.credentials_provider = provider;
264 self
265 }
266
267 pub fn observer(mut self, observer: Arc<dyn RequestObserver>) -> Self {
269 self.observer = Some(observer);
270 self
271 }
272
273 pub fn build(self) -> Result<S3Config, S3Error> {
275 let bucket = self
276 .bucket
277 .ok_or_else(|| S3Error::configuration("bucket is required"))?;
278 if !self.endpoint.is_https() && !self.allow_http {
279 return Err(S3Error::configuration(
280 "plain HTTP requires allow_http_for_local_testing",
281 ));
282 }
283 validate_nonempty_token("region", &self.region)?;
284 if bucket.is_empty() {
285 return Err(S3Error::configuration("bucket must not be empty"));
286 }
287 for (name, timeout) in [
288 ("connect timeout", self.connect_timeout),
289 ("attempt timeout", self.attempt_timeout),
290 ("operation timeout", self.operation_timeout),
291 ("idle body timeout", self.idle_body_timeout),
292 ] {
293 if timeout.is_zero() {
294 return Err(S3Error::configuration(format!(
295 "{name} must be greater than zero"
296 )));
297 }
298 if std::time::Instant::now().checked_add(timeout).is_none() {
299 return Err(S3Error::configuration(format!(
300 "{name} is too large to represent as a deadline"
301 )));
302 }
303 }
304 if self.max_xml_response_size == 0 || self.max_error_response_size == 0 {
305 return Err(S3Error::configuration(
306 "response body limits must be greater than zero",
307 ));
308 }
309 HeaderValue::from_str(&self.user_agent)
310 .map_err(|_| S3Error::configuration("user agent is not a valid HTTP header value"))?;
311 if self.user_agent.is_empty() {
312 return Err(S3Error::configuration("user agent must not be empty"));
313 }
314
315 self.endpoint
317 .object_url(&bucket, None, self.addressing_style)?;
318
319 Ok(S3Config {
320 endpoint: self.endpoint,
321 region: self.region,
322 bucket,
323 addressing_style: self.addressing_style,
324 connect_timeout: self.connect_timeout,
325 attempt_timeout: self.attempt_timeout,
326 operation_timeout: self.operation_timeout,
327 idle_body_timeout: self.idle_body_timeout,
328 max_xml_response_size: self.max_xml_response_size,
329 max_error_response_size: self.max_error_response_size,
330 retry_policy: self.retry_policy,
331 user_agent: self.user_agent,
332 credentials_provider: self.credentials_provider,
333 observer: self.observer,
334 })
335 }
336}
337
338impl Default for S3ConfigBuilder {
339 fn default() -> Self {
340 Self {
341 endpoint: Endpoint::default(),
342 region: "us-east-1".to_owned(),
343 bucket: None,
344 addressing_style: AddressingStyle::Path,
345 allow_http: false,
346 connect_timeout: Duration::from_secs(10),
347 attempt_timeout: Duration::from_secs(30),
348 operation_timeout: Duration::from_secs(5 * 60),
349 idle_body_timeout: Duration::from_secs(30),
350 max_xml_response_size: 1024 * 1024,
351 max_error_response_size: 64 * 1024,
352 retry_policy: RetryPolicy::default(),
353 user_agent: format!("s3-wire/{}", env!("CARGO_PKG_VERSION")),
354 credentials_provider: Arc::new(EnvironmentCredentialsProvider::new()),
355 observer: None,
356 }
357 }
358}
359
360fn validate_nonempty_token(name: &str, value: &str) -> Result<(), S3Error> {
361 if value.is_empty()
362 || !value
363 .bytes()
364 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
365 {
366 return Err(S3Error::configuration(format!("{name} is invalid")));
367 }
368 Ok(())
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn https_is_the_default_and_http_requires_opt_in() {
377 let default_config = S3Config::builder().bucket("bucket").build().unwrap();
378 assert!(default_config.endpoint().is_https());
379
380 let endpoint = Endpoint::new("http://127.0.0.1:9000").unwrap();
381 assert!(
382 S3Config::builder()
383 .endpoint(endpoint.clone())
384 .bucket("bucket")
385 .build()
386 .is_err()
387 );
388 assert!(
389 S3Config::builder()
390 .endpoint(endpoint)
391 .allow_http_for_local_testing()
392 .bucket("bucket")
393 .build()
394 .is_ok()
395 );
396 }
397
398 #[test]
399 fn timeouts_must_fit_in_an_instant_deadline() {
400 assert!(
401 S3Config::builder()
402 .bucket("bucket")
403 .operation_timeout(Duration::MAX)
404 .build()
405 .is_err()
406 );
407 }
408}