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
16#[derive(Clone)]
18pub struct S3Config {
19 endpoint: Endpoint,
20 region: String,
21 bucket: String,
22 addressing_style: AddressingStyle,
23 connect_timeout: Duration,
24 attempt_timeout: Duration,
25 operation_timeout: Duration,
26 idle_body_timeout: Duration,
27 max_xml_response_size: usize,
28 max_error_response_size: usize,
29 retry_policy: RetryPolicy,
30 user_agent: String,
31 credentials_provider: Arc<dyn CredentialsProvider>,
32}
33
34impl S3Config {
35 pub fn builder() -> S3ConfigBuilder {
37 S3ConfigBuilder::default()
38 }
39
40 pub fn endpoint(&self) -> &Endpoint {
42 &self.endpoint
43 }
44
45 pub fn region(&self) -> &str {
47 &self.region
48 }
49
50 pub fn bucket(&self) -> &str {
52 &self.bucket
53 }
54
55 pub fn addressing_style(&self) -> AddressingStyle {
57 self.addressing_style
58 }
59
60 pub fn connect_timeout(&self) -> Duration {
62 self.connect_timeout
63 }
64
65 pub fn attempt_timeout(&self) -> Duration {
67 self.attempt_timeout
68 }
69
70 pub fn operation_timeout(&self) -> Duration {
72 self.operation_timeout
73 }
74
75 pub fn idle_body_timeout(&self) -> Duration {
77 self.idle_body_timeout
78 }
79
80 pub fn max_xml_response_size(&self) -> usize {
82 self.max_xml_response_size
83 }
84
85 pub fn max_error_response_size(&self) -> usize {
87 self.max_error_response_size
88 }
89
90 pub fn retry_policy(&self) -> &RetryPolicy {
92 &self.retry_policy
93 }
94
95 pub fn user_agent(&self) -> &str {
97 &self.user_agent
98 }
99
100 pub fn credentials_provider(&self) -> &Arc<dyn CredentialsProvider> {
102 &self.credentials_provider
103 }
104}
105
106impl fmt::Debug for S3Config {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 formatter
109 .debug_struct("S3Config")
110 .field("endpoint", &self.endpoint)
111 .field("region", &self.region)
112 .field("bucket", &self.bucket)
113 .field("addressing_style", &self.addressing_style)
114 .field("connect_timeout", &self.connect_timeout)
115 .field("attempt_timeout", &self.attempt_timeout)
116 .field("operation_timeout", &self.operation_timeout)
117 .field("idle_body_timeout", &self.idle_body_timeout)
118 .field("max_xml_response_size", &self.max_xml_response_size)
119 .field("max_error_response_size", &self.max_error_response_size)
120 .field("retry_policy", &self.retry_policy)
121 .field("user_agent", &self.user_agent)
122 .field("credentials_provider", &"[REDACTED]")
123 .finish()
124 }
125}
126
127pub struct S3ConfigBuilder {
129 endpoint: Endpoint,
130 region: String,
131 bucket: Option<String>,
132 addressing_style: AddressingStyle,
133 allow_http: bool,
134 connect_timeout: Duration,
135 attempt_timeout: Duration,
136 operation_timeout: Duration,
137 idle_body_timeout: Duration,
138 max_xml_response_size: usize,
139 max_error_response_size: usize,
140 retry_policy: RetryPolicy,
141 user_agent: String,
142 credentials_provider: Arc<dyn CredentialsProvider>,
143}
144
145impl S3ConfigBuilder {
146 pub fn endpoint(mut self, endpoint: Endpoint) -> Self {
148 self.endpoint = endpoint;
149 self
150 }
151
152 pub fn region(mut self, region: impl Into<String>) -> Self {
154 self.region = region.into();
155 self
156 }
157
158 pub fn bucket(mut self, bucket: impl Into<String>) -> Self {
160 self.bucket = Some(bucket.into());
161 self
162 }
163
164 pub fn addressing_style(mut self, style: AddressingStyle) -> Self {
166 self.addressing_style = style;
167 self
168 }
169
170 pub fn allow_http_for_local_testing(mut self) -> Self {
175 self.allow_http = true;
176 self
177 }
178
179 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
181 self.connect_timeout = timeout;
182 self
183 }
184
185 pub fn attempt_timeout(mut self, timeout: Duration) -> Self {
187 self.attempt_timeout = timeout;
188 self
189 }
190
191 pub fn operation_timeout(mut self, timeout: Duration) -> Self {
196 self.operation_timeout = timeout;
197 self
198 }
199
200 pub fn idle_body_timeout(mut self, timeout: Duration) -> Self {
202 self.idle_body_timeout = timeout;
203 self
204 }
205
206 pub fn max_xml_response_size(mut self, bytes: usize) -> Self {
208 self.max_xml_response_size = bytes;
209 self
210 }
211
212 pub fn max_error_response_size(mut self, bytes: usize) -> Self {
214 self.max_error_response_size = bytes;
215 self
216 }
217
218 pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
220 self.retry_policy = policy;
221 self
222 }
223
224 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
226 self.user_agent = user_agent.into();
227 self
228 }
229
230 pub fn credentials_provider(mut self, provider: Arc<dyn CredentialsProvider>) -> Self {
232 self.credentials_provider = provider;
233 self
234 }
235
236 pub fn build(self) -> Result<S3Config, S3Error> {
238 let bucket = self
239 .bucket
240 .ok_or_else(|| S3Error::configuration("bucket is required"))?;
241 if !self.endpoint.is_https() && !self.allow_http {
242 return Err(S3Error::configuration(
243 "plain HTTP requires allow_http_for_local_testing",
244 ));
245 }
246 validate_nonempty_token("region", &self.region)?;
247 if bucket.is_empty() {
248 return Err(S3Error::configuration("bucket must not be empty"));
249 }
250 for (name, timeout) in [
251 ("connect timeout", self.connect_timeout),
252 ("attempt timeout", self.attempt_timeout),
253 ("operation timeout", self.operation_timeout),
254 ("idle body timeout", self.idle_body_timeout),
255 ] {
256 if timeout.is_zero() {
257 return Err(S3Error::configuration(format!(
258 "{name} must be greater than zero"
259 )));
260 }
261 if std::time::Instant::now().checked_add(timeout).is_none() {
262 return Err(S3Error::configuration(format!(
263 "{name} is too large to represent as a deadline"
264 )));
265 }
266 }
267 if self.max_xml_response_size == 0 || self.max_error_response_size == 0 {
268 return Err(S3Error::configuration(
269 "response body limits must be greater than zero",
270 ));
271 }
272 HeaderValue::from_str(&self.user_agent)
273 .map_err(|_| S3Error::configuration("user agent is not a valid HTTP header value"))?;
274 if self.user_agent.is_empty() {
275 return Err(S3Error::configuration("user agent must not be empty"));
276 }
277
278 self.endpoint
280 .object_url(&bucket, None, self.addressing_style)?;
281
282 Ok(S3Config {
283 endpoint: self.endpoint,
284 region: self.region,
285 bucket,
286 addressing_style: self.addressing_style,
287 connect_timeout: self.connect_timeout,
288 attempt_timeout: self.attempt_timeout,
289 operation_timeout: self.operation_timeout,
290 idle_body_timeout: self.idle_body_timeout,
291 max_xml_response_size: self.max_xml_response_size,
292 max_error_response_size: self.max_error_response_size,
293 retry_policy: self.retry_policy,
294 user_agent: self.user_agent,
295 credentials_provider: self.credentials_provider,
296 })
297 }
298}
299
300impl Default for S3ConfigBuilder {
301 fn default() -> Self {
302 Self {
303 endpoint: Endpoint::default(),
304 region: "us-east-1".to_owned(),
305 bucket: None,
306 addressing_style: AddressingStyle::Path,
307 allow_http: false,
308 connect_timeout: Duration::from_secs(10),
309 attempt_timeout: Duration::from_secs(30),
310 operation_timeout: Duration::from_secs(5 * 60),
311 idle_body_timeout: Duration::from_secs(30),
312 max_xml_response_size: 1024 * 1024,
313 max_error_response_size: 64 * 1024,
314 retry_policy: RetryPolicy::default(),
315 user_agent: format!("s3-wire/{}", env!("CARGO_PKG_VERSION")),
316 credentials_provider: Arc::new(EnvironmentCredentialsProvider::new()),
317 }
318 }
319}
320
321fn validate_nonempty_token(name: &str, value: &str) -> Result<(), S3Error> {
322 if value.is_empty()
323 || !value
324 .bytes()
325 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
326 {
327 return Err(S3Error::configuration(format!("{name} is invalid")));
328 }
329 Ok(())
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn https_is_the_default_and_http_requires_opt_in() {
338 let default_config = S3Config::builder().bucket("bucket").build().unwrap();
339 assert!(default_config.endpoint().is_https());
340
341 let endpoint = Endpoint::new("http://127.0.0.1:9000").unwrap();
342 assert!(
343 S3Config::builder()
344 .endpoint(endpoint.clone())
345 .bucket("bucket")
346 .build()
347 .is_err()
348 );
349 assert!(
350 S3Config::builder()
351 .endpoint(endpoint)
352 .allow_http_for_local_testing()
353 .bucket("bucket")
354 .build()
355 .is_ok()
356 );
357 }
358
359 #[test]
360 fn timeouts_must_fit_in_an_instant_deadline() {
361 assert!(
362 S3Config::builder()
363 .bucket("bucket")
364 .operation_timeout(Duration::MAX)
365 .build()
366 .is_err()
367 );
368 }
369}