1#[cfg(feature = "blocking")]
37use block_on_proc::block_on;
38#[cfg(feature = "tags")]
39use minidom::Element;
40use std::collections::{HashMap, HashSet};
41use std::time::Duration;
42
43use crate::bucket_ops::{BucketConfiguration, CreateBucketResponse};
44use crate::command::{Command, Multipart};
45use crate::creds::Credentials;
46use crate::region::Region;
47#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
48use crate::request::ResponseDataStream;
49#[cfg(feature = "with-tokio")]
50use crate::request::tokio_backend::ClientOptions;
51#[cfg(feature = "with-tokio")]
52use crate::request::tokio_backend::client;
53use crate::request::{Request as _, ResponseData};
54use std::str::FromStr;
55use std::sync::Arc;
56
57#[cfg(feature = "with-tokio")]
58use tokio::sync::{Mutex as AsyncMutex, RwLock};
59
60#[cfg(feature = "with-async-std")]
61use async_std::sync::{Mutex as AsyncMutex, RwLock};
62
63#[cfg(feature = "sync")]
64use std::sync::RwLock;
65
66pub type Query = HashMap<String, String>;
67
68#[cfg(feature = "with-async-std")]
69use crate::request::async_std_backend::SurfRequest as RequestImpl;
70#[cfg(feature = "with-tokio")]
71use crate::request::tokio_backend::ReqwestRequest as RequestImpl;
72
73#[cfg(feature = "with-async-std")]
74use async_std::io::Write as AsyncWrite;
75#[cfg(feature = "with-tokio")]
76use tokio::io::AsyncWrite;
77
78#[cfg(feature = "sync")]
79use crate::request::blocking::AttoRequest as RequestImpl;
80use std::io::Read;
81
82#[cfg(feature = "with-tokio")]
83use tokio::io::AsyncRead;
84
85#[cfg(feature = "with-async-std")]
86use async_std::io::Read as AsyncRead;
87
88use crate::PostPolicy;
89use crate::error::S3Error;
90use crate::post_policy::PresignedPost;
91use crate::serde_types::{
92 BucketLifecycleConfiguration, BucketLocationResult, CompleteMultipartUploadData,
93 CorsConfiguration, DeleteObjectsRequest, DeleteObjectsResult, GetObjectAttributesOutput,
94 HeadObjectResult, InitiateMultipartUploadResponse, ListBucketResult,
95 ListMultipartUploadsResult, ObjectIdentifier, Part,
96};
97#[allow(unused_imports)]
98use crate::utils::{PutStreamResponse, error_from_response_data};
99
100use http::HeaderMap;
101use http::header::HeaderName;
102#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
103use sysinfo::{MemoryRefreshKind, System};
104
105fn validate_success_xml_response(
106 response_data: ResponseData,
107 expected_root: &str,
108) -> Result<ResponseData, S3Error> {
109 if !(200..300).contains(&response_data.status_code()) {
110 return Ok(response_data);
111 }
112
113 let body = response_data.as_slice();
114 let body_text = std::str::from_utf8(body)?;
115 let mut reader = quick_xml::Reader::from_str(body_text);
116 reader.config_mut().check_end_names = true;
117 reader.config_mut().check_comments = true;
118 let mut depth = 0usize;
119 let mut root = None;
120 let mut valid = true;
121 let mut declaration_seen = false;
122 loop {
123 use quick_xml::events::Event;
124 match reader.read_event() {
125 Ok(Event::Start(element)) => {
126 for attr in element.attributes() {
127 match attr {
128 Ok(attr)
129 if attr
130 .decoded_and_normalized_value(
131 quick_xml::XmlVersion::Implicit1_0,
132 reader.decoder(),
133 )
134 .is_ok() => {}
135 _ => {
136 valid = false;
137 break;
138 }
139 }
140 }
141 if depth == 0 {
142 if root.is_some() {
143 valid = false;
144 break;
145 }
146 root =
147 Some(String::from_utf8_lossy(element.local_name().as_ref()).into_owned());
148 }
149 depth += 1;
150 }
151 Ok(Event::Empty(element)) => {
152 for attr in element.attributes() {
153 match attr {
154 Ok(attr)
155 if attr
156 .decoded_and_normalized_value(
157 quick_xml::XmlVersion::Implicit1_0,
158 reader.decoder(),
159 )
160 .is_ok() => {}
161 _ => {
162 valid = false;
163 break;
164 }
165 }
166 }
167 if depth == 0 {
168 if root.is_some() {
169 valid = false;
170 break;
171 }
172 root =
173 Some(String::from_utf8_lossy(element.local_name().as_ref()).into_owned());
174 }
175 }
176 Ok(Event::End(_)) => {
177 if depth == 0 {
178 valid = false;
179 break;
180 }
181 depth -= 1;
182 }
183 Ok(Event::Text(text)) => {
184 if text.xml11_content().is_err()
187 || (depth == 0 && !text.as_ref().iter().all(u8::is_ascii_whitespace))
188 {
189 valid = false;
190 break;
191 }
192 }
193 Ok(Event::GeneralRef(reference)) => {
194 let name = reference.as_ref();
195 let predefined = matches!(name, b"amp" | b"lt" | b"gt" | b"apos" | b"quot");
196 if depth == 0
197 || (!predefined && reference.resolve_char_ref().ok().flatten().is_none())
198 {
199 valid = false;
200 break;
201 }
202 }
203 Ok(Event::Eof) => break,
204 Ok(Event::DocType(_)) => {
205 valid = false;
206 break;
207 }
208 Ok(Event::Decl(declaration)) if root.is_none() && !declaration_seen => {
209 declaration_seen = true;
210 if !matches!(declaration.version().as_deref(), Ok(b"1.0") | Ok(b"1.1")) {
211 valid = false;
212 break;
213 }
214 }
215 Ok(Event::Decl(_)) => {
216 valid = false;
217 break;
218 }
219 Ok(Event::Comment(_) | Event::PI(_)) => {}
220 Ok(Event::CData(_)) if depth > 0 => {}
221 Ok(_) => {
222 valid = false;
223 break;
224 }
225 Err(_) => {
226 valid = false;
227 break;
228 }
229 }
230 }
231
232 if !valid || depth != 0 || root.is_none() {
233 return Err(S3Error::SerdeXml(quick_xml::de::DeError::Custom(
234 "invalid or incomplete XML response".to_owned(),
235 )));
236 }
237 match root.as_deref() {
238 Some("Error") => Err(error_from_response_data(response_data)?),
239 Some(root) if root == expected_root => Ok(response_data),
240 _ => Err(S3Error::HttpFailWithBody(
241 response_data.status_code(),
242 String::from_utf8_lossy(body).into_owned(),
243 )),
244 }
245}
246
247fn response_error_from_data(response_data: ResponseData) -> S3Error {
248 match error_from_response_data(response_data) {
249 Ok(error) | Err(error) => error,
250 }
251}
252
253#[cfg(all(
254 not(feature = "sync"),
255 any(feature = "with-tokio", feature = "with-async-std")
256))]
257#[derive(Default)]
258struct MultipartHeaderPlan {
259 initiate: HeaderMap,
260 part: HeaderMap,
261 complete: HeaderMap,
262 abort: HeaderMap,
263}
264
265#[cfg(all(
266 not(feature = "sync"),
267 any(feature = "with-tokio", feature = "with-async-std")
268))]
269impl MultipartHeaderPlan {
270 fn from_headers(headers: Option<&HeaderMap>) -> Result<Self, S3Error> {
271 let mut plan = Self::default();
272 let Some(headers) = headers else {
273 return Ok(plan);
274 };
275
276 for (name, value) in headers {
277 let name_text = name.as_str();
278 if name_text == "content-md5"
279 || name_text == "content-length"
280 || name_text == "transfer-encoding"
281 || name_text == "x-amz-content-sha256"
282 || name_text.starts_with("x-amz-checksum-")
283 || name_text.starts_with("x-amz-sdk-checksum-")
284 {
285 return Err(S3Error::UnsupportedMultipartHeader(name.clone()));
286 }
287
288 if name_text == "content-type" {
291 continue;
292 }
293
294 if matches!(
295 name_text,
296 "x-amz-expected-bucket-owner" | "x-amz-request-payer"
297 ) {
298 plan.initiate.insert(name.clone(), value.clone());
299 plan.part.insert(name.clone(), value.clone());
300 plan.complete.insert(name.clone(), value.clone());
301 plan.abort.insert(name.clone(), value.clone());
302 } else if matches!(
303 name_text,
304 "x-amz-server-side-encryption-customer-algorithm"
305 | "x-amz-server-side-encryption-customer-key"
306 | "x-amz-server-side-encryption-customer-key-md5"
307 ) {
308 plan.initiate.insert(name.clone(), value.clone());
309 plan.part.insert(name.clone(), value.clone());
310 plan.complete.insert(name.clone(), value.clone());
311 } else if matches!(name_text, "if-match" | "if-none-match") {
312 plan.complete.insert(name.clone(), value.clone());
313 } else {
314 plan.initiate.insert(name.clone(), value.clone());
317 }
318 }
319
320 Ok(plan)
321 }
322}
323
324#[cfg(feature = "sync")]
325fn part_from_response(response_data: ResponseData, part_number: u32) -> Result<Part, S3Error> {
326 if !(200..300).contains(&response_data.status_code()) {
327 return Err(response_error_from_data(response_data));
328 }
329 let etag = response_data.as_str()?.to_owned();
330 Ok(Part { etag, part_number })
331}
332
333pub const CHUNK_SIZE: usize = 8_388_608; const DEFAULT_REQUEST_TIMEOUT: Option<Duration> = Some(Duration::from_secs(60));
336
337#[derive(Debug, PartialEq, Eq)]
338pub struct Tag {
339 key: String,
340 value: String,
341}
342
343impl Tag {
344 pub fn key(&self) -> String {
345 self.key.to_owned()
346 }
347
348 pub fn value(&self) -> String {
349 self.value.to_owned()
350 }
351}
352
353#[derive(Clone, Debug)]
368pub struct Bucket {
369 pub name: String,
370 pub region: Region,
371 credentials: Arc<RwLock<Credentials>>,
372 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
373 credentials_refresh_gate: Arc<AsyncMutex<()>>,
374 pub extra_headers: HeaderMap,
375 pub extra_query: Query,
376 pub request_timeout: Option<Duration>,
377 path_style: bool,
378 listobjects_v2: bool,
379 #[cfg(feature = "with-tokio")]
380 http_client: reqwest::Client,
381 #[cfg(feature = "with-tokio")]
382 client_options: crate::request::tokio_backend::ClientOptions,
383}
384
385impl Bucket {
386 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
387 async fn credentials_refresh_with<F>(&self, refresh: F) -> Result<(), S3Error>
388 where
389 F: FnOnce(Credentials) -> Result<Credentials, crate::creds::error::CredentialsError>
390 + Send
391 + 'static,
392 {
393 let expired = {
394 let credentials = self.credentials.read().await;
395 credentials
396 .expiration
397 .is_some_and(|expiration| *expiration <= time::OffsetDateTime::now_utc())
398 };
399 if !expired {
400 return Ok(());
401 }
402
403 let _refresh_guard = self.credentials_refresh_gate.lock().await;
404 let credentials = { self.credentials.read().await.clone() };
405 if !credentials
406 .expiration
407 .is_some_and(|expiration| *expiration <= time::OffsetDateTime::now_utc())
408 {
409 return Ok(());
410 }
411
412 #[cfg(feature = "with-tokio")]
413 let refreshed = tokio::task::spawn_blocking(move || refresh(credentials))
414 .await
415 .map_err(|_| S3Error::Io(std::io::Error::other("credential refresh task failed")))??;
416
417 #[cfg(all(not(feature = "with-tokio"), feature = "with-async-std"))]
418 let refreshed = async_std::task::spawn_blocking(move || refresh(credentials)).await?;
419
420 *self.credentials.write().await = refreshed;
421 Ok(())
422 }
423
424 #[maybe_async::async_impl]
425 pub async fn credentials_refresh(&self) -> Result<(), S3Error> {
427 self.credentials_refresh_with(|mut credentials| {
428 credentials.refresh()?;
429 Ok(credentials)
430 })
431 .await
432 }
433
434 #[maybe_async::sync_impl]
435 pub fn credentials_refresh(&self) -> Result<(), S3Error> {
437 match self.credentials.write() {
438 Ok(mut credentials) => Ok(credentials.refresh()?),
439 Err(_) => Err(S3Error::CredentialsWriteLock),
440 }
441 }
442
443 #[cfg(feature = "with-tokio")]
444 pub fn http_client(&self) -> reqwest::Client {
445 self.http_client.clone()
446 }
447}
448
449fn validate_expiry(expiry_secs: u32) -> Result<(), S3Error> {
450 if 604800 < expiry_secs {
451 return Err(S3Error::MaxExpiry(expiry_secs));
452 }
453 Ok(())
454}
455
456#[cfg_attr(all(feature = "with-tokio", feature = "blocking"), block_on("tokio"))]
457#[cfg_attr(
458 all(feature = "with-async-std", feature = "blocking"),
459 block_on("async-std")
460)]
461impl Bucket {
462 #[maybe_async::maybe_async]
493 pub async fn presign_get<S: AsRef<str>>(
494 &self,
495 path: S,
496 expiry_secs: u32,
497 custom_queries: Option<HashMap<String, String>>,
498 ) -> Result<String, S3Error> {
499 validate_expiry(expiry_secs)?;
500 let request = RequestImpl::new(
501 self,
502 path.as_ref(),
503 Command::PresignGet {
504 expiry_secs,
505 custom_queries,
506 },
507 )
508 .await?;
509 request.presigned().await
510 }
511
512 #[maybe_async::maybe_async]
542 #[allow(clippy::needless_lifetimes)]
543 pub async fn presign_post<'a>(
544 &self,
545 post_policy: PostPolicy<'a>,
546 ) -> Result<PresignedPost, S3Error> {
547 post_policy.sign(Box::new(self.clone())).await
548 }
549
550 #[maybe_async::maybe_async]
581 pub async fn presign_put<S: AsRef<str>>(
582 &self,
583 path: S,
584 expiry_secs: u32,
585 custom_headers: Option<HeaderMap>,
586 custom_queries: Option<HashMap<String, String>>,
587 ) -> Result<String, S3Error> {
588 validate_expiry(expiry_secs)?;
589 let request = RequestImpl::new(
590 self,
591 path.as_ref(),
592 Command::PresignPut {
593 expiry_secs,
594 custom_headers,
595 custom_queries,
596 },
597 )
598 .await?;
599 request.presigned().await
600 }
601
602 #[maybe_async::maybe_async]
626 pub async fn presign_delete<S: AsRef<str>>(
627 &self,
628 path: S,
629 expiry_secs: u32,
630 ) -> Result<String, S3Error> {
631 validate_expiry(expiry_secs)?;
632 let request =
633 RequestImpl::new(self, path.as_ref(), Command::PresignDelete { expiry_secs }).await?;
634 request.presigned().await
635 }
636
637 #[maybe_async::maybe_async]
671 pub async fn create(
672 name: &str,
673 region: Region,
674 credentials: Credentials,
675 config: BucketConfiguration,
676 ) -> Result<CreateBucketResponse, S3Error> {
677 let mut config = config;
678
679 let skip_constraint = std::env::var("RUST_S3_SKIP_LOCATION_CONSTRAINT")
683 .unwrap_or_default()
684 .to_lowercase();
685
686 if skip_constraint != "true" && skip_constraint != "1" {
687 config.set_region(region.clone());
688 }
689
690 let command = Command::CreateBucket { config };
691 let bucket = Bucket::new(name, region, credentials)?;
692 let request = RequestImpl::new(&bucket, "", command).await?;
693 let response_data = request.response_data(false).await?;
694 let response_text = response_data.as_str()?;
695 Ok(CreateBucketResponse {
696 bucket,
697 response_text: response_text.to_string(),
698 response_code: response_data.status_code(),
699 })
700 }
701
702 #[maybe_async::maybe_async]
737 pub async fn list_buckets(
738 region: Region,
739 credentials: Credentials,
740 ) -> Result<crate::bucket_ops::ListBucketsResponse, S3Error> {
741 let dummy_bucket = Bucket::new("", region, credentials)?.with_path_style();
742 dummy_bucket._list_buckets().await
743 }
744
745 #[maybe_async::maybe_async]
748 async fn _list_buckets(&self) -> Result<crate::bucket_ops::ListBucketsResponse, S3Error> {
749 let request = RequestImpl::new(self, "", Command::ListBuckets).await?;
750 let response = request.response_data(false).await?;
751
752 Ok(quick_xml::de::from_str::<
753 crate::bucket_ops::ListBucketsResponse,
754 >(response.as_str()?)?)
755 }
756
757 #[maybe_async::maybe_async]
790 pub async fn exists(&self) -> Result<bool, S3Error> {
791 let mut dummy_bucket = self.clone();
792 dummy_bucket.name = "".into();
793
794 let response = dummy_bucket._list_buckets().await?;
795
796 Ok(response
797 .bucket_names()
798 .collect::<std::collections::HashSet<String>>()
799 .contains(&self.name))
800 }
801
802 #[maybe_async::maybe_async]
836 pub async fn create_with_path_style(
837 name: &str,
838 region: Region,
839 credentials: Credentials,
840 config: BucketConfiguration,
841 ) -> Result<CreateBucketResponse, S3Error> {
842 let mut config = config;
843
844 let skip_constraint = std::env::var("RUST_S3_SKIP_LOCATION_CONSTRAINT")
848 .unwrap_or_default()
849 .to_lowercase();
850
851 if skip_constraint != "true" && skip_constraint != "1" {
852 config.set_region(region.clone());
853 }
854
855 let command = Command::CreateBucket { config };
856 let bucket = Bucket::new(name, region, credentials)?.with_path_style();
857 let request = RequestImpl::new(&bucket, "", command).await?;
858 let response_data = request.response_data(false).await?;
859 let response_text = response_data.to_string()?;
860
861 Ok(CreateBucketResponse {
862 bucket,
863 response_text,
864 response_code: response_data.status_code(),
865 })
866 }
867
868 #[maybe_async::maybe_async]
900 pub async fn delete(&self) -> Result<u16, S3Error> {
901 let command = Command::DeleteBucket;
902 let request = RequestImpl::new(self, "", command).await?;
903 let response_data = request.response_data(false).await?;
904 Ok(response_data.status_code())
905 }
906
907 pub fn new(
922 name: &str,
923 region: Region,
924 credentials: Credentials,
925 ) -> Result<Box<Bucket>, S3Error> {
926 #[cfg(feature = "with-tokio")]
927 let options = ClientOptions::default();
928
929 Ok(Box::new(Bucket {
930 name: name.into(),
931 region,
932 credentials: Arc::new(RwLock::new(credentials)),
933 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
934 credentials_refresh_gate: Arc::new(AsyncMutex::new(())),
935 extra_headers: HeaderMap::new(),
936 extra_query: HashMap::new(),
937 request_timeout: DEFAULT_REQUEST_TIMEOUT,
938 path_style: false,
939 listobjects_v2: true,
940 #[cfg(feature = "with-tokio")]
941 http_client: client(&options)?,
942 #[cfg(feature = "with-tokio")]
943 client_options: options,
944 }))
945 }
946
947 pub fn new_public(name: &str, region: Region) -> Result<Bucket, S3Error> {
959 #[cfg(feature = "with-tokio")]
960 let options = ClientOptions::default();
961
962 Ok(Bucket {
963 name: name.into(),
964 region,
965 credentials: Arc::new(RwLock::new(Credentials::anonymous()?)),
966 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
967 credentials_refresh_gate: Arc::new(AsyncMutex::new(())),
968 extra_headers: HeaderMap::new(),
969 extra_query: HashMap::new(),
970 request_timeout: DEFAULT_REQUEST_TIMEOUT,
971 path_style: false,
972 listobjects_v2: true,
973 #[cfg(feature = "with-tokio")]
974 http_client: client(&options)?,
975 #[cfg(feature = "with-tokio")]
976 client_options: options,
977 })
978 }
979
980 pub fn with_path_style(&self) -> Box<Bucket> {
981 Box::new(Bucket {
982 name: self.name.clone(),
983 region: self.region.clone(),
984 credentials: self.credentials.clone(),
985 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
986 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
987 extra_headers: self.extra_headers.clone(),
988 extra_query: self.extra_query.clone(),
989 request_timeout: self.request_timeout,
990 path_style: true,
991 listobjects_v2: self.listobjects_v2,
992 #[cfg(feature = "with-tokio")]
993 http_client: self.http_client(),
994 #[cfg(feature = "with-tokio")]
995 client_options: self.client_options.clone(),
996 })
997 }
998
999 pub fn with_extra_headers(&self, extra_headers: HeaderMap) -> Result<Bucket, S3Error> {
1000 Ok(Bucket {
1001 name: self.name.clone(),
1002 region: self.region.clone(),
1003 credentials: self.credentials.clone(),
1004 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1005 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1006 extra_headers,
1007 extra_query: self.extra_query.clone(),
1008 request_timeout: self.request_timeout,
1009 path_style: self.path_style,
1010 listobjects_v2: self.listobjects_v2,
1011 #[cfg(feature = "with-tokio")]
1012 http_client: self.http_client(),
1013 #[cfg(feature = "with-tokio")]
1014 client_options: self.client_options.clone(),
1015 })
1016 }
1017
1018 pub fn with_extra_query(
1019 &self,
1020 extra_query: HashMap<String, String>,
1021 ) -> Result<Bucket, S3Error> {
1022 Ok(Bucket {
1023 name: self.name.clone(),
1024 region: self.region.clone(),
1025 credentials: self.credentials.clone(),
1026 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1027 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1028 extra_headers: self.extra_headers.clone(),
1029 extra_query,
1030 request_timeout: self.request_timeout,
1031 path_style: self.path_style,
1032 listobjects_v2: self.listobjects_v2,
1033 #[cfg(feature = "with-tokio")]
1034 http_client: self.http_client(),
1035 #[cfg(feature = "with-tokio")]
1036 client_options: self.client_options.clone(),
1037 })
1038 }
1039
1040 #[cfg(not(feature = "with-tokio"))]
1041 pub fn with_request_timeout(&self, request_timeout: Duration) -> Result<Box<Bucket>, S3Error> {
1042 Ok(Box::new(Bucket {
1043 name: self.name.clone(),
1044 region: self.region.clone(),
1045 credentials: self.credentials.clone(),
1046 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1047 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1048 extra_headers: self.extra_headers.clone(),
1049 extra_query: self.extra_query.clone(),
1050 request_timeout: Some(request_timeout),
1051 path_style: self.path_style,
1052 listobjects_v2: self.listobjects_v2,
1053 }))
1054 }
1055
1056 #[cfg(feature = "with-tokio")]
1057 pub fn with_request_timeout(&self, request_timeout: Duration) -> Result<Box<Bucket>, S3Error> {
1058 Ok(Box::new(Bucket {
1059 name: self.name.clone(),
1060 region: self.region.clone(),
1061 credentials: self.credentials.clone(),
1062 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1063 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1064 extra_headers: self.extra_headers.clone(),
1065 extra_query: self.extra_query.clone(),
1066 request_timeout: Some(request_timeout),
1067 path_style: self.path_style,
1068 listobjects_v2: self.listobjects_v2,
1069 #[cfg(feature = "with-tokio")]
1070 http_client: self.http_client(),
1071 #[cfg(feature = "with-tokio")]
1072 client_options: self.client_options.clone(),
1073 }))
1074 }
1075
1076 pub fn with_listobjects_v1(&self) -> Bucket {
1077 Bucket {
1078 name: self.name.clone(),
1079 region: self.region.clone(),
1080 credentials: self.credentials.clone(),
1081 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1082 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1083 extra_headers: self.extra_headers.clone(),
1084 extra_query: self.extra_query.clone(),
1085 request_timeout: self.request_timeout,
1086 path_style: self.path_style,
1087 listobjects_v2: false,
1088 #[cfg(feature = "with-tokio")]
1089 http_client: self.http_client(),
1090 #[cfg(feature = "with-tokio")]
1091 client_options: self.client_options.clone(),
1092 }
1093 }
1094
1095 #[cfg(any(
1128 feature = "tokio-native-tls",
1129 feature = "tokio-rustls-tls",
1130 feature = "tokio-rustls-tls-ring"
1131 ))]
1132 pub fn set_dangerous_config(
1133 &self,
1134 accept_invalid_certs: bool,
1135 accept_invalid_hostnames: bool,
1136 ) -> Result<Bucket, S3Error> {
1137 let mut options = self.client_options.clone();
1138 options.accept_invalid_certs = accept_invalid_certs;
1139 options.accept_invalid_hostnames = accept_invalid_hostnames;
1140
1141 Ok(Bucket {
1142 name: self.name.clone(),
1143 region: self.region.clone(),
1144 credentials: self.credentials.clone(),
1145 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1146 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1147 extra_headers: self.extra_headers.clone(),
1148 extra_query: self.extra_query.clone(),
1149 request_timeout: self.request_timeout,
1150 path_style: self.path_style,
1151 listobjects_v2: self.listobjects_v2,
1152 http_client: client(&options)?,
1153 client_options: options,
1154 })
1155 }
1156
1157 #[deprecated(
1159 since = "0.37.3",
1160 note = "use `set_dangerous_config`; this misspelled method remains for compatibility"
1161 )]
1162 #[cfg(any(
1163 feature = "tokio-native-tls",
1164 feature = "tokio-rustls-tls",
1165 feature = "tokio-rustls-tls-ring"
1166 ))]
1167 pub fn set_dangereous_config(
1168 &self,
1169 accept_invalid_certs: bool,
1170 accept_invalid_hostnames: bool,
1171 ) -> Result<Bucket, S3Error> {
1172 self.set_dangerous_config(accept_invalid_certs, accept_invalid_hostnames)
1173 }
1174
1175 #[cfg(feature = "with-tokio")]
1176 pub fn set_proxy(&self, proxy: reqwest::Proxy) -> Result<Bucket, S3Error> {
1177 let mut options = self.client_options.clone();
1178 options.proxy = Some(proxy);
1179
1180 Ok(Bucket {
1181 name: self.name.clone(),
1182 region: self.region.clone(),
1183 credentials: self.credentials.clone(),
1184 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1185 credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1186 extra_headers: self.extra_headers.clone(),
1187 extra_query: self.extra_query.clone(),
1188 request_timeout: self.request_timeout,
1189 path_style: self.path_style,
1190 listobjects_v2: self.listobjects_v2,
1191 http_client: client(&options)?,
1192 client_options: options,
1193 })
1194 }
1195
1196 #[maybe_async::maybe_async]
1227 pub async fn copy_object_internal<F: AsRef<str>, T: AsRef<str>>(
1228 &self,
1229 from: F,
1230 to: T,
1231 ) -> Result<u16, S3Error> {
1232 let fq_from = {
1233 let from = from.as_ref();
1234 let from = from.strip_prefix('/').unwrap_or(from);
1235 format!("{bucket}/{path}", bucket = self.name(), path = from)
1236 };
1237 self.copy_object(fq_from, to).await
1238 }
1239
1240 #[maybe_async::maybe_async]
1241 async fn copy_object<F: AsRef<str>, T: AsRef<str>>(
1242 &self,
1243 from: F,
1244 to: T,
1245 ) -> Result<u16, S3Error> {
1246 let command = Command::CopyObject {
1247 from: from.as_ref(),
1248 };
1249 let request = RequestImpl::new(self, to.as_ref(), command).await?;
1250 let response_data = request.response_data(false).await?;
1251 let response_data = validate_success_xml_response(response_data, "CopyObjectResult")?;
1252 Ok(response_data.status_code())
1253 }
1254
1255 #[maybe_async::maybe_async]
1288 pub async fn get_object<S: AsRef<str>>(&self, path: S) -> Result<ResponseData, S3Error> {
1289 let command = Command::GetObject;
1290 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1291 request.response_data(false).await
1292 }
1293
1294 #[maybe_async::maybe_async]
1295 pub async fn get_object_attributes<S: AsRef<str>>(
1296 &self,
1297 path: S,
1298 expected_bucket_owner: &str,
1299 version_id: Option<String>,
1300 ) -> Result<GetObjectAttributesOutput, S3Error> {
1301 let command = Command::GetObjectAttributes {
1302 expected_bucket_owner: expected_bucket_owner.to_string(),
1303 version_id,
1304 };
1305 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1306
1307 let response = request.response_data(false).await?;
1308
1309 Ok(quick_xml::de::from_str::<GetObjectAttributesOutput>(
1310 response.as_str()?,
1311 )?)
1312 }
1313
1314 #[maybe_async::maybe_async]
1358 pub async fn object_exists<S: AsRef<str>>(&self, path: S) -> Result<bool, S3Error> {
1359 let command = Command::HeadObject;
1360 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1361 let status_code = request.response_status().await?;
1362 Ok(status_code != 404)
1363 }
1364
1365 #[maybe_async::maybe_async]
1366 pub async fn put_bucket_cors(
1367 &self,
1368 expected_bucket_owner: &str,
1369 cors_config: &CorsConfiguration,
1370 ) -> Result<ResponseData, S3Error> {
1371 let command = Command::PutBucketCors {
1372 expected_bucket_owner: expected_bucket_owner.to_string(),
1373 configuration: cors_config.clone(),
1374 };
1375 let request = RequestImpl::new(self, "", command).await?;
1376 request.response_data(false).await
1377 }
1378
1379 #[maybe_async::maybe_async]
1380 pub async fn get_bucket_cors(
1381 &self,
1382 expected_bucket_owner: &str,
1383 ) -> Result<CorsConfiguration, S3Error> {
1384 let command = Command::GetBucketCors {
1385 expected_bucket_owner: expected_bucket_owner.to_string(),
1386 };
1387 let request = RequestImpl::new(self, "", command).await?;
1388 let response = request.response_data(false).await?;
1389 Ok(quick_xml::de::from_str::<CorsConfiguration>(
1390 response.as_str()?,
1391 )?)
1392 }
1393
1394 #[maybe_async::maybe_async]
1395 pub async fn delete_bucket_cors(
1396 &self,
1397 expected_bucket_owner: &str,
1398 ) -> Result<ResponseData, S3Error> {
1399 let command = Command::DeleteBucketCors {
1400 expected_bucket_owner: expected_bucket_owner.to_string(),
1401 };
1402 let request = RequestImpl::new(self, "", command).await?;
1403 request.response_data(false).await
1404 }
1405
1406 #[maybe_async::maybe_async]
1407 pub async fn get_bucket_lifecycle(&self) -> Result<BucketLifecycleConfiguration, S3Error> {
1408 let request = RequestImpl::new(self, "", Command::GetBucketLifecycle).await?;
1409 let response = request.response_data(false).await?;
1410 Ok(quick_xml::de::from_str::<BucketLifecycleConfiguration>(
1411 response.as_str()?,
1412 )?)
1413 }
1414
1415 #[maybe_async::maybe_async]
1416 pub async fn put_bucket_lifecycle(
1417 &self,
1418 lifecycle_config: BucketLifecycleConfiguration,
1419 ) -> Result<ResponseData, S3Error> {
1420 let command = Command::PutBucketLifecycle {
1421 configuration: lifecycle_config,
1422 };
1423 let request = RequestImpl::new(self, "", command).await?;
1424 request.response_data(false).await
1425 }
1426
1427 #[maybe_async::maybe_async]
1428 pub async fn delete_bucket_lifecycle(&self) -> Result<ResponseData, S3Error> {
1429 let request = RequestImpl::new(self, "", Command::DeleteBucketLifecycle).await?;
1430 request.response_data(false).await
1431 }
1432
1433 #[maybe_async::maybe_async]
1468 pub async fn get_bucket_policy(&self) -> Result<ResponseData, S3Error> {
1469 let request = RequestImpl::new(self, "", Command::GetBucketPolicy).await?;
1470 request.response_data(false).await
1471 }
1472
1473 #[maybe_async::maybe_async]
1510 pub async fn put_bucket_policy(&self, policy: &str) -> Result<ResponseData, S3Error> {
1511 let command = Command::PutBucketPolicy {
1512 policy: policy.to_string(),
1513 };
1514 let request = RequestImpl::new(self, "", command).await?;
1515 request.response_data(false).await
1516 }
1517
1518 #[maybe_async::maybe_async]
1553 pub async fn delete_bucket_policy(&self) -> Result<ResponseData, S3Error> {
1554 let request = RequestImpl::new(self, "", Command::DeleteBucketPolicy).await?;
1555 request.response_data(false).await
1556 }
1557
1558 #[maybe_async::maybe_async]
1591 pub async fn get_object_torrent<S: AsRef<str>>(
1592 &self,
1593 path: S,
1594 ) -> Result<ResponseData, S3Error> {
1595 let command = Command::GetObjectTorrent;
1596 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1597 request.response_data(false).await
1598 }
1599
1600 #[maybe_async::maybe_async]
1634 pub async fn get_object_range<S: AsRef<str>>(
1635 &self,
1636 path: S,
1637 start: u64,
1638 end: Option<u64>,
1639 ) -> Result<ResponseData, S3Error> {
1640 if let Some(end) = end {
1641 assert!(start <= end);
1642 }
1643
1644 let command = Command::GetObjectRange { start, end };
1645 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1646 request.response_data(false).await
1647 }
1648
1649 #[maybe_async::async_impl]
1690 pub async fn get_object_range_to_writer<T, S>(
1691 &self,
1692 path: S,
1693 start: u64,
1694 end: Option<u64>,
1695 writer: &mut T,
1696 ) -> Result<u16, S3Error>
1697 where
1698 T: AsyncWrite + Send + Unpin + ?Sized,
1699 S: AsRef<str>,
1700 {
1701 if let Some(end) = end {
1702 assert!(start <= end);
1703 }
1704
1705 let command = Command::GetObjectRange { start, end };
1706 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1707 request.response_data_to_writer(writer).await
1708 }
1709
1710 #[maybe_async::sync_impl]
1711 pub fn get_object_range_to_writer<T: std::io::Write + Send + ?Sized, S: AsRef<str>>(
1712 &self,
1713 path: S,
1714 start: u64,
1715 end: Option<u64>,
1716 writer: &mut T,
1717 ) -> Result<u16, S3Error> {
1718 if let Some(end) = end {
1719 assert!(start <= end);
1720 }
1721
1722 let command = Command::GetObjectRange { start, end };
1723 let request = RequestImpl::new(self, path.as_ref(), command)?;
1724 request.response_data_to_writer(writer)
1725 }
1726
1727 #[maybe_async::async_impl]
1765 pub async fn get_object_to_writer<T: AsyncWrite + Send + Unpin + ?Sized, S: AsRef<str>>(
1766 &self,
1767 path: S,
1768 writer: &mut T,
1769 ) -> Result<u16, S3Error> {
1770 let command = Command::GetObject;
1771 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1772 request.response_data_to_writer(writer).await
1773 }
1774
1775 #[maybe_async::sync_impl]
1776 pub fn get_object_to_writer<T: std::io::Write + Send + ?Sized, S: AsRef<str>>(
1777 &self,
1778 path: S,
1779 writer: &mut T,
1780 ) -> Result<u16, S3Error> {
1781 let command = Command::GetObject;
1782 let request = RequestImpl::new(self, path.as_ref(), command)?;
1783 request.response_data_to_writer(writer)
1784 }
1785
1786 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1828 pub async fn get_object_stream<S: AsRef<str>>(
1829 &self,
1830 path: S,
1831 ) -> Result<ResponseDataStream, S3Error> {
1832 let command = Command::GetObject;
1833 let request = RequestImpl::new(self, path.as_ref(), command).await?;
1834 request.response_data_to_stream().await
1835 }
1836
1837 #[maybe_async::async_impl]
1884 pub async fn put_object_stream<R: AsyncRead + Unpin + ?Sized>(
1885 &self,
1886 reader: &mut R,
1887 s3_path: impl AsRef<str>,
1888 ) -> Result<PutStreamResponse, S3Error> {
1889 self._put_object_stream_with_content_type(
1890 reader,
1891 s3_path.as_ref(),
1892 "application/octet-stream",
1893 )
1894 .await
1895 }
1896
1897 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1931 pub fn put_object_stream_builder<S: AsRef<str>>(
1932 &self,
1933 path: S,
1934 ) -> crate::put_object_request::PutObjectStreamRequest<'_> {
1935 crate::put_object_request::PutObjectStreamRequest::new(self, path)
1936 }
1937
1938 #[maybe_async::sync_impl]
1939 pub fn put_object_stream<R: Read>(
1940 &self,
1941 reader: &mut R,
1942 s3_path: impl AsRef<str>,
1943 ) -> Result<u16, S3Error> {
1944 self._put_object_stream_with_content_type(
1945 reader,
1946 s3_path.as_ref(),
1947 "application/octet-stream",
1948 )
1949 }
1950
1951 #[maybe_async::async_impl]
2008 pub async fn put_object_stream_with_content_type<R: AsyncRead + Unpin>(
2009 &self,
2010 reader: &mut R,
2011 s3_path: impl AsRef<str>,
2012 content_type: impl AsRef<str>,
2013 ) -> Result<PutStreamResponse, S3Error> {
2014 self._put_object_stream_with_content_type(reader, s3_path.as_ref(), content_type.as_ref())
2015 .await
2016 }
2017
2018 #[maybe_async::sync_impl]
2019 pub fn put_object_stream_with_content_type<R: Read>(
2020 &self,
2021 reader: &mut R,
2022 s3_path: impl AsRef<str>,
2023 content_type: impl AsRef<str>,
2024 ) -> Result<u16, S3Error> {
2025 self._put_object_stream_with_content_type(reader, s3_path.as_ref(), content_type.as_ref())
2026 }
2027
2028 #[maybe_async::maybe_async]
2029 async fn make_multipart_request(
2030 &self,
2031 path: &str,
2032 chunk: Vec<u8>,
2033 part_number: u32,
2034 upload_id: &str,
2035 content_type: &str,
2036 custom_headers: &HeaderMap,
2037 ) -> Result<ResponseData, S3Error> {
2038 let command = Command::PutObject {
2039 content: &chunk,
2040 multipart: Some(Multipart::new(part_number, upload_id)), custom_headers: Some(custom_headers.clone()),
2042 content_type,
2043 };
2044 let request = RequestImpl::new(self, path, command).await?;
2045 request.response_data(true).await
2046 }
2047
2048 #[maybe_async::maybe_async]
2049 async fn abort_upload_after_failure<T>(
2050 &self,
2051 path: &str,
2052 upload_id: &str,
2053 abort_headers: &HeaderMap,
2054 error: S3Error,
2055 ) -> Result<T, S3Error> {
2056 let bucket = self.with_overlaid_extra_headers(abort_headers);
2059 let _ = bucket.abort_upload(path, upload_id).await;
2060 Err(error)
2061 }
2062
2063 fn with_overlaid_extra_headers(&self, overlay: &HeaderMap) -> Bucket {
2064 let mut bucket = self.clone();
2065 bucket.extra_headers.extend(overlay.clone());
2066 bucket
2067 }
2068
2069 #[maybe_async::async_impl]
2070 async fn _put_object_stream_with_content_type<R: AsyncRead + Unpin + ?Sized>(
2071 &self,
2072 reader: &mut R,
2073 s3_path: &str,
2074 content_type: &str,
2075 ) -> Result<PutStreamResponse, S3Error> {
2076 self._put_object_stream_with_content_type_and_headers(
2077 reader,
2078 s3_path,
2079 content_type,
2080 None,
2081 None,
2082 )
2083 .await
2084 }
2085
2086 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
2090 fn max_concurrent_chunks_for_memory(available_memory: u64) -> usize {
2091 const DEFAULT_CONCURRENT_CHUNKS: usize = 3;
2092 const MAX_CONCURRENT_CHUNKS: usize = 10;
2093
2094 if available_memory == 0 {
2095 return DEFAULT_CONCURRENT_CHUNKS;
2096 }
2097
2098 let memory_per_chunk = CHUNK_SIZE as u64 * 3;
2099 (available_memory / memory_per_chunk).clamp(2, MAX_CONCURRENT_CHUNKS as u64) as usize
2100 }
2101
2102 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
2103 fn calculate_max_concurrent_chunks() -> usize {
2104 let mut system = System::new();
2106 system.refresh_memory_specifics(MemoryRefreshKind::everything());
2107
2108 let available_memory = system.available_memory();
2110
2111 Self::max_concurrent_chunks_for_memory(available_memory)
2112 }
2113
2114 #[maybe_async::async_impl]
2115 pub(crate) async fn _put_object_stream_with_content_type_and_headers<
2116 R: AsyncRead + Unpin + ?Sized,
2117 >(
2118 &self,
2119 reader: &mut R,
2120 s3_path: &str,
2121 content_type: &str,
2122 custom_headers: Option<http::HeaderMap>,
2123 max_concurrent_chunks: Option<std::num::NonZeroUsize>,
2124 ) -> Result<PutStreamResponse, S3Error> {
2125 let first_chunk = crate::utils::read_chunk_async(reader).await?;
2128 if first_chunk.len() < CHUNK_SIZE {
2130 let total_size = first_chunk.len();
2131 let mut builder = self
2133 .put_object_builder(s3_path, first_chunk.as_slice())
2134 .with_content_type(content_type);
2135
2136 if let Some(headers) = custom_headers {
2138 builder = builder.with_headers(headers);
2139 }
2140
2141 let response_data = builder.execute().await?;
2142 if response_data.status_code() >= 300 {
2143 return Err(error_from_response_data(response_data)?);
2144 }
2145 return Ok(PutStreamResponse::new(
2146 response_data.status_code(),
2147 total_size,
2148 ));
2149 }
2150
2151 let header_plan = MultipartHeaderPlan::from_headers(custom_headers.as_ref())?;
2152 let initiate_bucket = self.with_overlaid_extra_headers(&header_plan.initiate);
2153 let msg = initiate_bucket
2154 .initiate_multipart_upload(s3_path, content_type)
2155 .await?;
2156 let path = msg.key;
2157 let upload_id = &msg.upload_id;
2158
2159 let max_concurrent_chunks = max_concurrent_chunks.map_or_else(
2161 Self::calculate_max_concurrent_chunks,
2162 std::num::NonZeroUsize::get,
2163 );
2164
2165 use futures_util::FutureExt;
2167 use futures_util::stream::{FuturesUnordered, StreamExt};
2168
2169 let upload_result: Result<(Vec<(u32, String)>, usize), S3Error> = async {
2172 let mut part_number: u32 = 0;
2173 let mut total_size = 0;
2174 let mut etags = Vec::new();
2175 let mut active_uploads: FuturesUnordered<
2176 futures_util::future::BoxFuture<'_, (u32, Result<ResponseData, S3Error>)>,
2177 > = FuturesUnordered::new();
2178 let mut reading_done = false;
2179
2180 part_number += 1;
2181 total_size += first_chunk.len();
2182 if first_chunk.len() < CHUNK_SIZE {
2183 reading_done = true;
2184 }
2185
2186 let path_clone = path.clone();
2187 let upload_id_clone = upload_id.clone();
2188 let content_type_clone = content_type.to_string();
2189 let part_headers = header_plan.part.clone();
2190 let bucket_clone = self.clone();
2191
2192 active_uploads.push(
2193 async move {
2194 let result = bucket_clone
2195 .make_multipart_request(
2196 &path_clone,
2197 first_chunk,
2198 1,
2199 &upload_id_clone,
2200 &content_type_clone,
2201 &part_headers,
2202 )
2203 .await;
2204 (1, result)
2205 }
2206 .boxed(),
2207 );
2208
2209 while !active_uploads.is_empty() || !reading_done {
2210 while active_uploads.len() < max_concurrent_chunks && !reading_done {
2211 let chunk = crate::utils::read_chunk_async(reader).await?;
2212 let chunk_len = chunk.len();
2213
2214 if chunk_len == 0 {
2215 reading_done = true;
2216 break;
2217 }
2218
2219 total_size += chunk_len;
2220 part_number += 1;
2221
2222 if chunk_len < CHUNK_SIZE {
2223 reading_done = true;
2224 }
2225
2226 let current_part = part_number;
2227 let path_clone = path.clone();
2228 let upload_id_clone = upload_id.clone();
2229 let content_type_clone = content_type.to_string();
2230 let part_headers = header_plan.part.clone();
2231 let bucket_clone = self.clone();
2232
2233 active_uploads.push(
2234 async move {
2235 let result = bucket_clone
2236 .make_multipart_request(
2237 &path_clone,
2238 chunk,
2239 current_part,
2240 &upload_id_clone,
2241 &content_type_clone,
2242 &part_headers,
2243 )
2244 .await;
2245 (current_part, result)
2246 }
2247 .boxed(),
2248 );
2249 }
2250
2251 if let Some((part_num, result)) = active_uploads.next().await {
2252 let response_data = result?;
2253 if !(200..300).contains(&response_data.status_code()) {
2254 return Err(response_error_from_data(response_data));
2255 }
2256
2257 let etag = response_data.as_str()?;
2258 etags.push((part_num, etag.to_string()));
2259 }
2260 }
2261
2262 Ok((etags, total_size))
2263 }
2264 .await;
2265
2266 let (mut etags, total_size) = match upload_result {
2267 Ok(result) => result,
2268 Err(error) => {
2269 return self
2270 .abort_upload_after_failure(&path, upload_id, &header_plan.abort, error)
2271 .await;
2272 }
2273 };
2274
2275 etags.sort_by_key(|k| k.0);
2277 let etags: Vec<String> = etags.into_iter().map(|(_, etag)| etag).collect();
2278
2279 let inner_data = etags
2281 .into_iter()
2282 .enumerate()
2283 .map(|(i, x)| Part {
2284 etag: x,
2285 part_number: i as u32 + 1,
2286 })
2287 .collect::<Vec<Part>>();
2288 let complete_bucket = self.with_overlaid_extra_headers(&header_plan.complete);
2289 let response_data = match complete_bucket
2290 .complete_multipart_upload(&path, &msg.upload_id, inner_data)
2291 .await
2292 {
2293 Ok(response_data) if (200..300).contains(&response_data.status_code()) => response_data,
2294 Ok(response_data) => {
2295 let error = response_error_from_data(response_data);
2296 return self
2297 .abort_upload_after_failure(&path, upload_id, &header_plan.abort, error)
2298 .await;
2299 }
2300 Err(error) => {
2301 return self
2302 .abort_upload_after_failure(&path, upload_id, &header_plan.abort, error)
2303 .await;
2304 }
2305 };
2306
2307 Ok(PutStreamResponse::new(
2308 response_data.status_code(),
2309 total_size,
2310 ))
2311 }
2312
2313 #[maybe_async::sync_impl]
2314 fn _put_object_stream_with_content_type<R: Read + ?Sized>(
2315 &self,
2316 reader: &mut R,
2317 s3_path: &str,
2318 content_type: &str,
2319 ) -> Result<u16, S3Error> {
2320 let msg = self.initiate_multipart_upload(s3_path, content_type)?;
2321 let path = msg.key;
2322 let upload_id = &msg.upload_id;
2323
2324 let mut needs_abort = true;
2325 let upload_result = (|| {
2326 let mut part_number: u32 = 0;
2327 let mut etags = Vec::new();
2328 loop {
2329 let chunk = crate::utils::read_chunk(reader)?;
2330
2331 if chunk.len() < CHUNK_SIZE {
2332 if part_number == 0 {
2333 needs_abort = false;
2336 self.abort_upload(&path, upload_id)?;
2337 return Ok(self.put_object(s3_path, chunk.as_slice())?.status_code());
2338 }
2339
2340 part_number += 1;
2341 let response_data = self.make_multipart_request(
2342 &path,
2343 chunk,
2344 part_number,
2345 upload_id,
2346 content_type,
2347 &HeaderMap::new(),
2348 )?;
2349 let part = part_from_response(response_data, part_number)?;
2350 etags.push(part.etag);
2351 let inner_data = etags
2352 .into_iter()
2353 .enumerate()
2354 .map(|(i, x)| Part {
2355 etag: x,
2356 part_number: i as u32 + 1,
2357 })
2358 .collect::<Vec<Part>>();
2359 let response_data =
2360 self.complete_multipart_upload(&path, upload_id, inner_data)?;
2361 if !(200..300).contains(&response_data.status_code()) {
2362 return Err(response_error_from_data(response_data));
2363 }
2364 return Ok(response_data.status_code());
2365 }
2366
2367 part_number += 1;
2368 let response_data = self.make_multipart_request(
2369 &path,
2370 chunk,
2371 part_number,
2372 upload_id,
2373 content_type,
2374 &HeaderMap::new(),
2375 )?;
2376 let part = part_from_response(response_data, part_number)?;
2377 etags.push(part.etag);
2378 }
2379 })();
2380
2381 match upload_result {
2382 Ok(status_code) => Ok(status_code),
2383 Err(error) if needs_abort => {
2384 self.abort_upload_after_failure(&path, upload_id, &HeaderMap::new(), error)
2385 }
2386 Err(error) => Err(error),
2387 }
2388 }
2389
2390 #[maybe_async::async_impl]
2392 pub async fn initiate_multipart_upload(
2393 &self,
2394 s3_path: &str,
2395 content_type: &str,
2396 ) -> Result<InitiateMultipartUploadResponse, S3Error> {
2397 let command = Command::InitiateMultipartUpload { content_type };
2398 let request = RequestImpl::new(self, s3_path, command).await?;
2399 let response_data = request.response_data(false).await?;
2400 if response_data.status_code() >= 300 {
2401 return Err(error_from_response_data(response_data)?);
2402 }
2403
2404 let msg: InitiateMultipartUploadResponse =
2405 quick_xml::de::from_str(response_data.as_str()?)?;
2406 Ok(msg)
2407 }
2408
2409 #[maybe_async::sync_impl]
2410 pub fn initiate_multipart_upload(
2411 &self,
2412 s3_path: &str,
2413 content_type: &str,
2414 ) -> Result<InitiateMultipartUploadResponse, S3Error> {
2415 let command = Command::InitiateMultipartUpload { content_type };
2416 let request = RequestImpl::new(self, s3_path, command)?;
2417 let response_data = request.response_data(false)?;
2418 if response_data.status_code() >= 300 {
2419 return Err(error_from_response_data(response_data)?);
2420 }
2421
2422 let msg: InitiateMultipartUploadResponse =
2423 quick_xml::de::from_str(response_data.as_str()?)?;
2424 Ok(msg)
2425 }
2426
2427 #[maybe_async::async_impl]
2429 pub async fn put_multipart_stream<R: Read + Unpin>(
2430 &self,
2431 reader: &mut R,
2432 path: &str,
2433 part_number: u32,
2434 upload_id: &str,
2435 content_type: &str,
2436 ) -> Result<Part, S3Error> {
2437 let chunk = crate::utils::read_chunk(reader)?;
2438 self.put_multipart_chunk(chunk, path, part_number, upload_id, content_type)
2439 .await
2440 }
2441
2442 #[maybe_async::sync_impl]
2443 pub async fn put_multipart_stream<R: Read + Unpin>(
2444 &self,
2445 reader: &mut R,
2446 path: &str,
2447 part_number: u32,
2448 upload_id: &str,
2449 content_type: &str,
2450 ) -> Result<Part, S3Error> {
2451 let chunk = crate::utils::read_chunk(reader)?;
2452 self.put_multipart_chunk(&chunk, path, part_number, upload_id, content_type)
2453 }
2454
2455 #[maybe_async::async_impl]
2457 pub async fn put_multipart_chunk(
2458 &self,
2459 chunk: Vec<u8>,
2460 path: &str,
2461 part_number: u32,
2462 upload_id: &str,
2463 content_type: &str,
2464 ) -> Result<Part, S3Error> {
2465 let command = Command::PutObject {
2466 content: &chunk,
2468 multipart: Some(Multipart::new(part_number, upload_id)), custom_headers: None,
2470 content_type,
2471 };
2472 let request = RequestImpl::new(self, path, command).await?;
2473 let response_data = request.response_data(true).await?;
2474 if !(200..300).contains(&response_data.status_code()) {
2475 match self.abort_upload(path, upload_id).await {
2477 Ok(_) => {
2478 return Err(error_from_response_data(response_data)?);
2479 }
2480 Err(error) => {
2481 return Err(error);
2482 }
2483 }
2484 }
2485 let etag = response_data.as_str()?;
2486 Ok(Part {
2487 etag: etag.to_string(),
2488 part_number,
2489 })
2490 }
2491
2492 #[maybe_async::sync_impl]
2493 pub fn put_multipart_chunk(
2494 &self,
2495 chunk: &[u8],
2496 path: &str,
2497 part_number: u32,
2498 upload_id: &str,
2499 content_type: &str,
2500 ) -> Result<Part, S3Error> {
2501 let command = Command::PutObject {
2502 content: chunk,
2504 multipart: Some(Multipart::new(part_number, upload_id)), custom_headers: None,
2506 content_type,
2507 };
2508 let request = RequestImpl::new(self, path, command)?;
2509 let response_data = request.response_data(true)?;
2510 if !(200..300).contains(&response_data.status_code()) {
2511 match self.abort_upload(path, upload_id) {
2513 Ok(_) => {
2514 return Err(error_from_response_data(response_data)?);
2515 }
2516 Err(error) => {
2517 return Err(error);
2518 }
2519 }
2520 }
2521 let etag = response_data.as_str()?;
2522 Ok(Part {
2523 etag: etag.to_string(),
2524 part_number,
2525 })
2526 }
2527
2528 #[maybe_async::async_impl]
2532 pub async fn complete_multipart_upload(
2533 &self,
2534 path: &str,
2535 upload_id: &str,
2536 parts: Vec<Part>,
2537 ) -> Result<ResponseData, S3Error> {
2538 let data = CompleteMultipartUploadData { parts };
2539 let complete = Command::CompleteMultipartUpload { upload_id, data };
2540 let complete_request = RequestImpl::new(self, path, complete).await?;
2541 let response_data = complete_request.response_data(false).await?;
2542 validate_success_xml_response(response_data, "CompleteMultipartUploadResult")
2543 }
2544
2545 #[maybe_async::sync_impl]
2546 pub fn complete_multipart_upload(
2547 &self,
2548 path: &str,
2549 upload_id: &str,
2550 parts: Vec<Part>,
2551 ) -> Result<ResponseData, S3Error> {
2552 let data = CompleteMultipartUploadData { parts };
2553 let complete = Command::CompleteMultipartUpload { upload_id, data };
2554 let complete_request = RequestImpl::new(self, path, complete)?;
2555 let response_data = complete_request.response_data(false)?;
2556 validate_success_xml_response(response_data, "CompleteMultipartUploadResult")
2557 }
2558
2559 #[maybe_async::maybe_async]
2593 pub async fn location(&self) -> Result<(Region, u16), S3Error> {
2594 let request = RequestImpl::new(self, "?location", Command::GetBucketLocation).await?;
2595 let response_data = request.response_data(false).await?;
2596 let region_string = String::from_utf8_lossy(response_data.as_slice());
2597 let region = match quick_xml::de::from_reader(region_string.as_bytes()) {
2598 Ok(r) => {
2599 let location_result: BucketLocationResult = r;
2600 location_result.region.parse()?
2601 }
2602 Err(e) => {
2603 if response_data.status_code() == 200 {
2604 Region::Custom {
2605 region: "Custom".to_string(),
2606 endpoint: "".to_string(),
2607 }
2608 } else {
2609 Region::Custom {
2610 region: format!("Error encountered : {}", e),
2611 endpoint: "".to_string(),
2612 }
2613 }
2614 }
2615 };
2616 Ok((region, response_data.status_code()))
2617 }
2618
2619 #[maybe_async::maybe_async]
2653 pub async fn delete_object<S: AsRef<str>>(&self, path: S) -> Result<ResponseData, S3Error> {
2654 let command = Command::DeleteObject;
2655 let request = RequestImpl::new(self, path.as_ref(), command).await?;
2656 request.response_data(false).await
2657 }
2658
2659 #[maybe_async::maybe_async]
2704 pub async fn delete_objects<I: Into<Vec<ObjectIdentifier>>>(
2705 &self,
2706 objects: I,
2707 ) -> Result<DeleteObjectsResult, S3Error> {
2708 let objects = objects.into();
2709 let mut result = DeleteObjectsResult {
2710 deleted: Vec::new(),
2711 errors: Vec::new(),
2712 };
2713
2714 let objects: Vec<ObjectIdentifier> = objects
2718 .into_iter()
2719 .map(|mut obj| {
2720 if let Some(stripped) = obj.key.strip_prefix('/') {
2721 obj.key = stripped.to_string();
2722 }
2723 obj
2724 })
2725 .collect();
2726
2727 for chunk in objects.chunks(1000) {
2728 let data = DeleteObjectsRequest {
2729 objects: chunk.to_vec(),
2730 quiet: false,
2731 };
2732 let command = Command::DeleteObjects { data };
2733 let request = RequestImpl::new(self, "/", command).await?;
2734 let response_data = request.response_data(false).await?;
2735 if response_data.status_code() >= 300 {
2736 return Err(error_from_response_data(response_data)?);
2737 }
2738 let msg: DeleteObjectsResult = quick_xml::de::from_str(response_data.as_str()?)?;
2739 result.deleted.extend(msg.deleted);
2740 result.errors.extend(msg.errors);
2741 }
2742
2743 Ok(result)
2744 }
2745
2746 #[maybe_async::maybe_async]
2780 pub async fn head_object<S: AsRef<str>>(
2781 &self,
2782 path: S,
2783 ) -> Result<(HeadObjectResult, u16), S3Error> {
2784 let command = Command::HeadObject;
2785 let request = RequestImpl::new(self, path.as_ref(), command).await?;
2786 let (headers, status) = request.response_header().await?;
2787 let header_object = HeadObjectResult::from(&headers);
2788 Ok((header_object, status))
2789 }
2790
2791 #[maybe_async::maybe_async]
2826 pub async fn put_object_with_content_type<S: AsRef<str>>(
2827 &self,
2828 path: S,
2829 content: &[u8],
2830 content_type: &str,
2831 ) -> Result<ResponseData, S3Error> {
2832 let command = Command::PutObject {
2833 content,
2834 content_type,
2835 custom_headers: None,
2836 multipart: None,
2837 };
2838 let request = RequestImpl::new(self, path.as_ref(), command).await?;
2839 request.response_data(true).await
2840 }
2841
2842 #[maybe_async::maybe_async]
2886 pub async fn put_object_with_content_type_and_headers<S: AsRef<str>>(
2887 &self,
2888 path: S,
2889 content: &[u8],
2890 content_type: &str,
2891 custom_headers: Option<HeaderMap>,
2892 ) -> Result<ResponseData, S3Error> {
2893 let command = Command::PutObject {
2894 content,
2895 content_type,
2896 custom_headers,
2897 multipart: None,
2898 };
2899 let request = RequestImpl::new(self, path.as_ref(), command).await?;
2900 request.response_data(true).await
2901 }
2902
2903 #[maybe_async::maybe_async]
2944 pub async fn put_object_with_headers<S: AsRef<str>>(
2945 &self,
2946 path: S,
2947 content: &[u8],
2948 custom_headers: Option<HeaderMap>,
2949 ) -> Result<ResponseData, S3Error> {
2950 self.put_object_with_content_type_and_headers(
2951 path,
2952 content,
2953 "application/octet-stream",
2954 custom_headers,
2955 )
2956 .await
2957 }
2958
2959 #[maybe_async::maybe_async]
2994 pub async fn put_object<S: AsRef<str>>(
2995 &self,
2996 path: S,
2997 content: &[u8],
2998 ) -> Result<ResponseData, S3Error> {
2999 self.put_object_with_content_type(path, content, "application/octet-stream")
3000 .await
3001 }
3002
3003 pub fn put_object_builder<S: AsRef<str>>(
3040 &self,
3041 path: S,
3042 content: &[u8],
3043 ) -> crate::put_object_request::PutObjectRequest<'_> {
3044 crate::put_object_request::PutObjectRequest::new(self, path, content)
3045 }
3046
3047 fn _tags_xml<S: AsRef<str>>(&self, tags: &[(S, S)]) -> String {
3048 let mut s = String::new();
3049 let content = tags
3050 .iter()
3051 .map(|(name, value)| {
3052 format!(
3053 "<Tag><Key>{}</Key><Value>{}</Value></Tag>",
3054 name.as_ref(),
3055 value.as_ref()
3056 )
3057 })
3058 .fold(String::new(), |mut a, b| {
3059 a.push_str(b.as_str());
3060 a
3061 });
3062 s.push_str("<Tagging><TagSet>");
3063 s.push_str(&content);
3064 s.push_str("</TagSet></Tagging>");
3065 s
3066 }
3067
3068 #[maybe_async::maybe_async]
3102 pub async fn put_object_tagging<S: AsRef<str>>(
3103 &self,
3104 path: &str,
3105 tags: &[(S, S)],
3106 ) -> Result<ResponseData, S3Error> {
3107 let content = self._tags_xml(tags);
3108 let command = Command::PutObjectTagging { tags: &content };
3109 let request = RequestImpl::new(self, path, command).await?;
3110 request.response_data(false).await
3111 }
3112
3113 #[maybe_async::maybe_async]
3147 pub async fn delete_object_tagging<S: AsRef<str>>(
3148 &self,
3149 path: S,
3150 ) -> Result<ResponseData, S3Error> {
3151 let command = Command::DeleteObjectTagging;
3152 let request = RequestImpl::new(self, path.as_ref(), command).await?;
3153 request.response_data(false).await
3154 }
3155
3156 #[cfg(feature = "tags")]
3190 #[maybe_async::maybe_async]
3191 pub async fn get_object_tagging<S: AsRef<str>>(
3192 &self,
3193 path: S,
3194 ) -> Result<(Vec<Tag>, u16), S3Error> {
3195 let command = Command::GetObjectTagging {};
3196 let request = RequestImpl::new(self, path.as_ref(), command).await?;
3197 let result = request.response_data(false).await?;
3198
3199 let mut tags = Vec::new();
3200
3201 if result.status_code() == 200 {
3202 let result_string = String::from_utf8_lossy(result.as_slice());
3203
3204 let ns = "http://s3.amazonaws.com/doc/2006-03-01/";
3206 let result_string =
3207 if let Err(minidom::Error::MissingNamespace) = result_string.parse::<Element>() {
3208 result_string
3209 .replace("<Tagging>", &format!("<Tagging xmlns=\"{}\">", ns))
3210 .into()
3211 } else {
3212 result_string
3213 };
3214
3215 if let Ok(tagging) = result_string.parse::<Element>() {
3216 for tag_set in tagging.children() {
3217 if tag_set.is("TagSet", ns) {
3218 for tag in tag_set.children() {
3219 if tag.is("Tag", ns) {
3220 let key = if let Some(element) = tag.get_child("Key", ns) {
3221 element.text()
3222 } else {
3223 "Could not parse Key from Tag".to_string()
3224 };
3225 let value = if let Some(element) = tag.get_child("Value", ns) {
3226 element.text()
3227 } else {
3228 "Could not parse Values from Tag".to_string()
3229 };
3230 tags.push(Tag { key, value });
3231 }
3232 }
3233 }
3234 }
3235 }
3236 }
3237
3238 Ok((tags, result.status_code()))
3239 }
3240
3241 #[maybe_async::maybe_async]
3242 pub async fn list_page(
3243 &self,
3244 prefix: String,
3245 delimiter: Option<String>,
3246 continuation_token: Option<String>,
3247 start_after: Option<String>,
3248 max_keys: Option<usize>,
3249 ) -> Result<(ListBucketResult, u16), S3Error> {
3250 let command = if self.listobjects_v2 {
3251 Command::ListObjectsV2 {
3252 prefix,
3253 delimiter,
3254 continuation_token,
3255 start_after,
3256 max_keys,
3257 }
3258 } else {
3259 Command::ListObjects {
3263 prefix,
3264 delimiter,
3265 marker: std::cmp::max(continuation_token, start_after),
3266 max_keys,
3267 }
3268 };
3269 let request = RequestImpl::new(self, "/", command).await?;
3270 let response_data = request.response_data(false).await?;
3271 let list_bucket_result = quick_xml::de::from_reader(response_data.as_slice())?;
3272
3273 Ok((list_bucket_result, response_data.status_code()))
3274 }
3275
3276 #[maybe_async::maybe_async]
3310 #[allow(clippy::assigning_clones)]
3311 pub async fn list(
3312 &self,
3313 prefix: String,
3314 delimiter: Option<String>,
3315 ) -> Result<Vec<ListBucketResult>, S3Error> {
3316 let the_bucket = self.to_owned();
3317 let mut results = Vec::new();
3318 let mut continuation_token = None;
3319
3320 loop {
3321 let (list_bucket_result, _) = the_bucket
3322 .list_page(
3323 prefix.clone(),
3324 delimiter.clone(),
3325 continuation_token,
3326 None,
3327 None,
3328 )
3329 .await?;
3330 continuation_token = list_bucket_result.next_continuation_token.clone();
3331 results.push(list_bucket_result);
3332 if continuation_token.is_none() {
3333 break;
3334 }
3335 }
3336
3337 Ok(results)
3338 }
3339
3340 #[maybe_async::maybe_async]
3344 pub async fn list_multiparts_uploads_page(
3345 &self,
3346 prefix: Option<&str>,
3347 delimiter: Option<&str>,
3348 key_marker: Option<String>,
3349 upload_id_marker: Option<String>,
3350 max_uploads: Option<usize>,
3351 ) -> Result<(ListMultipartUploadsResult, u16), S3Error> {
3352 let command = Command::ListMultipartUploads {
3353 prefix,
3354 delimiter,
3355 key_marker,
3356 upload_id_marker,
3357 max_uploads,
3358 };
3359 let request = RequestImpl::new(self, "/", command).await?;
3360 let response_data = request.response_data(false).await?;
3361 let list_bucket_result = quick_xml::de::from_reader(response_data.as_slice())?;
3362
3363 Ok((list_bucket_result, response_data.status_code()))
3364 }
3365
3366 #[maybe_async::maybe_async]
3403 pub async fn list_multiparts_uploads(
3404 &self,
3405 prefix: Option<&str>,
3406 delimiter: Option<&str>,
3407 ) -> Result<Vec<ListMultipartUploadsResult>, S3Error> {
3408 let the_bucket = self.to_owned();
3409 let mut results = Vec::new();
3410 let mut key_marker: Option<String> = None;
3411 let mut upload_id_marker: Option<String> = None;
3412 let mut seen_cursors = HashSet::new();
3413
3414 loop {
3415 if !seen_cursors.insert((key_marker.clone(), upload_id_marker.clone())) {
3416 return Err(S3Error::InvalidMultipartUploadsPagination(
3417 "pagination cursor repeated",
3418 ));
3419 }
3420
3421 let (list_multiparts_uploads_result, _) = the_bucket
3422 .list_multiparts_uploads_page(prefix, delimiter, key_marker, upload_id_marker, None)
3423 .await?;
3424
3425 if !list_multiparts_uploads_result.is_truncated {
3426 results.push(list_multiparts_uploads_result);
3427 break;
3428 }
3429
3430 let next_key_marker = list_multiparts_uploads_result.next_marker.clone().ok_or(
3431 S3Error::InvalidMultipartUploadsPagination(
3432 "truncated response has no next key marker",
3433 ),
3434 )?;
3435 let next_upload_id_marker =
3436 list_multiparts_uploads_result.next_upload_id_marker.clone();
3437 if seen_cursors
3438 .contains(&(Some(next_key_marker.clone()), next_upload_id_marker.clone()))
3439 {
3440 return Err(S3Error::InvalidMultipartUploadsPagination(
3441 "pagination cursor repeated",
3442 ));
3443 }
3444
3445 key_marker = Some(next_key_marker);
3446 upload_id_marker = next_upload_id_marker;
3447 results.push(list_multiparts_uploads_result);
3448 }
3449
3450 Ok(results)
3451 }
3452
3453 #[maybe_async::maybe_async]
3487 pub async fn abort_upload(&self, key: &str, upload_id: &str) -> Result<(), S3Error> {
3488 let abort = Command::AbortMultipartUpload { upload_id };
3489 let abort_request = RequestImpl::new(self, key, abort).await?;
3490 let response_data = abort_request.response_data(false).await?;
3491
3492 if (200..300).contains(&response_data.status_code()) {
3493 Ok(())
3494 } else {
3495 let utf8_content = String::from_utf8(response_data.as_slice().to_vec())?;
3496 Err(S3Error::HttpFailWithBody(
3497 response_data.status_code(),
3498 utf8_content,
3499 ))
3500 }
3501 }
3502
3503 pub fn is_path_style(&self) -> bool {
3505 self.path_style
3506 }
3507
3508 pub fn is_subdomain_style(&self) -> bool {
3510 !self.path_style
3511 }
3512
3513 pub fn set_path_style(&mut self) {
3515 self.path_style = true;
3516 }
3517
3518 pub fn set_subdomain_style(&mut self) {
3520 self.path_style = false;
3521 }
3522
3523 pub fn set_request_timeout(&mut self, timeout: Option<Duration>) {
3531 self.request_timeout = timeout;
3532 }
3533
3534 pub fn set_listobjects_v1(&mut self) {
3540 self.listobjects_v2 = false;
3541 }
3542
3543 pub fn set_listobjects_v2(&mut self) {
3545 self.listobjects_v2 = true;
3546 }
3547
3548 pub fn name(&self) -> String {
3550 self.name.to_string()
3551 }
3552
3553 pub fn host(&self) -> String {
3555 if self.path_style {
3556 self.path_style_host()
3557 } else {
3558 self.subdomain_style_host()
3559 }
3560 }
3561
3562 pub fn url(&self) -> String {
3563 if self.path_style {
3564 format!(
3565 "{}://{}/{}",
3566 self.scheme(),
3567 self.path_style_host(),
3568 self.name()
3569 )
3570 } else {
3571 format!("{}://{}", self.scheme(), self.subdomain_style_host())
3572 }
3573 }
3574
3575 pub fn path_style_host(&self) -> String {
3577 self.region.host()
3578 }
3579
3580 pub fn subdomain_style_host(&self) -> String {
3581 format!("{}.{}", self.name, self.region.host())
3582 }
3583
3584 pub fn scheme(&self) -> String {
3589 self.region.scheme()
3590 }
3591
3592 pub fn region(&self) -> Region {
3594 self.region.clone()
3595 }
3596
3597 #[maybe_async::maybe_async]
3599 pub async fn access_key(&self) -> Result<Option<String>, S3Error> {
3600 Ok(self.credentials().await?.access_key)
3601 }
3602
3603 #[maybe_async::maybe_async]
3605 pub async fn secret_key(&self) -> Result<Option<String>, S3Error> {
3606 Ok(self.credentials().await?.secret_key)
3607 }
3608
3609 #[maybe_async::maybe_async]
3611 pub async fn security_token(&self) -> Result<Option<String>, S3Error> {
3612 Ok(self.credentials().await?.security_token)
3613 }
3614
3615 #[maybe_async::maybe_async]
3617 pub async fn session_token(&self) -> Result<Option<String>, S3Error> {
3618 Ok(self.credentials().await?.session_token)
3619 }
3620
3621 #[maybe_async::async_impl]
3624 pub async fn credentials(&self) -> Result<Credentials, S3Error> {
3625 Ok(self.credentials.read().await.clone())
3626 }
3627
3628 #[maybe_async::sync_impl]
3629 pub fn credentials(&self) -> Result<Credentials, S3Error> {
3630 match self.credentials.read() {
3631 Ok(credentials) => Ok(credentials.clone()),
3632 Err(_) => Err(S3Error::CredentialsReadLock),
3633 }
3634 }
3635
3636 pub fn set_credentials(&mut self, credentials: Credentials) {
3638 self.credentials = Arc::new(RwLock::new(credentials));
3639 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
3640 {
3641 self.credentials_refresh_gate = Arc::new(AsyncMutex::new(()));
3642 }
3643 }
3644
3645 pub fn add_header(&mut self, key: &str, value: &str) {
3658 self.extra_headers
3659 .insert(HeaderName::from_str(key).unwrap(), value.parse().unwrap());
3660 }
3661
3662 pub fn extra_headers(&self) -> &HeaderMap {
3664 &self.extra_headers
3665 }
3666
3667 pub fn extra_headers_mut(&mut self) -> &mut HeaderMap {
3670 &mut self.extra_headers
3671 }
3672
3673 pub fn add_query(&mut self, key: &str, value: &str) {
3675 self.extra_query.insert(key.into(), value.into());
3676 }
3677
3678 pub fn extra_query(&self) -> &Query {
3680 &self.extra_query
3681 }
3682
3683 pub fn extra_query_mut(&mut self) -> &mut Query {
3686 &mut self.extra_query
3687 }
3688
3689 pub fn request_timeout(&self) -> Option<Duration> {
3690 self.request_timeout
3691 }
3692}
3693
3694#[cfg(test)]
3695mod test {
3696
3697 use super::validate_success_xml_response;
3698
3699 use crate::BucketConfiguration;
3700 use crate::Tag;
3701 use crate::creds::Credentials;
3702 use crate::error::S3Error;
3703 use crate::post_policy::{PostPolicyField, PostPolicyValue};
3704 use crate::region::Region;
3705 use crate::request::ResponseData;
3706 use crate::serde_types::{
3707 BucketLifecycleConfiguration, CorsConfiguration, CorsRule, Expiration, LifecycleFilter,
3708 LifecycleRule, Part,
3709 };
3710 use crate::{Bucket, PostPolicy};
3711 use http::header::{CACHE_CONTROL, HeaderMap, HeaderName, HeaderValue};
3712 use std::collections::HashMap;
3713 use std::env;
3714 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3715 use std::io::{Read, Write};
3716 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3717 use std::net::TcpListener;
3718 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3719 use std::sync::mpsc::{self, TryRecvError};
3720 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3721 use std::thread;
3722 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3723 use std::time::{Duration, Instant};
3724
3725 fn init() {
3726 let _ = env_logger::builder().is_test(true).try_init();
3727 }
3728
3729 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
3730 #[test]
3731 fn concurrent_chunk_calculation_is_bounded_and_has_a_fallback() {
3732 let per_chunk = super::CHUNK_SIZE as u64 * 3;
3733 assert_eq!(Bucket::max_concurrent_chunks_for_memory(0), 3);
3734 assert_eq!(Bucket::max_concurrent_chunks_for_memory(per_chunk - 1), 2);
3735 assert_eq!(Bucket::max_concurrent_chunks_for_memory(per_chunk * 9), 9);
3736 assert_eq!(Bucket::max_concurrent_chunks_for_memory(per_chunk * 10), 10);
3737 assert_eq!(Bucket::max_concurrent_chunks_for_memory(u64::MAX), 10);
3738 }
3739
3740 fn test_object_key(suffix: &str) -> String {
3741 format!(
3742 "{}{suffix}",
3743 env::var("RUST_S3_TEST_PREFIX").unwrap_or_default()
3744 )
3745 }
3746
3747 fn test_object_path(suffix: &str) -> String {
3748 format!("/{}", test_object_key(suffix))
3749 }
3750
3751 fn xml_response(body: &str) -> ResponseData {
3752 ResponseData::new(
3753 bytes::Bytes::copy_from_slice(body.as_bytes()),
3754 200,
3755 HashMap::new(),
3756 )
3757 }
3758
3759 fn make_mock_stream_blocking(
3760 stream: std::net::TcpStream,
3761 ) -> std::io::Result<std::net::TcpStream> {
3762 stream.set_nonblocking(false)?;
3764 Ok(stream)
3765 }
3766
3767 #[maybe_async::maybe_async]
3768 async fn run_multipart_upload_listing_case(
3769 responses: Vec<String>,
3770 ) -> (
3771 Result<Vec<crate::serde_types::ListMultipartUploadsResult>, S3Error>,
3772 Result<Vec<String>, String>,
3773 ) {
3774 use std::io::{Read, Write};
3775 use std::net::TcpListener;
3776 use std::sync::mpsc;
3777 use std::thread;
3778 use std::time::{Duration, Instant};
3779
3780 fn read_request_target(
3781 stream: &mut std::net::TcpStream,
3782 deadline: Instant,
3783 ) -> Result<String, String> {
3784 let mut headers = Vec::new();
3785 let mut byte = [0u8; 1];
3786 while !headers.ends_with(b"\r\n\r\n") {
3787 if headers.len() >= 16 * 1024 || Instant::now() >= deadline {
3788 return Err("request headers exceeded the fixture bound".to_owned());
3789 }
3790 match stream.read(&mut byte) {
3791 Ok(0) => return Err("client closed before sending request headers".to_owned()),
3792 Ok(_) => headers.push(byte[0]),
3793 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
3794 Err(error) => return Err(format!("failed to read request headers: {error}")),
3795 }
3796 }
3797 let request = String::from_utf8_lossy(&headers);
3798 let mut fields = request
3799 .lines()
3800 .next()
3801 .unwrap_or_default()
3802 .split_whitespace();
3803 let method = fields.next().unwrap_or_default();
3804 let target = fields.next().unwrap_or_default();
3805 if method != "GET" || target.is_empty() {
3806 return Err("unexpected request line in multipart listing fixture".to_owned());
3807 }
3808 Ok(target.to_owned())
3809 }
3810
3811 fn send_listing_response(
3812 stream: &mut std::net::TcpStream,
3813 body: &str,
3814 ) -> std::io::Result<()> {
3815 write!(
3816 stream,
3817 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
3818 body.len(),
3819 body
3820 )
3821 }
3822
3823 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3824 listener.set_nonblocking(true).unwrap();
3825 let endpoint = format!("http://{}", listener.local_addr().unwrap());
3826 let (done_tx, done_rx) = mpsc::channel();
3827 let server = thread::spawn(move || {
3828 let deadline = Instant::now() + Duration::from_secs(8);
3829 let mut requests = Vec::new();
3830 for body in &responses {
3831 let (stream, _) = loop {
3832 match listener.accept() {
3833 Ok(connection) => break connection,
3834 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
3835 if Instant::now() >= deadline {
3836 return Err("timed out waiting for a pagination request".to_owned());
3837 }
3838 if done_rx.try_recv().is_ok() {
3839 return Err(
3840 "client finished before all scripted requests".to_owned()
3841 );
3842 }
3843 thread::sleep(Duration::from_millis(2));
3844 }
3845 Err(error) => return Err(format!("failed to accept request: {error}")),
3846 }
3847 };
3848 let mut stream = make_mock_stream_blocking(stream)
3849 .map_err(|error| format!("failed to configure accepted stream: {error}"))?;
3850 stream
3851 .set_read_timeout(Some(Duration::from_secs(3)))
3852 .map_err(|error| format!("failed to set read timeout: {error}"))?;
3853 stream
3854 .set_write_timeout(Some(Duration::from_secs(3)))
3855 .map_err(|error| format!("failed to set write timeout: {error}"))?;
3856 let target = read_request_target(&mut stream, deadline)?;
3857 requests.push(target);
3858 send_listing_response(&mut stream, body)
3859 .map_err(|error| format!("failed to send listing page: {error}"))?;
3860 }
3861
3862 loop {
3863 match listener.accept() {
3864 Ok((stream, _)) => {
3865 let mut stream = make_mock_stream_blocking(stream).map_err(|error| {
3866 format!("failed to configure extra stream: {error}")
3867 })?;
3868 stream
3869 .set_read_timeout(Some(Duration::from_secs(3)))
3870 .map_err(|error| format!("failed to set read timeout: {error}"))?;
3871 let target = read_request_target(&mut stream, deadline)?;
3872 requests.push(target);
3873 let final_page = "<ListMultipartUploadsResult><Bucket>test-bucket</Bucket><IsTruncated>false</IsTruncated></ListMultipartUploadsResult>";
3875 send_listing_response(&mut stream, final_page)
3876 .map_err(|error| format!("failed to answer extra request: {error}"))?;
3877 return Err("client made more pagination requests than scripted".to_owned());
3878 }
3879 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
3880 if done_rx.recv_timeout(Duration::from_millis(10)).is_ok() {
3881 return Ok(requests);
3882 }
3883 if Instant::now() >= deadline {
3884 return Err("client did not finish before fixture deadline".to_owned());
3885 }
3886 }
3887 Err(error) => return Err(format!("failed to accept extra request: {error}")),
3888 }
3889 }
3890 });
3891
3892 let credentials = Credentials::new(
3893 Some("test_access_key"),
3894 Some("test_secret_key"),
3895 None,
3896 None,
3897 None,
3898 )
3899 .unwrap();
3900 let bucket = Bucket::new(
3901 "test-bucket",
3902 Region::Custom {
3903 region: "us-east-1".to_owned(),
3904 endpoint,
3905 },
3906 credentials,
3907 )
3908 .unwrap()
3909 .with_path_style()
3910 .with_request_timeout(Duration::from_secs(4))
3911 .unwrap();
3912 let result = bucket.list_multiparts_uploads(Some("same-key"), None).await;
3913 let _ = done_tx.send(());
3914 let server_result = server.join().expect("multipart listing server panicked");
3915 (result, server_result)
3916 }
3917
3918 fn multipart_listing_xml(
3919 next_key_marker: Option<&str>,
3920 next_upload_id_marker: Option<&str>,
3921 is_truncated: bool,
3922 upload: Option<(&str, &str)>,
3923 ) -> String {
3924 let mut xml = String::from("<ListMultipartUploadsResult><Bucket>test-bucket</Bucket>");
3925 if let Some(marker) = next_key_marker {
3926 xml.push_str(&format!("<NextKeyMarker>{marker}</NextKeyMarker>"));
3927 }
3928 if let Some(marker) = next_upload_id_marker {
3929 xml.push_str(&format!(
3930 "<NextUploadIdMarker>{marker}</NextUploadIdMarker>"
3931 ));
3932 }
3933 xml.push_str(&format!("<IsTruncated>{is_truncated}</IsTruncated>"));
3934 if let Some((key, upload_id)) = upload {
3935 xml.push_str(&format!(
3936 "<Upload><Initiated>2026-01-01T00:00:00.000Z</Initiated><StorageClass>STANDARD</StorageClass><Key>{key}</Key><UploadId>{upload_id}</UploadId></Upload>"
3937 ));
3938 }
3939 xml.push_str("</ListMultipartUploadsResult>");
3940 xml
3941 }
3942
3943 #[maybe_async::test(
3944 feature = "sync",
3945 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
3946 async(
3947 all(not(feature = "sync"), feature = "with-async-std"),
3948 async_std::test
3949 )
3950 )]
3951 async fn multipart_upload_pagination_uses_both_markers_and_clears_missing_upload_marker() {
3952 fn query(target: &str) -> HashMap<String, String> {
3953 url::Url::parse(&format!("http://localhost{target}"))
3954 .unwrap()
3955 .query_pairs()
3956 .into_owned()
3957 .collect()
3958 }
3959
3960 let (result, requests) = run_multipart_upload_listing_case(vec![
3961 multipart_listing_xml(
3962 Some("same-key"),
3963 Some("upload +/2"),
3964 true,
3965 Some(("same-key", "upload +/2")),
3966 ),
3967 multipart_listing_xml(None, None, false, Some(("same-key", "upload-2"))),
3968 ])
3969 .await;
3970 let pages = result.expect("two-page listing should succeed");
3971 let requests = requests.expect("mock server request sequence should succeed");
3972 assert_eq!(pages.len(), 2);
3973 let mut listed_ids = pages
3974 .iter()
3975 .flat_map(|page| page.uploads.iter().map(|upload| upload.id.clone()))
3976 .collect::<Vec<_>>();
3977 listed_ids.sort();
3978 assert_eq!(listed_ids, ["upload +/2", "upload-2"]);
3979 assert_eq!(requests.len(), 2);
3980 let second_query = query(&requests[1]);
3981 assert_eq!(second_query.get("key-marker").unwrap(), "same-key");
3982 assert_eq!(second_query.get("upload-id-marker").unwrap(), "upload +/2");
3983
3984 let (result, requests) = run_multipart_upload_listing_case(vec![
3985 multipart_listing_xml(Some("same-key"), Some("id-one"), true, None),
3986 multipart_listing_xml(Some("later-key"), None, true, None),
3987 multipart_listing_xml(None, None, false, None),
3988 ])
3989 .await;
3990 assert_eq!(result.unwrap().len(), 3);
3991 let requests = requests.expect("key-only continuation should succeed");
3992 assert_eq!(requests.len(), 3);
3993 let second_query = query(&requests[1]);
3994 assert_eq!(second_query.get("upload-id-marker").unwrap(), "id-one");
3995 let third_query = query(&requests[2]);
3996 assert_eq!(third_query.get("key-marker").unwrap(), "later-key");
3997 assert!(!third_query.contains_key("upload-id-marker"));
3998 }
3999
4000 #[maybe_async::test(
4001 feature = "sync",
4002 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
4003 async(
4004 all(not(feature = "sync"), feature = "with-async-std"),
4005 async_std::test
4006 )
4007 )]
4008 async fn multipart_upload_pagination_rejects_missing_repeated_and_cyclic_cursors() {
4009 let cases = vec![
4010 (
4011 "truncated response has no next key marker",
4012 vec![multipart_listing_xml(None, Some("id-a"), true, None)],
4013 1,
4014 ),
4015 (
4016 "pagination cursor repeated",
4017 vec![
4018 multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4019 multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4020 ],
4021 2,
4022 ),
4023 (
4024 "pagination cursor repeated",
4025 vec![
4026 multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4027 multipart_listing_xml(Some("same-key"), Some("id-b"), true, None),
4028 multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4029 ],
4030 3,
4031 ),
4032 ];
4033
4034 for (expected_reason, responses, expected_requests) in cases {
4035 let (result, requests) = run_multipart_upload_listing_case(responses).await;
4036 match result {
4037 Err(S3Error::InvalidMultipartUploadsPagination(reason)) => {
4038 assert_eq!(reason, expected_reason);
4039 }
4040 other => panic!("expected pagination error, got {other:?}"),
4041 }
4042 let requests = requests.expect("mock server request sequence should succeed");
4043 assert_eq!(requests.len(), expected_requests);
4044 }
4045 }
4046
4047 #[cfg(all(
4048 not(feature = "sync"),
4049 any(feature = "with-tokio", feature = "with-async-std")
4050 ))]
4051 #[derive(Debug)]
4052 struct MultipartWireRequest {
4053 line: String,
4054 headers: HashMap<String, String>,
4055 content_length: usize,
4056 }
4057
4058 #[cfg(all(
4059 not(feature = "sync"),
4060 any(feature = "with-tokio", feature = "with-async-std")
4061 ))]
4062 #[maybe_async::maybe_async]
4063 async fn run_multipart_header_case(
4064 size: usize,
4065 fail_reader: bool,
4066 custom_headers: HeaderMap,
4067 ) -> (
4068 Result<(), S3Error>,
4069 Result<Vec<MultipartWireRequest>, String>,
4070 ) {
4071 use std::io::{Read, Write};
4072 use std::net::TcpListener;
4073 use std::sync::mpsc;
4074 use std::thread;
4075 use std::time::{Duration, Instant};
4076
4077 fn read_request(
4078 stream: &mut std::net::TcpStream,
4079 deadline: Instant,
4080 ) -> Result<MultipartWireRequest, String> {
4081 let mut raw_headers = Vec::new();
4082 let mut byte = [0u8; 1];
4083 while !raw_headers.ends_with(b"\r\n\r\n") {
4084 if raw_headers.len() >= 16 * 1024 || Instant::now() >= deadline {
4085 return Err("header limit or deadline exceeded".to_owned());
4086 }
4087 stream
4088 .read_exact(&mut byte)
4089 .map_err(|error| format!("read headers: {error}"))?;
4090 raw_headers.push(byte[0]);
4091 }
4092
4093 let header_text = String::from_utf8_lossy(&raw_headers);
4094 let mut lines = header_text.lines();
4095 let line = lines.next().unwrap_or_default().to_owned();
4096 let mut headers = HashMap::new();
4097 for header in lines {
4098 if let Some((name, value)) = header.split_once(':') {
4099 let name = name.trim().to_ascii_lowercase();
4100 if name != "authorization" && name != "x-amz-security-token" {
4101 headers.insert(name, value.trim().to_owned());
4102 }
4103 }
4104 }
4105 let content_length = headers
4106 .get("content-length")
4107 .and_then(|value| value.parse::<usize>().ok())
4108 .unwrap_or(0);
4109 if content_length > super::CHUNK_SIZE + 16 * 1024 {
4110 return Err("request body exceeded test cap".to_owned());
4111 }
4112 let mut body = vec![0; content_length];
4113 stream
4114 .read_exact(&mut body)
4115 .map_err(|error| format!("read request body: {error}"))?;
4116
4117 Ok(MultipartWireRequest {
4118 line,
4119 headers,
4120 content_length,
4121 })
4122 }
4123
4124 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4125 listener.set_nonblocking(true).unwrap();
4126 let endpoint = format!("http://{}", listener.local_addr().unwrap());
4127 let (done_tx, done_rx) = mpsc::channel();
4128 let rejected_headers = custom_headers.clone();
4129 let server = thread::spawn(move || {
4130 let expected = if rejected_headers.contains_key("content-length")
4131 || rejected_headers.contains_key("content-md5")
4132 || rejected_headers.contains_key("transfer-encoding")
4133 || rejected_headers.contains_key("x-amz-content-sha256")
4134 || rejected_headers.keys().any(|name| {
4135 name.as_str().starts_with("x-amz-checksum-")
4136 || name.as_str().starts_with("x-amz-sdk-checksum-")
4137 }) {
4138 Vec::new()
4139 } else if fail_reader {
4140 vec!["init", "abort"]
4141 } else if size < super::CHUNK_SIZE {
4142 vec!["put"]
4143 } else {
4144 let part_count = size.div_ceil(super::CHUNK_SIZE);
4145 let mut steps = vec!["init"];
4146 steps.extend(std::iter::repeat_n("part", part_count));
4147 steps.push("complete");
4148 steps
4149 };
4150
4151 let deadline = Instant::now() + Duration::from_secs(12);
4152 let mut requests = Vec::new();
4153 for step in expected {
4154 let (mut stream, _) = loop {
4155 if Instant::now() >= deadline {
4156 return Err(format!("accept deadline after {} requests", requests.len()));
4157 }
4158 match listener.accept() {
4159 Ok(pair) => break pair,
4160 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
4161 thread::sleep(Duration::from_millis(5));
4162 }
4163 Err(error) => return Err(format!("accept request: {error}")),
4164 }
4165 };
4166 stream
4167 .set_nonblocking(false)
4168 .map_err(|error| error.to_string())?;
4169 stream
4170 .set_read_timeout(Some(Duration::from_secs(4)))
4171 .map_err(|error| error.to_string())?;
4172 stream
4173 .set_write_timeout(Some(Duration::from_secs(4)))
4174 .map_err(|error| error.to_string())?;
4175 let request = read_request(&mut stream, deadline)?;
4176
4177 let (status, body, etag) = match step {
4178 "init" => {
4179 if !request.line.starts_with("POST ") || !request.line.contains("uploads") {
4180 return Err(format!("unexpected initiation request: {}", request.line));
4181 }
4182 (
4183 200,
4184 "<InitiateMultipartUploadResult><Bucket>test-bucket</Bucket><Key>multipart-header-test</Key><UploadId>header-upload-id</UploadId></InitiateMultipartUploadResult>",
4185 "",
4186 )
4187 }
4188 "part" => {
4189 if !request.line.starts_with("PUT ")
4190 || !request.line.contains("partNumber=")
4191 {
4192 return Err(format!("unexpected part request: {}", request.line));
4193 }
4194 (200, "", "ETag: \"part-etag\"\r\n")
4195 }
4196 "complete" => {
4197 if !request.line.starts_with("POST ") || !request.line.contains("uploadId=")
4198 {
4199 return Err(format!("unexpected completion request: {}", request.line));
4200 }
4201 (200, "<CompleteMultipartUploadResult/>", "")
4202 }
4203 "abort" => {
4204 if !request.line.starts_with("DELETE ")
4205 || !request.line.contains("uploadId=")
4206 {
4207 return Err(format!("unexpected abort request: {}", request.line));
4208 }
4209 (204, "", "")
4210 }
4211 _ => {
4212 if !request.line.starts_with("PUT ") || request.line.contains('?') {
4213 return Err(format!("unexpected small PUT request: {}", request.line));
4214 }
4215 (200, "", "ETag: \"small-etag\"\r\n")
4216 }
4217 };
4218
4219 let response = format!(
4220 "HTTP/1.1 {status} OK\r\nContent-Length: {}\r\n{etag}Connection: close\r\n\r\n{body}",
4221 body.len()
4222 );
4223 stream
4224 .write_all(response.as_bytes())
4225 .map_err(|error| format!("write response: {error}"))?;
4226 requests.push(request);
4227 }
4228
4229 loop {
4232 if done_rx.try_recv().is_ok() {
4233 match listener.accept() {
4234 Ok((mut stream, _)) => {
4235 stream
4236 .set_nonblocking(false)
4237 .map_err(|error| error.to_string())?;
4238 stream
4239 .set_read_timeout(Some(Duration::from_secs(4)))
4240 .map_err(|error| error.to_string())?;
4241 requests.push(read_request(&mut stream, deadline)?);
4242 }
4243 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
4244 Err(error) => return Err(format!("final accept check: {error}")),
4245 }
4246 return Ok(requests);
4247 }
4248 if Instant::now() >= deadline {
4249 return Err(format!(
4250 "operation completion deadline after {} requests",
4251 requests.len()
4252 ));
4253 }
4254 match listener.accept() {
4255 Ok((mut stream, _)) => {
4256 stream
4257 .set_nonblocking(false)
4258 .map_err(|error| error.to_string())?;
4259 stream
4260 .set_read_timeout(Some(Duration::from_secs(4)))
4261 .map_err(|error| error.to_string())?;
4262 let request = read_request(&mut stream, deadline)?;
4263 requests.push(request);
4264 return Ok(requests);
4265 }
4266 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
4267 thread::sleep(Duration::from_millis(5));
4268 }
4269 Err(error) => return Err(format!("accept extra request: {error}")),
4270 }
4271 }
4272 });
4273
4274 let credentials = Credentials::new(
4275 Some("test-access-key"),
4276 Some("test-secret-key"),
4277 None,
4278 None,
4279 None,
4280 )
4281 .unwrap();
4282 let mut bucket = Bucket::new(
4283 "test-bucket",
4284 Region::Custom {
4285 region: "us-east-1".to_owned(),
4286 endpoint,
4287 },
4288 credentials,
4289 )
4290 .unwrap()
4291 .with_path_style();
4292 bucket.add_header("x-amz-meta-uploader", "bucket-level-value");
4293 bucket.add_header("x-amz-request-payer", "owner-level-value");
4294 bucket.add_header("x-test-global-option", "raw-global-value");
4295 let mut reader = MultipartFaultReader {
4296 remaining: size,
4297 fail_at_eof: fail_reader,
4298 };
4299 let result = bucket
4300 .put_object_stream_builder("/multipart-header-test")
4301 .with_content_type("text/plain")
4302 .with_headers(custom_headers)
4303 .execute_stream(&mut reader)
4304 .await
4305 .map(|_| ());
4306 let _ = done_tx.send(());
4307 let server_result = server.join().expect("multipart header server panicked");
4308 (result, server_result)
4309 }
4310
4311 #[cfg(all(
4312 not(feature = "sync"),
4313 any(feature = "with-tokio", feature = "with-async-std")
4314 ))]
4315 fn multipart_test_headers() -> HeaderMap {
4316 let mut headers = HeaderMap::new();
4317 for (name, value) in [
4318 ("x-amz-meta-uploader", "synthetic-test"),
4319 ("content-type", "application/x-custom-overridden"),
4320 ("cache-control", "public, max-age=120"),
4321 ("content-disposition", "attachment; filename=test.bin"),
4322 ("x-amz-storage-class", "STANDARD_IA"),
4323 ("x-amz-server-side-encryption", "aws:kms"),
4324 (
4325 "x-amz-server-side-encryption-aws-kms-key-id",
4326 "synthetic-kms-key",
4327 ),
4328 ("x-test-provider-option", "init-only"),
4329 ("x-amz-server-side-encryption-customer-algorithm", "AES256"),
4330 (
4331 "x-amz-server-side-encryption-customer-key",
4332 "synthetic-key-value",
4333 ),
4334 (
4335 "x-amz-server-side-encryption-customer-key-md5",
4336 "synthetic-key-md5",
4337 ),
4338 ("x-amz-expected-bucket-owner", "123456789012"),
4339 ("x-amz-request-payer", "requester"),
4340 ("if-match", "\"existing-etag\""),
4341 ("if-none-match", "\"other-etag\""),
4342 ] {
4343 headers.insert(
4344 HeaderName::from_bytes(name.as_bytes()).unwrap(),
4345 HeaderValue::from_str(value).unwrap(),
4346 );
4347 }
4348 headers
4349 }
4350
4351 #[cfg(all(
4352 not(feature = "sync"),
4353 any(feature = "with-tokio", feature = "with-async-std")
4354 ))]
4355 fn assert_wire_header(request: &MultipartWireRequest, name: &str, expected: Option<&str>) {
4356 assert_eq!(
4357 request.headers.get(name).map(String::as_str),
4358 expected,
4359 "{} {name}",
4360 request.line
4361 );
4362 }
4363
4364 #[cfg(all(
4365 not(feature = "sync"),
4366 any(feature = "with-tokio", feature = "with-async-std")
4367 ))]
4368 #[maybe_async::maybe_async]
4369 async fn assert_multipart_builder_header_routing() {
4370 let headers = multipart_test_headers();
4371 for size in [
4372 super::CHUNK_SIZE - 1,
4373 super::CHUNK_SIZE,
4374 super::CHUNK_SIZE + 1,
4375 ] {
4376 let (result, server_result) =
4377 run_multipart_header_case(size, false, headers.clone()).await;
4378 assert!(result.is_ok(), "stream upload failed: {result:?}");
4379 let requests = server_result.expect("local HTTP fixture failed");
4380
4381 if size < super::CHUNK_SIZE {
4382 assert_eq!(requests.len(), 1);
4383 let put = &requests[0];
4384 assert!(put.line.starts_with("PUT "));
4385 for name in [
4386 "x-amz-meta-uploader",
4387 "cache-control",
4388 "content-disposition",
4389 "x-amz-storage-class",
4390 "x-amz-server-side-encryption",
4391 "x-amz-server-side-encryption-aws-kms-key-id",
4392 "x-test-provider-option",
4393 "x-amz-server-side-encryption-customer-algorithm",
4394 "x-amz-server-side-encryption-customer-key",
4395 "x-amz-server-side-encryption-customer-key-md5",
4396 "x-amz-expected-bucket-owner",
4397 "x-amz-request-payer",
4398 "if-match",
4399 "if-none-match",
4400 ] {
4401 assert_wire_header(put, name, Some(headers[name].to_str().unwrap()));
4402 }
4403 assert_wire_header(put, "x-test-global-option", Some("raw-global-value"));
4404 assert_wire_header(put, "content-type", Some("text/plain"));
4405 assert_wire_header(put, "content-length", Some(&size.to_string()));
4406 assert!(put.headers.contains_key("content-md5"));
4407 continue;
4408 }
4409
4410 let initiate = requests
4411 .iter()
4412 .find(|request| request.line.contains("uploads"))
4413 .expect("multipart initiation request missing");
4414 assert_wire_header(initiate, "content-type", Some("text/plain"));
4415 assert_wire_header(initiate, "content-length", Some("0"));
4416 for name in [
4417 "x-amz-meta-uploader",
4418 "cache-control",
4419 "content-disposition",
4420 "x-amz-storage-class",
4421 "x-amz-server-side-encryption",
4422 "x-amz-server-side-encryption-aws-kms-key-id",
4423 "x-test-provider-option",
4424 "x-amz-server-side-encryption-customer-algorithm",
4425 "x-amz-server-side-encryption-customer-key",
4426 "x-amz-server-side-encryption-customer-key-md5",
4427 "x-amz-expected-bucket-owner",
4428 "x-amz-request-payer",
4429 ] {
4430 assert_wire_header(initiate, name, Some(headers[name].to_str().unwrap()));
4431 }
4432 assert_wire_header(initiate, "x-test-global-option", Some("raw-global-value"));
4433 for name in ["if-match", "if-none-match"] {
4434 assert_wire_header(initiate, name, None);
4435 }
4436
4437 let parts = requests
4438 .iter()
4439 .filter(|request| request.line.contains("partNumber="))
4440 .collect::<Vec<_>>();
4441 let mut part_numbers_and_lengths = parts
4442 .iter()
4443 .map(|request| {
4444 let target = request.line.split_whitespace().nth(1).unwrap();
4445 let query = target.split_once('?').unwrap().1;
4446 let part_number = query
4447 .split('&')
4448 .find_map(|pair| pair.strip_prefix("partNumber="))
4449 .unwrap()
4450 .parse::<u32>()
4451 .unwrap();
4452 (part_number, request.content_length)
4453 })
4454 .collect::<Vec<_>>();
4455 part_numbers_and_lengths.sort_unstable_by_key(|(part_number, _)| *part_number);
4456 let expected_parts = if size == super::CHUNK_SIZE {
4457 vec![(1, super::CHUNK_SIZE)]
4458 } else {
4459 vec![(1, super::CHUNK_SIZE), (2, 1)]
4460 };
4461 assert_eq!(part_numbers_and_lengths, expected_parts);
4462 for part in parts {
4463 assert_wire_header(part, "content-type", Some("text/plain"));
4464 assert_wire_header(
4465 part,
4466 "content-length",
4467 Some(&part.content_length.to_string()),
4468 );
4469 assert!(part.headers.contains_key("content-md5"));
4470 for name in [
4471 "x-amz-server-side-encryption-customer-algorithm",
4472 "x-amz-server-side-encryption-customer-key",
4473 "x-amz-server-side-encryption-customer-key-md5",
4474 "x-amz-expected-bucket-owner",
4475 "x-amz-request-payer",
4476 ] {
4477 assert_wire_header(part, name, Some(headers[name].to_str().unwrap()));
4478 }
4479 assert_wire_header(part, "x-test-global-option", Some("raw-global-value"));
4480 for name in [
4481 "x-amz-meta-uploader",
4482 "cache-control",
4483 "content-disposition",
4484 "x-amz-storage-class",
4485 "x-amz-server-side-encryption",
4486 "x-amz-server-side-encryption-aws-kms-key-id",
4487 "x-test-provider-option",
4488 "if-match",
4489 "if-none-match",
4490 ] {
4491 assert_wire_header(part, name, None);
4492 }
4493 }
4494
4495 let complete = requests
4496 .iter()
4497 .find(|request| {
4498 request.line.starts_with("POST ") && request.line.contains("uploadId=")
4499 })
4500 .expect("multipart completion request missing");
4501 assert_wire_header(complete, "content-type", Some("application/xml"));
4502 assert!(complete.content_length > 0);
4503 for name in [
4504 "x-amz-server-side-encryption-customer-algorithm",
4505 "x-amz-server-side-encryption-customer-key",
4506 "x-amz-server-side-encryption-customer-key-md5",
4507 "x-amz-expected-bucket-owner",
4508 "x-amz-request-payer",
4509 "if-match",
4510 "if-none-match",
4511 ] {
4512 assert_wire_header(complete, name, Some(headers[name].to_str().unwrap()));
4513 }
4514 assert_wire_header(complete, "x-test-global-option", Some("raw-global-value"));
4515 for name in [
4516 "x-amz-meta-uploader",
4517 "cache-control",
4518 "content-disposition",
4519 "x-amz-storage-class",
4520 "x-amz-server-side-encryption",
4521 "x-amz-server-side-encryption-aws-kms-key-id",
4522 "x-test-provider-option",
4523 ] {
4524 assert_wire_header(complete, name, None);
4525 }
4526 }
4527
4528 let (result, server_result) =
4529 run_multipart_header_case(super::CHUNK_SIZE, true, headers.clone()).await;
4530 assert!(matches!(
4531 result,
4532 Err(S3Error::Io(error)) if error.to_string() == "injected reader failure"
4533 ));
4534 let requests = server_result.expect("abort fixture failed");
4535 assert_eq!(requests.len(), 2);
4536 let abort = requests
4537 .iter()
4538 .find(|request| request.line.starts_with("DELETE "))
4539 .expect("multipart abort request missing");
4540 for name in ["x-amz-expected-bucket-owner", "x-amz-request-payer"] {
4541 assert_wire_header(abort, name, Some(headers[name].to_str().unwrap()));
4542 }
4543 assert_wire_header(abort, "x-test-global-option", Some("raw-global-value"));
4544 for name in [
4545 "x-amz-server-side-encryption-customer-algorithm",
4546 "x-amz-server-side-encryption-customer-key",
4547 "x-amz-server-side-encryption-customer-key-md5",
4548 "x-amz-meta-uploader",
4549 "cache-control",
4550 "if-match",
4551 "if-none-match",
4552 ] {
4553 assert_wire_header(abort, name, None);
4554 }
4555 assert!(matches!(
4556 abort.headers.get("content-length").map(String::as_str),
4557 None | Some("0")
4558 ));
4559 assert!(matches!(
4560 abort.headers.get("content-type").map(String::as_str),
4561 None | Some("application/octet-stream")
4562 ));
4563
4564 for rejected_name in [
4565 "content-md5",
4566 "content-length",
4567 "transfer-encoding",
4568 "x-amz-content-sha256",
4569 "x-amz-checksum-sha256",
4570 "x-amz-sdk-checksum-algorithm",
4571 ] {
4572 let mut rejected = HeaderMap::new();
4573 rejected.insert(
4574 HeaderName::from_bytes(rejected_name.as_bytes()).unwrap(),
4575 HeaderValue::from_static("synthetic-value"),
4576 );
4577 let (result, server_result) =
4578 run_multipart_header_case(super::CHUNK_SIZE, false, rejected).await;
4579 assert!(matches!(
4580 result,
4581 Err(S3Error::UnsupportedMultipartHeader(ref name)) if name.as_str() == rejected_name
4582 ));
4583 assert!(
4584 server_result.expect("rejection fixture failed").is_empty(),
4585 "rejected headers must fail before initiation"
4586 );
4587 }
4588 }
4589
4590 #[cfg(all(
4591 not(feature = "sync"),
4592 any(feature = "with-tokio", feature = "with-async-std")
4593 ))]
4594 struct MultipartTrackedReader {
4595 remaining: usize,
4596 bytes_read: std::sync::Arc<std::sync::atomic::AtomicUsize>,
4597 }
4598
4599 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
4600 impl tokio::io::AsyncRead for MultipartTrackedReader {
4601 fn poll_read(
4602 self: std::pin::Pin<&mut Self>,
4603 _context: &mut std::task::Context<'_>,
4604 buffer: &mut tokio::io::ReadBuf<'_>,
4605 ) -> std::task::Poll<std::io::Result<()>> {
4606 use std::sync::atomic::Ordering;
4607 let this = self.get_mut();
4608 let count = this.remaining.min(buffer.remaining());
4609 let offset = this.bytes_read.fetch_add(count, Ordering::SeqCst);
4610 for (index, byte) in buffer.initialize_unfilled()[..count].iter_mut().enumerate() {
4611 *byte = ((offset + index) % 251) as u8;
4612 }
4613 buffer.advance(count);
4614 this.remaining -= count;
4615 std::task::Poll::Ready(Ok(()))
4616 }
4617 }
4618
4619 #[cfg(all(not(feature = "sync"), feature = "with-async-std"))]
4620 impl async_std::io::Read for MultipartTrackedReader {
4621 fn poll_read(
4622 self: std::pin::Pin<&mut Self>,
4623 _context: &mut std::task::Context<'_>,
4624 buffer: &mut [u8],
4625 ) -> std::task::Poll<std::io::Result<usize>> {
4626 use std::sync::atomic::Ordering;
4627 let this = self.get_mut();
4628 let count = this.remaining.min(buffer.len());
4629 let offset = this.bytes_read.fetch_add(count, Ordering::SeqCst);
4630 for (index, byte) in buffer[..count].iter_mut().enumerate() {
4631 *byte = ((offset + index) % 251) as u8;
4632 }
4633 this.remaining -= count;
4634 std::task::Poll::Ready(Ok(count))
4635 }
4636 }
4637
4638 #[cfg(all(
4639 not(feature = "sync"),
4640 any(feature = "with-tokio", feature = "with-async-std")
4641 ))]
4642 #[maybe_async::maybe_async]
4643 async fn run_multipart_builder_success_case(
4644 max_concurrent_chunks: usize,
4645 ) -> Result<(), String> {
4646 use std::io::{Read, Write};
4647 use std::net::{TcpListener, TcpStream};
4648 use std::sync::atomic::{AtomicUsize, Ordering};
4649 use std::sync::{Arc, mpsc};
4650 use std::thread;
4651 use std::time::{Duration, Instant};
4652
4653 const TAIL: usize = 17;
4654 let total = 3 * super::CHUNK_SIZE + TAIL;
4655 let bytes_read = Arc::new(AtomicUsize::new(0));
4656 let server_bytes_read = Arc::clone(&bytes_read);
4657 let listener = TcpListener::bind("127.0.0.1:0").map_err(|error| error.to_string())?;
4658 listener
4659 .set_nonblocking(true)
4660 .map_err(|error| error.to_string())?;
4661 let endpoint = format!(
4662 "http://{}",
4663 listener.local_addr().map_err(|error| error.to_string())?
4664 );
4665 let (done_tx, done_rx) = mpsc::channel::<String>();
4666 let server = thread::spawn(move || -> Result<(), String> {
4667 fn accept_until(
4668 listener: &TcpListener,
4669 done_rx: &mpsc::Receiver<String>,
4670 deadline: Instant,
4671 ) -> Result<TcpStream, String> {
4672 loop {
4673 match listener.accept() {
4674 Ok((stream, _)) => {
4675 stream.set_nonblocking(false).map_err(|e| e.to_string())?;
4676 stream
4677 .set_read_timeout(Some(Duration::from_secs(3)))
4678 .map_err(|e| e.to_string())?;
4679 stream
4680 .set_write_timeout(Some(Duration::from_secs(3)))
4681 .map_err(|e| e.to_string())?;
4682 return Ok(stream);
4683 }
4684 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
4685 if let Ok(result) = done_rx.try_recv() {
4686 return Err(format!(
4687 "client finished before expected request: {result}"
4688 ));
4689 }
4690 if Instant::now() >= deadline {
4691 return Err("multipart server accept deadline".to_owned());
4692 }
4693 thread::sleep(Duration::from_millis(2));
4694 }
4695 Err(error) => return Err(error.to_string()),
4696 }
4697 }
4698 }
4699 fn read_request(
4700 stream: &mut TcpStream,
4701 deadline: Instant,
4702 ) -> Result<(String, Vec<u8>), String> {
4703 let mut headers = Vec::new();
4704 let mut byte = [0];
4705 while !headers.ends_with(b"\r\n\r\n") {
4706 if headers.len() > 16 * 1024 || Instant::now() >= deadline {
4707 return Err("request header bound or deadline exceeded".to_owned());
4708 }
4709 stream.read_exact(&mut byte).map_err(|e| e.to_string())?;
4710 headers.push(byte[0]);
4711 }
4712 let text = String::from_utf8_lossy(&headers);
4713 let line = text.lines().next().unwrap_or_default().to_owned();
4714 let len = text
4715 .lines()
4716 .find_map(|line| {
4717 line.to_ascii_lowercase()
4718 .strip_prefix("content-length:")
4719 .map(str::to_owned)
4720 })
4721 .and_then(|v| v.trim().parse::<usize>().ok())
4722 .unwrap_or(0);
4723 if len > super::CHUNK_SIZE + 64 * 1024 {
4724 return Err("request body exceeded bound".to_owned());
4725 }
4726 let mut body = vec![0; len];
4727 stream.read_exact(&mut body).map_err(|e| e.to_string())?;
4728 Ok((line, body))
4729 }
4730 fn respond(
4731 stream: &mut TcpStream,
4732 status: u16,
4733 body: &str,
4734 etag: Option<&str>,
4735 ) -> Result<(), String> {
4736 let tag = etag
4737 .map(|value| format!("ETag: \"{value}\"\r\n"))
4738 .unwrap_or_default();
4739 write!(stream, "HTTP/1.1 {status} OK\r\nContent-Length: {}\r\n{tag}Connection: close\r\n\r\n{body}", body.len()).map_err(|e| e.to_string())
4740 }
4741
4742 let deadline = Instant::now() + Duration::from_secs(20);
4743 let mut init = accept_until(&listener, &done_rx, deadline)?;
4744 let (line, _) = read_request(&mut init, deadline)?;
4745 if !line.starts_with("POST ") || !line.contains("uploads") {
4746 return Err(format!("unexpected init: {line}"));
4747 }
4748 respond(
4749 &mut init,
4750 200,
4751 "<InitiateMultipartUploadResult><Bucket>test-bucket</Bucket><Key>multipart-test</Key><UploadId>upload-id</UploadId></InitiateMultipartUploadResult>",
4752 None,
4753 )?;
4754
4755 fn number(line: &str) -> Result<usize, String> {
4756 line.split("partNumber=")
4757 .nth(1)
4758 .and_then(|value| value.split('&').next())
4759 .and_then(|value| value.parse().ok())
4760 .ok_or_else(|| format!("missing part number in {line}"))
4761 }
4762 fn verify_part(number: usize, body: &[u8]) -> Result<(), String> {
4763 let expected_len = if number == 4 { TAIL } else { super::CHUNK_SIZE };
4764 if body.len() != expected_len {
4765 return Err(format!(
4766 "part {number} length {} != {expected_len}",
4767 body.len()
4768 ));
4769 }
4770 let offset = (number - 1) * super::CHUNK_SIZE;
4771 if body
4772 .iter()
4773 .enumerate()
4774 .any(|(index, byte)| *byte != ((offset + index) % 251) as u8)
4775 {
4776 return Err(format!("part {number} bytes did not match source offsets"));
4777 }
4778 Ok(())
4779 }
4780 let mut parts = Vec::new();
4781 for _ in 0..max_concurrent_chunks {
4782 let mut stream = accept_until(&listener, &done_rx, deadline)?;
4783 let (line, body) = read_request(&mut stream, deadline)?;
4784 let part_number = number(&line)?;
4785 if part_number > max_concurrent_chunks {
4786 return Err(format!(
4787 "part {part_number} arrived beyond cap {max_concurrent_chunks}"
4788 ));
4789 }
4790 verify_part(part_number, &body)?;
4791 let observed = server_bytes_read.load(Ordering::SeqCst);
4792 if observed > max_concurrent_chunks * super::CHUNK_SIZE {
4793 return Err(format!(
4794 "reader exceeded cap before response: {observed} bytes"
4795 ));
4796 }
4797 parts.push((part_number, stream));
4798 }
4799
4800 if max_concurrent_chunks == 2 {
4801 let index = parts
4802 .iter()
4803 .position(|(part, _)| *part == 2)
4804 .ok_or_else(|| "part 2 not received".to_owned())?;
4805 respond(&mut parts[index].1, 200, "", Some("etag-2"))?;
4806 } else {
4807 respond(&mut parts[0].1, 200, "", Some("etag-1"))?;
4808 parts.clear();
4809 }
4810
4811 let start = if max_concurrent_chunks == 2 { 3 } else { 2 };
4812 let mut acknowledged_bytes = super::CHUNK_SIZE;
4813 for part_number in start..=4 {
4814 let mut stream = accept_until(&listener, &done_rx, deadline)?;
4815 let (line, body) = read_request(&mut stream, deadline)?;
4816 let received_number = number(&line)?;
4817 if received_number != part_number {
4818 return Err(format!("expected part {part_number}, got {line}"));
4819 }
4820 verify_part(part_number, &body)?;
4821 let allowed = acknowledged_bytes + max_concurrent_chunks * super::CHUNK_SIZE;
4822 let observed = server_bytes_read.load(Ordering::SeqCst);
4823 if observed > allowed.min(total) {
4824 return Err(format!(
4825 "reader exceeded concurrency cap at part {part_number}: read {observed}, allowed {allowed}"
4826 ));
4827 }
4828 respond(&mut stream, 200, "", Some(&format!("etag-{part_number}")))?;
4829 acknowledged_bytes += body.len();
4830 }
4831 if max_concurrent_chunks == 2 {
4832 let index = parts
4834 .iter()
4835 .position(|(part, _)| *part == 1)
4836 .ok_or_else(|| "part 1 not received".to_owned())?;
4837 respond(&mut parts[index].1, 200, "", Some("etag-1"))?;
4838 }
4839
4840 let mut completion = accept_until(&listener, &done_rx, deadline)?;
4841 let (line, body) = read_request(&mut completion, deadline)?;
4842 if !line.contains("uploadId=upload-id") {
4843 return Err(format!("unexpected completion: {line}"));
4844 }
4845 let xml = String::from_utf8(body).map_err(|e| e.to_string())?;
4846 let expected_blocks = (1..=4)
4847 .map(|part| {
4848 format!(
4849 "<Part><PartNumber>{part}</PartNumber><ETag>\"etag-{part}\"</ETag></Part>"
4850 )
4851 })
4852 .collect::<String>();
4853 if !xml.contains(&expected_blocks) {
4854 return Err(format!("completion parts were not ordered: {xml}"));
4855 }
4856 respond(
4857 &mut completion,
4858 200,
4859 "<CompleteMultipartUploadResult/>",
4860 None,
4861 )?;
4862 Ok(())
4863 });
4864
4865 let credentials = Credentials::new(Some("access"), Some("secret"), None, None, None)
4866 .map_err(|e| e.to_string())?;
4867 let bucket = Bucket::new(
4868 "test-bucket",
4869 Region::Custom {
4870 region: "us-east-1".to_owned(),
4871 endpoint,
4872 },
4873 credentials,
4874 )
4875 .map_err(|e| e.to_string())?
4876 .with_path_style()
4877 .with_request_timeout(Duration::from_secs(5))
4878 .map_err(|e| e.to_string())?;
4879 let mut reader = MultipartTrackedReader {
4880 remaining: total,
4881 bytes_read,
4882 };
4883 let result = bucket
4884 .put_object_stream_builder("/multipart-test")
4885 .with_max_concurrent_chunks(max_concurrent_chunks)
4886 .execute_stream(&mut reader)
4887 .await;
4888 let _ = done_tx.send(format!("{result:?}"));
4889 let server_result = server
4890 .join()
4891 .map_err(|_| "multipart server panicked".to_owned())?;
4892 server_result?;
4893 let response = result.map_err(|error| format!("upload failed: {error:?}"))?;
4894 if response.uploaded_bytes() != total {
4895 return Err(format!(
4896 "uploaded byte count {} != {total}",
4897 response.uploaded_bytes()
4898 ));
4899 }
4900 Ok(())
4901 }
4902
4903 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
4904 #[tokio::test]
4905 async fn multipart_stream_builder_routes_headers_by_operation() {
4906 assert_multipart_builder_header_routing().await;
4907 }
4908
4909 #[cfg(all(
4910 not(feature = "sync"),
4911 not(feature = "with-tokio"),
4912 feature = "with-async-std"
4913 ))]
4914 #[async_std::test]
4915 async fn multipart_stream_builder_routes_headers_by_operation() {
4916 assert_multipart_builder_header_routing().await;
4917 }
4918
4919 #[allow(dead_code)]
4920 #[derive(Clone, Copy, Debug)]
4921 enum MultipartFailureCase {
4922 Reader,
4923 Part,
4924 Completion,
4925 CompletionEmbeddedError,
4926 Abort,
4927 SmallPut,
4928 }
4929
4930 struct MultipartFaultReader {
4931 remaining: usize,
4932 fail_at_eof: bool,
4933 }
4934
4935 impl MultipartFaultReader {
4936 fn read_into(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
4937 if self.remaining == 0 {
4938 return if self.fail_at_eof {
4939 Err(std::io::Error::other("injected reader failure"))
4940 } else {
4941 Ok(0)
4942 };
4943 }
4944 let read = self.remaining.min(buffer.len());
4945 buffer[..read].fill(b'x');
4946 self.remaining -= read;
4947 Ok(read)
4948 }
4949 }
4950
4951 impl std::io::Read for MultipartFaultReader {
4952 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
4953 self.read_into(buffer)
4954 }
4955 }
4956
4957 #[cfg(feature = "with-tokio")]
4958 impl tokio::io::AsyncRead for MultipartFaultReader {
4959 fn poll_read(
4960 self: std::pin::Pin<&mut Self>,
4961 _context: &mut std::task::Context<'_>,
4962 buffer: &mut tokio::io::ReadBuf<'_>,
4963 ) -> std::task::Poll<std::io::Result<()>> {
4964 let this = self.get_mut();
4965 match this.read_into(buffer.initialize_unfilled()) {
4966 Ok(read) => {
4967 buffer.advance(read);
4968 std::task::Poll::Ready(Ok(()))
4969 }
4970 Err(error) => std::task::Poll::Ready(Err(error)),
4971 }
4972 }
4973 }
4974
4975 #[cfg(feature = "with-async-std")]
4976 impl async_std::io::Read for MultipartFaultReader {
4977 fn poll_read(
4978 self: std::pin::Pin<&mut Self>,
4979 _context: &mut std::task::Context<'_>,
4980 buffer: &mut [u8],
4981 ) -> std::task::Poll<std::io::Result<usize>> {
4982 std::task::Poll::Ready(self.get_mut().read_into(buffer))
4983 }
4984 }
4985
4986 #[maybe_async::maybe_async]
4987 async fn run_multipart_failure_case(
4988 case: MultipartFailureCase,
4989 max_concurrent_chunks: Option<usize>,
4990 ) -> (Result<u16, S3Error>, Result<Vec<String>, String>) {
4991 use super::CHUNK_SIZE;
4992 use std::io::{Read, Write};
4993 use std::net::TcpListener;
4994 use std::sync::mpsc;
4995 use std::thread;
4996 use std::time::{Duration, Instant};
4997
4998 fn read_request(
4999 stream: &mut std::net::TcpStream,
5000 request_index: usize,
5001 deadline: Instant,
5002 ) -> Result<(String, usize), String> {
5003 let mut header_bytes = Vec::new();
5004 let mut byte = [0u8; 1];
5005 while !header_bytes.ends_with(b"\r\n\r\n") {
5006 if header_bytes.len() >= 16 * 1024 || Instant::now() >= deadline {
5007 return Err(format!("header bounds exceeded at request {request_index}"));
5008 }
5009 stream
5010 .read_exact(&mut byte)
5011 .map_err(|error| format!("read headers at {request_index}: {error}"))?;
5012 header_bytes.push(byte[0]);
5013 }
5014 let header_text = String::from_utf8_lossy(&header_bytes);
5015 let request_line = header_text.lines().next().unwrap_or_default().to_owned();
5016 let content_length = header_text
5017 .lines()
5018 .find_map(|line| {
5019 line.to_ascii_lowercase()
5020 .strip_prefix("content-length:")
5021 .map(str::to_owned)
5022 })
5023 .and_then(|value| value.trim().parse::<usize>().ok())
5024 .unwrap_or(0);
5025 if content_length > CHUNK_SIZE + 64 * 1024 {
5026 return Err(format!("request body exceeded cap at {request_index}"));
5027 }
5028 let mut request_body = vec![0u8; content_length];
5029 stream
5030 .read_exact(&mut request_body)
5031 .map_err(|error| format!("read body at {request_index}: {error}"))?;
5032 Ok((request_line, content_length))
5033 }
5034
5035 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5036 listener.set_nonblocking(true).unwrap();
5037 let endpoint = format!("http://{}", listener.local_addr().unwrap());
5038 let (done_tx, done_rx) = mpsc::channel::<String>();
5039 let server = thread::spawn(move || {
5040 let init_body = "<InitiateMultipartUploadResult><Bucket>test-bucket</Bucket><Key>multipart-test</Key><UploadId>upload-id</UploadId></InitiateMultipartUploadResult>";
5041 let part_failure = "part failure";
5042 let completion_failure = "completion failure";
5043 let completion_embedded_error =
5044 "<Error><Code>InternalError</Code><RequestId>completion-req</RequestId></Error>";
5045 let abort_failure = "abort failure";
5046 let put_failure = "fallback put failure";
5047 let failure_attempts = if cfg!(feature = "fail-on-err") {
5048 crate::get_retries() as usize + 1
5049 } else {
5050 1
5051 };
5052 let mut steps = vec![("POST ", "uploads", 200, init_body, false)];
5053 match case {
5054 MultipartFailureCase::Reader | MultipartFailureCase::Abort => {
5055 if cfg!(feature = "sync") || max_concurrent_chunks == Some(1) {
5056 steps.push(("PUT ", "partNumber=1", 200, "", true));
5057 }
5058 let (status, body) = if matches!(case, MultipartFailureCase::Abort) {
5059 (500, abort_failure)
5060 } else {
5061 (204, "")
5062 };
5063 steps.push(("DELETE ", "uploadId=upload-id", status, body, false));
5065 }
5066 MultipartFailureCase::SmallPut => {
5067 steps.push(("DELETE ", "uploadId=upload-id", 204, "", false));
5068 steps.push(("PUT ", "multipart-test", 500, put_failure, false));
5070 }
5071 MultipartFailureCase::Part => {
5072 for _ in 0..failure_attempts {
5073 steps.push(("PUT ", "partNumber=1", 500, part_failure, false));
5074 }
5075 steps.push(("DELETE ", "uploadId=upload-id", 204, "", false));
5076 }
5077 MultipartFailureCase::Completion
5078 | MultipartFailureCase::CompletionEmbeddedError => {
5079 steps.push(("PUT ", "partNumber=1", 200, "", true));
5080 if cfg!(feature = "sync") {
5081 steps.push(("PUT ", "partNumber=2", 200, "", true));
5082 }
5083 let (status, body) = if matches!(case, MultipartFailureCase::Completion) {
5084 (500, completion_failure)
5085 } else {
5086 (200, completion_embedded_error)
5087 };
5088 steps.push(("POST ", "uploadId=upload-id", status, body, false));
5090 steps.push(("DELETE ", "uploadId=upload-id", 204, "", false));
5091 }
5092 }
5093 let deadline = Instant::now() + Duration::from_secs(10);
5094 let mut requests = Vec::new();
5095
5096 for (step_index, (expected_method, expected_query, status, body, with_etag)) in
5097 steps.into_iter().enumerate()
5098 {
5099 let (mut stream, _) = loop {
5100 match listener.accept() {
5101 Ok(pair) => break pair,
5102 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5103 if let Ok(client_result) = done_rx.try_recv() {
5104 return Err(format!(
5105 "case {case:?}: client completed before request {} (expected {expected_method}...{expected_query}) after {} requests; client result: {client_result}",
5106 step_index + 1,
5107 requests.len(),
5108 ));
5109 }
5110 if Instant::now() >= deadline {
5111 return Err(format!(
5112 "case {case:?}: accept deadline after {} requests; awaiting request {} expected {expected_method}...{expected_query}",
5113 requests.len(),
5114 step_index + 1,
5115 ));
5116 }
5117 thread::sleep(Duration::from_millis(5));
5118 }
5119 Err(error) => {
5120 return Err(format!(
5121 "case {case:?}: accept failed before request {} expected {expected_method}...{expected_query}: {error}",
5122 step_index + 1,
5123 ));
5124 }
5125 }
5126 };
5127 stream
5128 .set_nonblocking(false)
5129 .map_err(|error| format!("set blocking: {error}"))?;
5130 stream
5131 .set_read_timeout(Some(Duration::from_secs(3)))
5132 .map_err(|error| format!("set read timeout: {error}"))?;
5133 stream
5134 .set_write_timeout(Some(Duration::from_secs(3)))
5135 .map_err(|error| format!("set write timeout: {error}"))?;
5136 let (request_line, _) = read_request(&mut stream, requests.len() + 1, deadline)
5137 .map_err(|error| {
5138 format!(
5139 "case {case:?}: reading request {} expected {expected_method}...{expected_query}: {error}",
5140 step_index + 1,
5141 )
5142 })?;
5143 if !request_line.starts_with(expected_method)
5144 || !request_line.contains(expected_query)
5145 {
5146 return Err(format!(
5147 "case {case:?}: unexpected request {} while expecting {expected_method}...{expected_query}: received {request_line:?}",
5148 step_index + 1,
5149 ));
5150 }
5151 let reason = if (200..300).contains(&status) {
5152 "OK"
5153 } else {
5154 "Internal Server Error"
5155 };
5156 let etag = if with_etag {
5157 "ETag: \"part-etag\"\r\n"
5158 } else {
5159 ""
5160 };
5161 let response = format!(
5162 "HTTP/1.1 {status} {reason}\r\nContent-Length: {}\r\n{etag}Connection: close\r\n\r\n{body}",
5163 body.len()
5164 );
5165 stream.write_all(response.as_bytes()).map_err(|error| {
5166 format!(
5167 "case {case:?}: writing response for request {} expected {expected_method}...{expected_query}: {error}",
5168 step_index + 1,
5169 )
5170 })?;
5171 requests.push(request_line);
5172 }
5173
5174 loop {
5177 match listener.accept() {
5178 Ok((mut stream, _)) => {
5179 stream
5180 .set_nonblocking(false)
5181 .map_err(|error| format!("set blocking for extra request: {error}"))?;
5182 stream
5183 .set_read_timeout(Some(Duration::from_secs(3)))
5184 .map_err(|error| format!("set extra read timeout: {error}"))?;
5185 stream
5186 .set_write_timeout(Some(Duration::from_secs(3)))
5187 .map_err(|error| format!("set extra write timeout: {error}"))?;
5188 let (request_line, _) = read_request(
5189 &mut stream,
5190 requests.len() + 1,
5191 deadline,
5192 )
5193 .map_err(|error| {
5194 format!(
5195 "case {case:?}: reading unexpected request after {} expected requests: {error}",
5196 requests.len(),
5197 )
5198 })?;
5199 requests.push(format!("unexpected extra request: {request_line}"));
5200 stream
5201 .write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
5202 .map_err(|error| format!("write extra response: {error}"))?;
5203 }
5204 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5205 if done_rx.try_recv().is_ok() {
5206 return Ok(requests);
5207 }
5208 if Instant::now() >= deadline {
5209 return Err(format!(
5210 "case {case:?}: post-request deadline after {} requests while awaiting client completion",
5211 requests.len()
5212 ));
5213 }
5214 thread::sleep(Duration::from_millis(5));
5215 }
5216 Err(error) => {
5217 return Err(format!(
5218 "case {case:?}: accept unexpected request after {} expected requests failed: {error}",
5219 requests.len(),
5220 ));
5221 }
5222 }
5223 }
5224 });
5225
5226 let credentials = Credentials::new(
5227 Some("test_access_key"),
5228 Some("test_secret_key"),
5229 None,
5230 None,
5231 None,
5232 )
5233 .unwrap();
5234 let bucket = Bucket::new(
5235 "test-bucket",
5236 Region::Custom {
5237 region: "us-east-1".to_owned(),
5238 endpoint,
5239 },
5240 credentials,
5241 )
5242 .unwrap()
5243 .with_path_style()
5244 .with_request_timeout(Duration::from_secs(4))
5245 .unwrap();
5246 let mut reader = MultipartFaultReader {
5247 remaining: if matches!(case, MultipartFailureCase::SmallPut) {
5248 1
5249 } else {
5250 CHUNK_SIZE
5251 },
5252 fail_at_eof: matches!(
5253 case,
5254 MultipartFailureCase::Reader | MultipartFailureCase::Abort
5255 ),
5256 };
5257 #[cfg(feature = "sync")]
5258 let _ = max_concurrent_chunks;
5259 #[cfg(feature = "sync")]
5260 let result = bucket
5261 .put_object_stream_with_content_type(
5262 &mut reader,
5263 "/multipart-test",
5264 "application/octet-stream",
5265 )
5266 .await;
5267 #[cfg(not(feature = "sync"))]
5268 let result = if let Some(limit) = max_concurrent_chunks {
5269 bucket
5270 .put_object_stream_builder("/multipart-test")
5271 .with_content_type("application/octet-stream")
5272 .with_max_concurrent_chunks(limit)
5273 .execute_stream(&mut reader)
5274 .await
5275 } else {
5276 bucket
5277 .put_object_stream_with_content_type(
5278 &mut reader,
5279 "/multipart-test",
5280 "application/octet-stream",
5281 )
5282 .await
5283 };
5284 #[cfg(not(feature = "sync"))]
5285 let result = result.map(|response| response.status_code());
5286 let _ = done_tx.send(format!("{result:?}"));
5287 let server_result = server.join().expect("mock multipart server panicked");
5288 (result, server_result)
5289 }
5290
5291 #[maybe_async::test(
5292 feature = "sync",
5293 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5294 async(
5295 all(not(feature = "sync"), feature = "with-async-std"),
5296 async_std::test
5297 )
5298 )]
5299 async fn multipart_stream_errors_attempt_abort_and_preserve_primary_error() {
5300 let failure_attempts = if cfg!(feature = "fail-on-err") {
5301 crate::get_retries() as usize + 1
5302 } else {
5303 1
5304 };
5305 for case in [
5306 MultipartFailureCase::Reader,
5307 MultipartFailureCase::Part,
5308 MultipartFailureCase::Completion,
5309 MultipartFailureCase::CompletionEmbeddedError,
5310 MultipartFailureCase::Abort,
5311 #[cfg(feature = "sync")]
5312 MultipartFailureCase::SmallPut,
5313 ] {
5314 let (result, requests) = run_multipart_failure_case(case, None).await;
5315 let requests = requests.unwrap_or_else(|server_error| {
5316 panic!(
5317 "{case:?}: mock server request sequence failed; client result: {result:?}; {server_error}"
5318 )
5319 });
5320 let expected_requests = match case {
5321 MultipartFailureCase::Reader => 2 + usize::from(cfg!(feature = "sync")),
5322 MultipartFailureCase::Part => 2 + failure_attempts,
5323 MultipartFailureCase::Completion => 4 + usize::from(cfg!(feature = "sync")),
5324 MultipartFailureCase::CompletionEmbeddedError => {
5325 4 + usize::from(cfg!(feature = "sync"))
5326 }
5327 MultipartFailureCase::Abort => 2 + usize::from(cfg!(feature = "sync")),
5328 MultipartFailureCase::SmallPut => 3,
5329 };
5330 assert_eq!(requests.len(), expected_requests);
5331 if !matches!(case, MultipartFailureCase::SmallPut) {
5332 assert!(result.is_err(), "{case:?} should return an error");
5333 }
5334
5335 match case {
5336 MultipartFailureCase::Reader | MultipartFailureCase::Abort => {
5337 assert!(
5338 matches!(result, Err(S3Error::Io(error)) if error.to_string() == "injected reader failure")
5339 );
5340 }
5341 MultipartFailureCase::Part => {
5342 assert!(matches!(
5343 result,
5344 Err(S3Error::HttpFail | S3Error::HttpFailWithBody(500, _))
5345 ));
5346 }
5347 MultipartFailureCase::Completion => match result {
5348 Err(S3Error::HttpFailWithBody(500, body)) => {
5349 assert_eq!(body, "completion failure");
5350 }
5351 Err(S3Error::HttpFail) if cfg!(feature = "fail-on-err") => {}
5352 other => panic!("completion failure was not preserved: {other:?}"),
5353 },
5354 MultipartFailureCase::CompletionEmbeddedError => {
5355 assert!(
5356 matches!(result, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("completion-req"))
5357 );
5358 }
5359 MultipartFailureCase::SmallPut => {
5360 if cfg!(feature = "fail-on-err") {
5361 assert!(matches!(
5362 result,
5363 Err(S3Error::HttpFail | S3Error::HttpFailWithBody(500, _))
5364 ));
5365 } else {
5366 assert!(matches!(result, Ok(500)));
5367 }
5368 }
5369 }
5370 }
5371 }
5372
5373 #[cfg(all(
5374 not(feature = "sync"),
5375 any(feature = "with-tokio", feature = "with-async-std")
5376 ))]
5377 #[maybe_async::test(
5378 feature = "sync",
5379 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5380 async(
5381 all(not(feature = "sync"), feature = "with-async-std"),
5382 async_std::test
5383 )
5384 )]
5385 async fn multipart_stream_builder_limits_preserve_abort_errors() {
5386 let failure_attempts = if cfg!(feature = "fail-on-err") {
5387 crate::get_retries() as usize + 1
5388 } else {
5389 1
5390 };
5391 for (limit, case, expected_requests) in [
5392 (1, MultipartFailureCase::Reader, 3),
5393 (1, MultipartFailureCase::Abort, 3),
5394 (1, MultipartFailureCase::Part, 2 + failure_attempts),
5395 (1, MultipartFailureCase::Completion, 4),
5396 (1, MultipartFailureCase::CompletionEmbeddedError, 4),
5397 (2, MultipartFailureCase::Reader, 2),
5398 (2, MultipartFailureCase::Abort, 2),
5399 (2, MultipartFailureCase::Part, 2 + failure_attempts),
5400 (2, MultipartFailureCase::Completion, 4),
5401 (2, MultipartFailureCase::CompletionEmbeddedError, 4),
5402 ] {
5403 let (result, requests) = run_multipart_failure_case(case, Some(limit)).await;
5404 let requests = requests.unwrap_or_else(|server_error| {
5405 panic!("limit {limit} {case:?}: client result {result:?}; {server_error}")
5406 });
5407 assert_eq!(requests.len(), expected_requests, "limit {limit} {case:?}");
5408 assert!(result.is_err(), "limit {limit} {case:?} should fail");
5409 match case {
5410 MultipartFailureCase::Reader | MultipartFailureCase::Abort => assert!(
5411 matches!(result, Err(S3Error::Io(error)) if error.to_string() == "injected reader failure")
5412 ),
5413 MultipartFailureCase::Part => assert!(matches!(
5414 result,
5415 Err(S3Error::HttpFail | S3Error::HttpFailWithBody(500, _))
5416 )),
5417 MultipartFailureCase::Completion => match result {
5418 Err(S3Error::HttpFailWithBody(500, body)) => {
5419 assert_eq!(body, "completion failure")
5420 }
5421 Err(S3Error::HttpFail) if cfg!(feature = "fail-on-err") => {}
5422 other => panic!("limit {limit}: completion error was not preserved: {other:?}"),
5423 },
5424 MultipartFailureCase::CompletionEmbeddedError => assert!(
5425 matches!(result, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("completion-req"))
5426 ),
5427 MultipartFailureCase::SmallPut => unreachable!(),
5428 }
5429 }
5430 }
5431
5432 #[cfg(all(
5433 not(feature = "sync"),
5434 any(feature = "with-tokio", feature = "with-async-std")
5435 ))]
5436 #[maybe_async::test(
5437 feature = "sync",
5438 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5439 async(
5440 all(not(feature = "sync"), feature = "with-async-std"),
5441 async_std::test
5442 )
5443 )]
5444 async fn multipart_stream_builder_limits_bound_and_order_parts() {
5445 for limit in [1, 2] {
5446 run_multipart_builder_success_case(limit)
5447 .await
5448 .unwrap_or_else(|error| panic!("limit {limit}: {error}"));
5449 }
5450 }
5451
5452 #[test]
5453 fn accepted_mock_stream_blocks_until_delayed_client_byte() {
5454 use std::io::{Read, Write};
5455 use std::net::{TcpListener, TcpStream};
5456 use std::sync::mpsc;
5457 use std::thread;
5458 use std::time::{Duration, Instant};
5459
5460 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5461 listener.set_nonblocking(true).unwrap();
5462 let address = listener.local_addr().unwrap();
5463 let (accepted_tx, accepted_rx) = mpsc::sync_channel(0);
5464 let (read_result_tx, read_result_rx) = mpsc::channel();
5465 let server = thread::spawn(move || {
5466 let deadline = Instant::now() + Duration::from_secs(2);
5467 let (stream, _) = loop {
5468 match listener.accept() {
5469 Ok(pair) => break pair,
5470 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5471 assert!(Instant::now() < deadline, "mock accept timed out");
5472 thread::sleep(Duration::from_millis(2));
5473 }
5474 Err(error) => panic!("mock accept failed: {error}"),
5475 }
5476 };
5477 let mut stream = make_mock_stream_blocking(stream).unwrap();
5478 stream
5479 .set_read_timeout(Some(Duration::from_secs(1)))
5480 .unwrap();
5481 accepted_tx.send(()).unwrap();
5482 let mut byte = [0];
5483 read_result_tx
5484 .send(stream.read_exact(&mut byte).map(|()| byte[0]))
5485 .unwrap();
5486 });
5487
5488 let mut client = TcpStream::connect(address).unwrap();
5489 accepted_rx
5490 .recv_timeout(Duration::from_secs(1))
5491 .expect("server did not accept the client");
5492 assert!(matches!(
5493 read_result_rx.recv_timeout(Duration::from_millis(50)),
5494 Err(mpsc::RecvTimeoutError::Timeout)
5495 ));
5496 client.write_all(b"x").unwrap();
5497 assert_eq!(
5498 read_result_rx
5499 .recv_timeout(Duration::from_secs(1))
5500 .expect("server did not finish reading")
5501 .unwrap(),
5502 b'x'
5503 );
5504 server.join().unwrap();
5505 }
5506
5507 #[test]
5508 fn xml_response_embedded_error_rejects_invalid_or_unexpected_documents() {
5509 for body in [
5510 "",
5511 "<CopyObjectResult>",
5512 "<CopyObjectResult></CopyObjectResult><Extra/>",
5513 "<CopyObjectResult></CopyObjectResult>trailing",
5514 "<CopyObjectResult/>&",
5515 "<CopyObjectResult><A></CopyObjectResult>",
5516 "<CopyObjectResult A=\"unterminated></CopyObjectResult>",
5517 "<CopyObjectResult>&unknown;</CopyObjectResult>",
5518 "<?xml version=\"1.0\"?><?xml version=\"1.0\"?><CopyObjectResult/>",
5519 "<?xml bogus?><CopyObjectResult/>",
5520 "<?xml version=\"1.0\"?><CopyObjectResult><?xml version=\"1.0\"?></CopyObjectResult>",
5521 "<CopyObjectResult><!-- invalid -- comment --></CopyObjectResult>",
5522 ] {
5523 assert!(
5524 matches!(
5525 validate_success_xml_response(xml_response(body), "CopyObjectResult"),
5526 Err(S3Error::SerdeXml(_))
5527 ),
5528 "accepted invalid XML: {body:?}"
5529 );
5530 }
5531 assert!(matches!(
5532 validate_success_xml_response(xml_response("<Other/>"), "CopyObjectResult"),
5533 Err(S3Error::HttpFailWithBody(200, _))
5534 ));
5535 }
5536
5537 #[test]
5538 fn xml_response_embedded_error_accepts_xml_11_text_and_decoded_attributes() {
5539 let body = "<?xml version=\"1.1\"?>\n<CopyObjectResult note=\"café&tea\">first\r\u{0085}second</CopyObjectResult>\n";
5540 assert!(validate_success_xml_response(xml_response(body), "CopyObjectResult").is_ok());
5541 }
5542
5543 #[test]
5544 fn bucket_debug_redacts_nested_credentials() {
5545 let credentials = Credentials::new(
5546 Some("BUCKET_ACCESS_KEY_SENTINEL"),
5547 Some("BUCKET_SECRET_KEY_SENTINEL"),
5548 Some("BUCKET_SECURITY_TOKEN_SENTINEL"),
5549 Some("BUCKET_SESSION_TOKEN_SENTINEL"),
5550 None,
5551 )
5552 .unwrap();
5553 let bucket = Bucket::new(
5554 "debug-test-bucket",
5555 "us-east-1".parse::<Region>().unwrap(),
5556 credentials,
5557 )
5558 .unwrap();
5559
5560 for rendered in [format!("{bucket:?}"), format!("{bucket:#?}")] {
5561 for sentinel in [
5562 "BUCKET_ACCESS_KEY_SENTINEL",
5563 "BUCKET_SECRET_KEY_SENTINEL",
5564 "BUCKET_SECURITY_TOKEN_SENTINEL",
5565 "BUCKET_SESSION_TOKEN_SENTINEL",
5566 ] {
5567 assert!(
5568 !rendered.contains(sentinel),
5569 "bucket debug output leaked a credential"
5570 );
5571 }
5572 }
5573 }
5574
5575 #[test]
5576 fn xml_response_embedded_error_preserves_success_framing_and_service_error_body() {
5577 let success = xml_response(
5578 "<s:CopyObjectResult xmlns:s=\"urn:s3\"><s:ETag>\"abc\"</s:ETag><Extra/></s:CopyObjectResult>",
5579 );
5580 assert!(validate_success_xml_response(success, "CopyObjectResult").is_ok());
5581
5582 let error_body = "<Error><Code>SlowDown</Code><RequestId>req-123</RequestId></Error>";
5583 assert!(matches!(
5584 validate_success_xml_response(xml_response(error_body), "CopyObjectResult"),
5585 Err(S3Error::HttpFailWithBody(200, body)) if body == error_body
5586 ));
5587
5588 let non_success =
5589 ResponseData::new(bytes::Bytes::from_static(b"not xml"), 404, HashMap::new());
5590 assert_eq!(
5591 validate_success_xml_response(non_success, "CopyObjectResult")
5592 .unwrap()
5593 .status_code(),
5594 404
5595 );
5596 }
5597
5598 #[maybe_async::test(
5599 feature = "sync",
5600 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5601 async(
5602 all(not(feature = "sync"), feature = "with-async-std"),
5603 async_std::test
5604 )
5605 )]
5606 async fn xml_response_embedded_error_is_checked_by_copy_and_multipart_operations() {
5607 use std::io::{Read, Write};
5608 use std::net::TcpListener;
5609 use std::thread;
5610 use std::time::{Duration, Instant};
5611
5612 fn server_error(
5613 stage: &str,
5614 completed_requests: usize,
5615 error: &dyn std::fmt::Display,
5616 ) -> String {
5617 format!(
5618 "stage={stage} completed_requests={completed_requests} request_index={} error={error}",
5619 completed_requests + 1
5620 )
5621 }
5622
5623 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5624 listener.set_nonblocking(true).unwrap();
5625 let endpoint = format!("http://{}", listener.local_addr().unwrap());
5626 let server = thread::spawn(move || {
5627 let responses = [
5628 "<CopyObjectResult><ETag>\"copy-etag\"</ETag></CopyObjectResult>",
5629 " \n<?xml version=\"1.0\"?>\n<CompleteMultipartUploadResult> \n<ETag>\"multipart-etag\"</ETag></CompleteMultipartUploadResult> \n",
5630 "<Error><Code>SlowDown</Code><RequestId>copy-req</RequestId></Error>",
5631 " \n<?xml version=\"1.0\"?>\n<Error><Code>InternalError</Code><RequestId>multipart-req</RequestId></Error> \n",
5632 "<CopyObjectResult>",
5633 ];
5634 let mut count = 0;
5635 while count < responses.len() {
5636 let request_deadline = Instant::now() + Duration::from_secs(8);
5640 let (stream, _) = loop {
5641 match listener.accept() {
5642 Ok(pair) => break pair,
5643 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5644 if Instant::now() < request_deadline {
5645 thread::sleep(Duration::from_millis(5));
5646 continue;
5647 }
5648 return Err(format!(
5649 "stage=accept_deadline completed_requests={count} expected_requests={} deadline_elapsed=true",
5650 responses.len()
5651 ));
5652 }
5653 Err(error) => {
5654 return Err(format!(
5655 "stage=accept completed_requests={count} next_request={} error_kind={:?} error={error}",
5656 count + 1,
5657 error.kind()
5658 ));
5659 }
5660 }
5661 };
5662 let mut request = Vec::new();
5663 let mut stream = make_mock_stream_blocking(stream)
5664 .map_err(|error| server_error("set_blocking", count, &error))?;
5665 stream
5666 .set_read_timeout(Some(Duration::from_secs(2)))
5667 .map_err(|error| server_error("set_read_timeout", count, &error))?;
5668 stream
5669 .set_write_timeout(Some(Duration::from_secs(2)))
5670 .map_err(|error| server_error("set_write_timeout", count, &error))?;
5671 let mut byte = [0u8; 1];
5672 while !request.ends_with(b"\r\n\r\n") {
5673 if request.len() >= 16 * 1024 || Instant::now() >= request_deadline {
5674 return Err(server_error(
5675 "read_headers_bounds",
5676 count,
5677 &"request headers exceeded bounds",
5678 ));
5679 }
5680 stream
5681 .read_exact(&mut byte)
5682 .map_err(|error| server_error("read_headers", count, &error))?;
5683 request.push(byte[0]);
5684 }
5685 let headers = String::from_utf8_lossy(&request).to_ascii_lowercase();
5686 let content_length = headers
5687 .lines()
5688 .find_map(|line| line.strip_prefix("content-length:"))
5689 .and_then(|value| value.trim().parse::<usize>().ok())
5690 .unwrap_or(0);
5691 let content_length_value = content_length;
5692 if content_length_value > 64 * 1024 {
5693 return Err(server_error(
5694 "validate_content_length",
5695 count,
5696 &"request body exceeded mock server limit",
5697 ));
5698 }
5699 let mut body = vec![0; content_length_value];
5700 stream
5701 .read_exact(&mut body)
5702 .map_err(|error| server_error("read_body", count, &error))?;
5703 let response_body = responses[count];
5704 let response = format!(
5705 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
5706 response_body.len(),
5707 response_body
5708 );
5709 stream
5710 .write_all(response.as_bytes())
5711 .map_err(|error| server_error("write_response", count, &error))?;
5712 count += 1;
5713 }
5714 Ok(count)
5715 });
5716
5717 let credentials = Credentials::new(
5718 Some("test_access_key"),
5719 Some("test_secret_key"),
5720 None,
5721 None,
5722 None,
5723 )
5724 .unwrap();
5725 let bucket = Bucket::new(
5726 "test-bucket",
5727 Region::Custom {
5728 region: "us-east-1".to_owned(),
5729 endpoint,
5730 },
5731 credentials,
5732 )
5733 .unwrap()
5734 .with_path_style()
5735 .with_request_timeout(Duration::from_secs(4))
5736 .unwrap();
5737
5738 let copy_ok = bucket.copy_object_internal("/source", "/destination").await;
5739 let multipart_ok = bucket
5740 .complete_multipart_upload(
5741 "/destination",
5742 "upload-id",
5743 vec![Part {
5744 etag: "\"part-etag\"".to_owned(),
5745 part_number: 1,
5746 }],
5747 )
5748 .await;
5749 let copy_error = bucket.copy_object_internal("/source", "/destination").await;
5750 let multipart_error = bucket
5751 .complete_multipart_upload(
5752 "/destination",
5753 "upload-id",
5754 vec![Part {
5755 etag: "\"part-etag\"".to_owned(),
5756 part_number: 1,
5757 }],
5758 )
5759 .await;
5760 let malformed_copy = bucket.copy_object_internal("/source", "/destination").await;
5761
5762 let server_result = server.join().expect("mock server panicked");
5763 assert_eq!(server_result.unwrap(), 5);
5764 assert_eq!(copy_ok.unwrap(), 200);
5765 assert_eq!(multipart_ok.unwrap().status_code(), 200);
5766 assert!(
5767 matches!(copy_error, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("SlowDown") && body.contains("copy-req"))
5768 );
5769 assert!(
5770 matches!(multipart_error, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("InternalError") && body.contains("multipart-req"))
5771 );
5772 assert!(matches!(malformed_copy, Err(S3Error::SerdeXml(_))));
5773 }
5774
5775 #[cfg(all(
5776 not(feature = "sync"),
5777 not(feature = "with-tokio"),
5778 feature = "with-async-std"
5779 ))]
5780 #[async_std::test]
5781 async fn copy_object_opens_new_connection_after_server_close() {
5782 use std::io::{Read, Write};
5783 use std::net::TcpListener;
5784 use std::thread;
5785 use std::time::{Duration, Instant};
5786
5787 fn read_request(
5788 stream: &mut std::net::TcpStream,
5789 deadline: Instant,
5790 ) -> Result<String, String> {
5791 let mut request = Vec::new();
5792 let mut byte = [0u8; 1];
5793 while !request.ends_with(b"\r\n\r\n") {
5794 let now = Instant::now();
5795 if now >= deadline {
5796 return Err("deadline reading request headers".to_owned());
5797 }
5798 stream
5799 .set_read_timeout(Some((deadline - now).min(Duration::from_millis(100))))
5800 .map_err(|error| format!("set request read timeout: {error}"))?;
5801 if request.len() >= 16 * 1024 {
5802 return Err("request headers exceeded test limit".to_owned());
5803 }
5804 match stream.read_exact(&mut byte) {
5805 Ok(()) => {}
5806 Err(error)
5807 if matches!(
5808 error.kind(),
5809 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
5810 ) && Instant::now() < deadline =>
5811 {
5812 continue;
5813 }
5814 Err(error) => return Err(format!("read request headers: {error}")),
5815 }
5816 request.push(byte[0]);
5817 }
5818 Ok(String::from_utf8_lossy(&request).into_owned())
5819 }
5820
5821 fn write_response(stream: &mut std::net::TcpStream) -> Result<(), String> {
5822 let body = "<CopyObjectResult><ETag>\"copy-etag\"</ETag></CopyObjectResult>";
5823 let response = format!(
5824 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
5825 body.len()
5826 );
5827 stream
5828 .write_all(response.as_bytes())
5829 .map_err(|error| format!("write close response: {error}"))
5830 }
5831
5832 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5833 listener.set_nonblocking(true).unwrap();
5834 let endpoint = format!("http://{}", listener.local_addr().unwrap());
5835 let server = thread::spawn(move || -> Result<String, String> {
5836 let deadline = Instant::now() + Duration::from_secs(6);
5837 let (mut first, _) = loop {
5838 match listener.accept() {
5839 Ok(pair) => break pair,
5840 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5841 if Instant::now() >= deadline {
5842 return Err("deadline waiting for first copy request".to_owned());
5843 }
5844 thread::sleep(Duration::from_millis(1));
5845 }
5846 Err(error) => return Err(format!("accept first request: {error}")),
5847 }
5848 };
5849 first
5850 .set_write_timeout(Some(Duration::from_secs(1)))
5851 .map_err(|error| format!("set first connection write timeout: {error}"))?;
5852 first
5853 .set_nonblocking(false)
5854 .map_err(|error| format!("set first connection blocking: {error}"))?;
5855 let first_line = read_request(&mut first, deadline)?;
5856 if !first_line.starts_with("PUT ") {
5857 return Err(format!("first request was not a copy PUT: {first_line:?}"));
5858 }
5859 write_response(&mut first)?;
5860 first
5861 .set_nonblocking(true)
5862 .map_err(|error| format!("set first connection nonblocking: {error}"))?;
5863
5864 loop {
5867 let mut byte = [0u8; 1];
5868 match first.read(&mut byte) {
5869 Ok(0) => {}
5870 Ok(_) => return Ok(format!("reused closed connection: {first_line:?}")),
5871 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
5872 Err(error) => {
5873 return Err(format!("read old connection during reuse check: {error}"));
5874 }
5875 }
5876
5877 match listener.accept() {
5878 Ok((mut second, _)) => {
5879 second
5880 .set_write_timeout(Some(Duration::from_secs(1)))
5881 .map_err(|error| {
5882 format!("set second connection write timeout: {error}")
5883 })?;
5884 second
5885 .set_nonblocking(false)
5886 .map_err(|error| format!("set second connection blocking: {error}"))?;
5887 let second_line = read_request(&mut second, deadline)?;
5888 if !second_line.starts_with("PUT ") {
5889 return Err(format!(
5890 "second request was not a copy PUT: {second_line:?}"
5891 ));
5892 }
5893 write_response(&mut second)?;
5894 return Ok(format!(
5895 "fresh connection; first={first_line:?}; second={second_line:?}"
5896 ));
5897 }
5898 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
5899 Err(error) => return Err(format!("accept second request: {error}")),
5900 }
5901
5902 if Instant::now() >= deadline {
5903 return Err("deadline waiting for a fresh second connection".to_owned());
5904 }
5905 thread::sleep(Duration::from_millis(1));
5906 }
5907 });
5908
5909 let credentials = Credentials::new(
5910 Some("test_access_key"),
5911 Some("test_secret_key"),
5912 None,
5913 None,
5914 None,
5915 )
5916 .unwrap();
5917 let bucket = Bucket::new(
5918 "test-bucket",
5919 Region::Custom {
5920 region: "us-east-1".to_owned(),
5921 endpoint,
5922 },
5923 credentials,
5924 )
5925 .unwrap()
5926 .with_path_style()
5927 .with_request_timeout(Duration::from_secs(4))
5928 .unwrap();
5929
5930 let first_result = bucket.copy_object_internal("/source", "/destination").await;
5931 let second_result = bucket.copy_object_internal("/source", "/destination").await;
5932 let server_result = server.join().unwrap();
5933
5934 assert!(
5935 matches!(&server_result, Ok(result) if result.starts_with("fresh connection;")),
5936 "server={server_result:?}; first copy={first_result:?}; second copy={second_result:?}"
5937 );
5938 assert_eq!(first_result.unwrap(), 200);
5939 assert_eq!(second_result.unwrap(), 200);
5940 }
5941
5942 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
5943 #[tokio::test]
5944 async fn test_object_exists_404_does_not_retry() {
5945 init();
5946
5947 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5948 let endpoint = format!("http://{}", listener.local_addr().unwrap());
5949 let credentials = Credentials::new(
5950 Some("test_access_key"),
5951 Some("test_secret_key"),
5952 None,
5953 None,
5954 None,
5955 )
5956 .unwrap();
5957 let bucket = Bucket::new(
5958 "test-bucket",
5959 Region::Custom {
5960 region: "us-east-1".to_owned(),
5961 endpoint,
5962 },
5963 credentials,
5964 )
5965 .unwrap()
5966 .with_path_style();
5967 listener.set_nonblocking(true).unwrap();
5968 let (stop_server, stop_rx) = mpsc::channel();
5969
5970 let server = thread::spawn(move || {
5971 let mut requests = 0;
5972 let accept_deadline = Instant::now() + Duration::from_secs(15);
5973 loop {
5974 match stop_rx.try_recv() {
5975 Ok(()) | Err(TryRecvError::Disconnected) => break,
5976 Err(TryRecvError::Empty) => {}
5977 }
5978 assert!(
5979 Instant::now() < accept_deadline,
5980 "mock server did not receive a stop signal before its deadline"
5981 );
5982
5983 match listener.accept() {
5984 Ok((stream, _)) => {
5985 requests += 1;
5986 let mut stream = make_mock_stream_blocking(stream).unwrap();
5987 stream
5988 .set_read_timeout(Some(Duration::from_secs(3)))
5989 .unwrap();
5990 stream
5991 .set_write_timeout(Some(Duration::from_secs(3)))
5992 .unwrap();
5993
5994 let mut request = Vec::new();
5995 let mut byte = [0; 1];
5996 let header_deadline = Instant::now() + Duration::from_secs(3);
5997 while !request.ends_with(b"\r\n\r\n") {
5998 assert!(
5999 request.len() < 16 * 1024,
6000 "mock server received oversized HTTP headers"
6001 );
6002 assert!(
6003 Instant::now() < header_deadline,
6004 "mock server timed out reading HTTP headers"
6005 );
6006 match stream.read(&mut byte) {
6007 Ok(0) => panic!("client closed before sending HTTP headers"),
6008 Ok(_) => request.push(byte[0]),
6009 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {
6010 continue;
6011 }
6012 Err(error) => panic!("failed to read HTTP request: {error}"),
6013 }
6014 }
6015 stream.write_all(
6016 b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
6017 )
6018 .unwrap();
6019 }
6020 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
6021 if Instant::now() >= accept_deadline {
6022 break;
6023 }
6024 thread::sleep(Duration::from_millis(5));
6025 }
6026 Err(_) => break,
6027 }
6028 }
6029 requests
6030 });
6031
6032 let exists =
6033 tokio::time::timeout(Duration::from_secs(5), bucket.object_exists("/missing.txt"))
6034 .await;
6035 let _ = stop_server.send(());
6036 let request_count = server.join().unwrap();
6037
6038 assert!(!exists.expect("object_exists request timed out").unwrap());
6039 assert_eq!(request_count, 1);
6040 }
6041
6042 #[test]
6043 #[cfg(any(
6044 feature = "tokio-native-tls",
6045 feature = "tokio-rustls-tls",
6046 feature = "tokio-rustls-tls-ring"
6047 ))]
6048 #[allow(deprecated)]
6049 fn dangerous_config_correct_spelling_and_compat_alias_set_same_options() {
6050 let bucket = Bucket::new(
6051 "test-bucket",
6052 Region::Custom {
6053 region: "test-region".to_owned(),
6054 endpoint: "https://example.com".to_owned(),
6055 },
6056 Credentials::anonymous().unwrap(),
6057 )
6058 .unwrap();
6059
6060 let corrected = bucket.set_dangerous_config(true, true).unwrap();
6061 let deprecated_alias = bucket.set_dangereous_config(true, true).unwrap();
6062
6063 assert!(corrected.client_options.accept_invalid_certs);
6064 assert!(corrected.client_options.accept_invalid_hostnames);
6065 assert_eq!(
6066 corrected.client_options.accept_invalid_certs,
6067 deprecated_alias.client_options.accept_invalid_certs
6068 );
6069 assert_eq!(
6070 corrected.client_options.accept_invalid_hostnames,
6071 deprecated_alias.client_options.accept_invalid_hostnames
6072 );
6073 }
6074 fn test_aws_credentials() -> Credentials {
6075 Credentials::new(
6076 Some(&env::var("EU_AWS_ACCESS_KEY_ID").unwrap()),
6077 Some(&env::var("EU_AWS_SECRET_ACCESS_KEY").unwrap()),
6078 None,
6079 None,
6080 None,
6081 )
6082 .unwrap()
6083 }
6084
6085 fn test_gc_credentials() -> Credentials {
6086 Credentials::new(
6087 Some(&env::var("GC_ACCESS_KEY_ID").unwrap()),
6088 Some(&env::var("GC_SECRET_ACCESS_KEY").unwrap()),
6089 None,
6090 None,
6091 None,
6092 )
6093 .unwrap()
6094 }
6095
6096 fn test_wasabi_credentials() -> Credentials {
6097 Credentials::new(
6098 Some(&env::var("WASABI_ACCESS_KEY_ID").unwrap()),
6099 Some(&env::var("WASABI_SECRET_ACCESS_KEY").unwrap()),
6100 None,
6101 None,
6102 None,
6103 )
6104 .unwrap()
6105 }
6106
6107 fn test_minio_credentials() -> Credentials {
6108 Credentials::new(
6109 Some(&env::var("MINIO_ACCESS_KEY_ID").unwrap()),
6110 Some(&env::var("MINIO_SECRET_ACCESS_KEY").unwrap()),
6111 None,
6112 None,
6113 None,
6114 )
6115 .unwrap()
6116 }
6117
6118 fn test_digital_ocean_credentials() -> Credentials {
6119 Credentials::new(
6120 Some(&env::var("DIGITAL_OCEAN_ACCESS_KEY_ID").unwrap()),
6121 Some(&env::var("DIGITAL_OCEAN_SECRET_ACCESS_KEY").unwrap()),
6122 None,
6123 None,
6124 None,
6125 )
6126 .unwrap()
6127 }
6128
6129 fn test_r2_credentials() -> Credentials {
6130 Credentials::new(
6131 Some(&env::var("R2_ACCESS_KEY_ID").unwrap()),
6132 Some(&env::var("R2_SECRET_ACCESS_KEY").unwrap()),
6133 None,
6134 None,
6135 None,
6136 )
6137 .unwrap()
6138 }
6139
6140 fn test_aws_bucket() -> Box<Bucket> {
6141 Bucket::new(
6142 "rust-s3-test",
6143 "eu-central-1".parse().unwrap(),
6144 test_aws_credentials(),
6145 )
6146 .unwrap()
6147 }
6148
6149 fn isolated_test_bucket_name() -> String {
6150 std::env::var("RUST_S3_TEST_BUCKET")
6151 .unwrap_or_else(|_| format!("rust-s3-test-{}", uuid::Uuid::new_v4()))
6152 }
6153
6154 #[maybe_async::maybe_async]
6155 async fn report_bucket_cleanup(bucket: &Bucket, context: &str) {
6156 match bucket.delete().await {
6157 Ok(status) if status < 300 => {}
6158 Ok(status) => eprintln!("{context}: bucket cleanup returned HTTP {status}"),
6159 Err(error) => eprintln!("{context}: bucket cleanup failed: {error}"),
6160 }
6161 }
6162
6163 fn check_status(status: u16, expected: u16, operation: &str) -> Result<(), String> {
6164 if status == expected {
6165 Ok(())
6166 } else {
6167 Err(format!(
6168 "{operation} returned HTTP {status}, expected {expected}"
6169 ))
6170 }
6171 }
6172
6173 fn test_wasabi_bucket() -> Box<Bucket> {
6174 Bucket::new(
6175 "rust-s3",
6176 "wa-eu-central-1".parse().unwrap(),
6177 test_wasabi_credentials(),
6178 )
6179 .unwrap()
6180 }
6181
6182 fn test_gc_bucket() -> Box<Bucket> {
6183 let mut bucket = Bucket::new(
6184 "rust-s3",
6185 Region::Custom {
6186 region: "us-east1".to_owned(),
6187 endpoint: "https://storage.googleapis.com".to_owned(),
6188 },
6189 test_gc_credentials(),
6190 )
6191 .unwrap();
6192 bucket.set_listobjects_v1();
6193 bucket
6194 }
6195
6196 fn test_minio_bucket() -> Box<Bucket> {
6197 Bucket::new(
6198 "rust-s3",
6199 Region::Custom {
6200 region: "us-east-1".to_owned(),
6201 endpoint: "http://localhost:9000".to_owned(),
6202 },
6203 test_minio_credentials(),
6204 )
6205 .unwrap()
6206 .with_path_style()
6207 }
6208
6209 fn test_presign_bucket() -> Box<Bucket> {
6212 Bucket::new(
6213 "rust-s3",
6214 Region::Custom {
6215 region: "us-east-1".to_owned(),
6216 endpoint: "http://localhost:9000".to_owned(),
6217 },
6218 Credentials::new(
6219 Some("test_access_key"),
6220 Some("test_secret_key"),
6221 None,
6222 None,
6223 None,
6224 )
6225 .unwrap(),
6226 )
6227 .unwrap()
6228 .with_path_style()
6229 }
6230
6231 #[allow(dead_code)]
6232 fn test_digital_ocean_bucket() -> Box<Bucket> {
6233 Bucket::new("rust-s3", Region::DoFra1, test_digital_ocean_credentials()).unwrap()
6234 }
6235
6236 fn test_r2_bucket() -> Box<Bucket> {
6237 Bucket::new(
6238 "rust-s3",
6239 Region::R2 {
6240 account_id: "f048f3132be36fa1aaa8611992002b3f".to_string(),
6241 },
6242 test_r2_credentials(),
6243 )
6244 .unwrap()
6245 }
6246
6247 fn object(size: u32) -> Vec<u8> {
6248 (0..size).map(|_| 33).collect()
6249 }
6250
6251 #[maybe_async::maybe_async]
6252 async fn put_head_get_delete_object(bucket: Bucket, head: bool) {
6253 let s3_path = test_object_path("+test.file");
6254 let non_existant_path = test_object_path("+non_existant.file");
6255 let test: Vec<u8> = object(3072);
6256
6257 let response_data = bucket.put_object(&s3_path, &test).await.unwrap();
6258 assert_eq!(response_data.status_code(), 200);
6259
6260 let response_data = bucket.get_object(&s3_path).await.unwrap();
6266 assert_eq!(response_data.status_code(), 200);
6267 assert_eq!(test, response_data.as_slice());
6268
6269 let exists = bucket.object_exists(&s3_path).await.unwrap();
6270 assert!(exists);
6271
6272 let not_exists = bucket.object_exists(&non_existant_path).await.unwrap();
6273 assert!(!not_exists);
6274
6275 let response_data = bucket
6276 .get_object_range(&s3_path, 100, Some(1000))
6277 .await
6278 .unwrap();
6279 assert_eq!(response_data.status_code(), 206);
6280 assert_eq!(test[100..1001].to_vec(), response_data.as_slice());
6281
6282 let response_data = bucket
6284 .get_object_range(&s3_path, 100, Some(100))
6285 .await
6286 .unwrap();
6287 assert_eq!(response_data.status_code(), 206);
6288 assert_eq!(vec![test[100]], response_data.as_slice());
6289
6290 if head {
6291 let (_head_object_result, code) = bucket.head_object(&s3_path).await.unwrap();
6292 assert_eq!(code, 200);
6294 }
6295
6296 let response_data = bucket.delete_object(&s3_path).await.unwrap();
6298 assert_eq!(response_data.status_code(), 204);
6299 }
6300
6301 #[maybe_async::maybe_async]
6302 async fn put_head_delete_object_with_headers(bucket: Bucket) {
6303 let s3_path = test_object_path("+test.file");
6304 let non_existant_path = test_object_path("+non_existant.file");
6305 let test: Vec<u8> = object(3072);
6306 let header_value = "max-age=42";
6307
6308 let mut custom_headers = HeaderMap::new();
6309 custom_headers.insert(CACHE_CONTROL, HeaderValue::from_static(header_value));
6310 custom_headers.insert(
6311 HeaderName::from_static("test-key"),
6312 "value".parse().unwrap(),
6313 );
6314
6315 let response_data = bucket
6316 .put_object_with_headers(&s3_path, &test, Some(custom_headers.clone()))
6317 .await
6318 .expect("Put object with custom headers failed");
6319 assert_eq!(response_data.status_code(), 200);
6320
6321 let response_data = bucket.get_object(&s3_path).await.unwrap();
6322 assert_eq!(response_data.status_code(), 200);
6323 assert_eq!(test, response_data.as_slice());
6324
6325 let exists = bucket.object_exists(&s3_path).await.unwrap();
6326 assert!(exists);
6327
6328 let not_exists = bucket.object_exists(&non_existant_path).await.unwrap();
6329 assert!(!not_exists);
6330
6331 let response_data = bucket
6332 .get_object_range(&s3_path, 100, Some(1000))
6333 .await
6334 .unwrap();
6335 assert_eq!(response_data.status_code(), 206);
6336 assert_eq!(test[100..1001].to_vec(), response_data.as_slice());
6337
6338 let (head_object_result, code) = bucket.head_object(&s3_path).await.unwrap();
6339 assert_eq!(code, 200);
6341 assert_eq!(
6342 head_object_result.cache_control,
6343 Some(header_value.to_string())
6344 );
6345
6346 let response_data = bucket.delete_object(&s3_path).await.unwrap();
6347 assert_eq!(response_data.status_code(), 204);
6348 }
6349
6350 #[ignore]
6351 #[cfg(feature = "tags")]
6352 #[maybe_async::test(
6353 feature = "sync",
6354 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6355 async(
6356 all(not(feature = "sync"), feature = "with-async-std"),
6357 async_std::test
6358 )
6359 )]
6360 async fn test_tagging_aws() {
6361 let bucket = test_aws_bucket();
6362 let tagging_path = test_object_key("tagging_test");
6363 let target_tags = vec![
6364 Tag {
6365 key: "Tag1".to_string(),
6366 value: "Value1".to_string(),
6367 },
6368 Tag {
6369 key: "Tag2".to_string(),
6370 value: "Value2".to_string(),
6371 },
6372 ];
6373 let empty_tags: Vec<Tag> = Vec::new();
6374 let response_data = bucket
6375 .put_object(&tagging_path, b"Gimme tags")
6376 .await
6377 .unwrap();
6378 assert_eq!(response_data.status_code(), 200);
6379 let (tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6380 assert_eq!(tags, empty_tags);
6381 let response_data = bucket
6382 .put_object_tagging(&tagging_path, &[("Tag1", "Value1"), ("Tag2", "Value2")])
6383 .await
6384 .unwrap();
6385 assert_eq!(response_data.status_code(), 200);
6386 let (mut tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6387 tags.sort_by(|left, right| left.key.cmp(&right.key));
6388 let mut target_tags = target_tags;
6389 target_tags.sort_by(|left, right| left.key.cmp(&right.key));
6390 assert_eq!(tags, target_tags);
6391 let _response_data = bucket.delete_object(&tagging_path).await.unwrap();
6392 }
6393
6394 #[ignore]
6395 #[cfg(feature = "tags")]
6396 #[maybe_async::test(
6397 feature = "sync",
6398 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6399 async(
6400 all(not(feature = "sync"), feature = "with-async-std"),
6401 async_std::test
6402 )
6403 )]
6404 async fn test_tagging_minio() {
6405 let bucket = test_minio_bucket();
6406 let tagging_path = test_object_key("tagging_test");
6407 let target_tags = vec![
6408 Tag {
6409 key: "Tag1".to_string(),
6410 value: "Value1".to_string(),
6411 },
6412 Tag {
6413 key: "Tag2".to_string(),
6414 value: "Value2".to_string(),
6415 },
6416 ];
6417 let empty_tags: Vec<Tag> = Vec::new();
6418 let response_data = bucket
6419 .put_object(&tagging_path, b"Gimme tags")
6420 .await
6421 .unwrap();
6422 assert_eq!(response_data.status_code(), 200);
6423 let (tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6424 assert_eq!(tags, empty_tags);
6425 let response_data = bucket
6426 .put_object_tagging(&tagging_path, &[("Tag1", "Value1"), ("Tag2", "Value2")])
6427 .await
6428 .unwrap();
6429 assert_eq!(response_data.status_code(), 200);
6430 let (mut tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6431 tags.sort_by(|left, right| left.key.cmp(&right.key));
6432 let mut target_tags = target_tags;
6433 target_tags.sort_by(|left, right| left.key.cmp(&right.key));
6434 assert_eq!(tags, target_tags);
6435 let _response_data = bucket.delete_object(&tagging_path).await.unwrap();
6436 }
6437
6438 #[ignore]
6439 #[maybe_async::test(
6440 feature = "sync",
6441 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6442 async(
6443 all(not(feature = "sync"), feature = "with-async-std"),
6444 async_std::test
6445 )
6446 )]
6447 async fn streaming_big_aws_put_head_get_delete_object() {
6448 streaming_test_put_get_delete_big_object(*test_aws_bucket()).await;
6449 }
6450
6451 #[ignore]
6452 #[maybe_async::test(
6453 feature = "sync",
6454 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6455 async(
6456 all(not(feature = "sync"), feature = "with-async-std"),
6457 async_std::test
6458 )
6459 )]
6460 async fn streaming_big_gc_put_head_get_delete_object() {
6461 streaming_test_put_get_delete_big_object(*test_gc_bucket()).await;
6462 }
6463
6464 #[ignore]
6465 #[maybe_async::test(
6466 feature = "sync",
6467 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6468 async(
6469 all(not(feature = "sync"), feature = "with-async-std"),
6470 async_std::test
6471 )
6472 )]
6473 async fn streaming_big_minio_put_head_get_delete_object() {
6474 streaming_test_put_get_delete_big_object(*test_minio_bucket()).await;
6475 }
6476
6477 #[ignore]
6478 #[maybe_async::test(
6479 feature = "sync",
6480 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6481 async(
6482 all(not(feature = "sync"), feature = "with-async-std"),
6483 async_std::test
6484 )
6485 )]
6486 async fn streaming_big_wasabi_put_head_get_delete_object() {
6487 streaming_test_put_get_delete_big_object(*test_wasabi_bucket()).await;
6488 }
6489
6490 #[ignore]
6491 #[maybe_async::test(
6492 feature = "sync",
6493 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6494 async(
6495 all(not(feature = "sync"), feature = "with-async-std"),
6496 async_std::test
6497 )
6498 )]
6499 async fn streaming_big_digital_ocean_put_head_get_delete_object() {
6500 streaming_test_put_get_delete_big_object(*test_digital_ocean_bucket()).await;
6501 }
6502
6503 #[ignore]
6504 #[maybe_async::test(
6505 feature = "sync",
6506 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6507 async(
6508 all(not(feature = "sync"), feature = "with-async-std"),
6509 async_std::test
6510 )
6511 )]
6512 async fn streaming_big_r2_put_head_get_delete_object() {
6513 streaming_test_put_get_delete_big_object(*test_r2_bucket()).await;
6514 }
6515
6516 #[maybe_async::maybe_async]
6518 async fn streaming_test_put_get_delete_big_object(bucket: Bucket) {
6519 #[cfg(feature = "with-async-std")]
6520 use async_std::fs::File;
6521 #[cfg(feature = "with-async-std")]
6522 use async_std::io::WriteExt;
6523 #[cfg(feature = "with-async-std")]
6524 use async_std::stream::StreamExt;
6525 #[cfg(feature = "with-tokio")]
6526 use futures_util::StreamExt;
6527 #[cfg(not(any(feature = "with-tokio", feature = "with-async-std")))]
6528 use std::fs::File;
6529 #[cfg(not(any(feature = "with-tokio", feature = "with-async-std")))]
6530 use std::io::Write;
6531 #[cfg(feature = "with-tokio")]
6532 use tokio::fs::File;
6533 #[cfg(feature = "with-tokio")]
6534 use tokio::io::AsyncWriteExt;
6535
6536 init();
6537 let remote_path = test_object_key("+stream_test_big");
6538 let local_path = "+stream_test_big";
6539 std::fs::remove_file(local_path).unwrap_or(());
6540 let content: Vec<u8> = (0..20_000_000).map(|i| (i % 251) as u8).collect();
6541
6542 let mut file = File::create(local_path).await.unwrap();
6543 file.write_all(&content).await.unwrap();
6544 file.flush().await.unwrap();
6545 let mut reader = File::open(local_path).await.unwrap();
6546
6547 #[cfg(not(feature = "sync"))]
6548 let response = bucket
6549 .put_object_stream_builder(&remote_path)
6550 .with_content_type("application/x-rust-s3-stream-test")
6551 .with_headers(streaming_builder_test_headers())
6552 .execute_stream(&mut reader)
6553 .await
6554 .unwrap();
6555 #[cfg(feature = "sync")]
6556 let response = bucket
6557 .put_object_stream(&mut reader, &remote_path)
6558 .await
6559 .unwrap();
6560 #[cfg(not(feature = "sync"))]
6561 assert_eq!(response.status_code(), 200);
6562 #[cfg(feature = "sync")]
6563 assert_eq!(response, 200);
6564 #[cfg(not(feature = "sync"))]
6565 {
6566 let (head, code) = bucket.head_object(&remote_path).await.unwrap();
6567 assert_eq!(code, 200);
6568 assert_eq!(
6569 head.content_type.as_deref(),
6570 Some("application/x-rust-s3-stream-test")
6571 );
6572 assert_eq!(head.cache_control.as_deref(), Some("public, max-age=120"));
6573 assert_eq!(
6574 head.metadata
6575 .as_ref()
6576 .and_then(|metadata| metadata.get("stream-marker"))
6577 .map(String::as_str),
6578 Some("multipart-builder")
6579 );
6580 }
6581 let mut writer = Vec::new();
6582 let code = bucket
6583 .get_object_to_writer(&remote_path, &mut writer)
6584 .await
6585 .unwrap();
6586 assert_eq!(code, 200);
6587 assert!(
6588 content == writer,
6589 "writer download differs from uploaded bytes"
6590 );
6591 assert_eq!(content.len(), 20_000_000);
6592
6593 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
6594 {
6595 let mut response_data_stream = bucket.get_object_stream(&remote_path).await.unwrap();
6596
6597 let mut streamed_len = 0;
6598 while let Some(chunk) = response_data_stream.bytes().next().await {
6599 let chunk = chunk.unwrap();
6600 let end = streamed_len + chunk.len();
6601 assert!(end <= content.len(), "stream returned too many bytes");
6602 assert!(
6603 content[streamed_len..end] == chunk[..],
6604 "streamed bytes differ from uploaded bytes at offset {streamed_len}"
6605 );
6606 streamed_len = end;
6607 }
6608 assert_eq!(streamed_len, content.len());
6609 }
6610
6611 let response_data = bucket.delete_object(&remote_path).await.unwrap();
6612 assert_eq!(response_data.status_code(), 204);
6613 std::fs::remove_file(local_path).unwrap_or(());
6614 }
6615
6616 #[ignore]
6617 #[maybe_async::test(
6618 feature = "sync",
6619 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6620 async(
6621 all(not(feature = "sync"), feature = "with-async-std"),
6622 async_std::test
6623 )
6624 )]
6625 async fn streaming_aws_put_head_get_delete_object() {
6626 streaming_test_put_get_delete_small_object(test_aws_bucket()).await;
6627 }
6628
6629 #[ignore]
6630 #[maybe_async::test(
6631 feature = "sync",
6632 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6633 async(
6634 all(not(feature = "sync"), feature = "with-async-std"),
6635 async_std::test
6636 )
6637 )]
6638 async fn streaming_gc_put_head_get_delete_object() {
6639 streaming_test_put_get_delete_small_object(test_gc_bucket()).await;
6640 }
6641
6642 #[ignore]
6643 #[maybe_async::test(
6644 feature = "sync",
6645 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6646 async(
6647 all(not(feature = "sync"), feature = "with-async-std"),
6648 async_std::test
6649 )
6650 )]
6651 async fn streaming_r2_put_head_get_delete_object() {
6652 streaming_test_put_get_delete_small_object(test_r2_bucket()).await;
6653 }
6654
6655 #[ignore]
6656 #[maybe_async::test(
6657 feature = "sync",
6658 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6659 async(
6660 all(not(feature = "sync"), feature = "with-async-std"),
6661 async_std::test
6662 )
6663 )]
6664 async fn streaming_minio_put_head_get_delete_object() {
6665 streaming_test_put_get_delete_small_object(test_minio_bucket()).await;
6666 }
6667
6668 #[ignore]
6669 #[cfg(not(feature = "sync"))]
6670 #[maybe_async::test(
6671 feature = "sync",
6672 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6673 async(
6674 all(not(feature = "sync"), feature = "with-async-std"),
6675 async_std::test
6676 )
6677 )]
6678 async fn streaming_aws_explicit_multipart_limits_roundtrip() {
6679 streaming_test_explicit_multipart_limits(test_aws_bucket()).await;
6680 }
6681
6682 #[ignore]
6683 #[cfg(not(feature = "sync"))]
6684 #[maybe_async::test(
6685 feature = "sync",
6686 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6687 async(
6688 all(not(feature = "sync"), feature = "with-async-std"),
6689 async_std::test
6690 )
6691 )]
6692 async fn streaming_minio_explicit_multipart_limits_roundtrip() {
6693 streaming_test_explicit_multipart_limits(test_minio_bucket()).await;
6694 }
6695
6696 #[cfg(not(feature = "sync"))]
6697 #[maybe_async::maybe_async]
6698 async fn streaming_test_explicit_multipart_limits(bucket: Box<Bucket>) {
6699 init();
6700 let content = object((2 * super::CHUNK_SIZE + 23) as u32);
6701 for limit in [1, 2] {
6702 let key = test_object_key(&format!("+stream_concurrency_{limit}"));
6703 #[cfg(feature = "with-tokio")]
6704 let mut reader = std::io::Cursor::new(content.clone());
6705 #[cfg(feature = "with-async-std")]
6706 let mut reader = async_std::io::Cursor::new(content.clone());
6707 let response = bucket
6708 .put_object_stream_builder(&key)
6709 .with_max_concurrent_chunks(limit)
6710 .execute_stream(&mut reader)
6711 .await
6712 .unwrap();
6713 assert_eq!(response.status_code(), 200);
6714 assert_eq!(response.uploaded_bytes(), content.len());
6715 let mut downloaded = Vec::new();
6716 assert_eq!(
6717 bucket
6718 .get_object_to_writer(&key, &mut downloaded)
6719 .await
6720 .unwrap(),
6721 200
6722 );
6723 assert_eq!(downloaded, content);
6724 assert_eq!(bucket.delete_object(&key).await.unwrap().status_code(), 204);
6725 }
6726 }
6727
6728 #[maybe_async::maybe_async]
6729 async fn streaming_test_put_get_delete_small_object(bucket: Box<Bucket>) {
6730 init();
6731 let remote_path = test_object_key("+stream_test_small");
6732 let content: Vec<u8> = object(1000);
6733 #[cfg(feature = "with-tokio")]
6734 let mut reader = std::io::Cursor::new(&content);
6735 #[cfg(feature = "with-async-std")]
6736 let mut reader = async_std::io::Cursor::new(&content);
6737 #[cfg(feature = "sync")]
6738 let mut reader = std::io::Cursor::new(&content);
6739
6740 #[cfg(not(feature = "sync"))]
6741 let response = bucket
6742 .put_object_stream_builder(&remote_path)
6743 .with_content_type("application/x-rust-s3-stream-test")
6744 .with_headers(streaming_builder_test_headers())
6745 .execute_stream(&mut reader)
6746 .await
6747 .unwrap();
6748 #[cfg(feature = "sync")]
6749 let response = bucket
6750 .put_object_stream(&mut reader, &remote_path)
6751 .await
6752 .unwrap();
6753 #[cfg(not(feature = "sync"))]
6754 assert_eq!(response.status_code(), 200);
6755 #[cfg(feature = "sync")]
6756 assert_eq!(response, 200);
6757 #[cfg(not(feature = "sync"))]
6758 {
6759 let (head, code) = bucket.head_object(&remote_path).await.unwrap();
6760 assert_eq!(code, 200);
6761 assert_eq!(
6762 head.content_type.as_deref(),
6763 Some("application/x-rust-s3-stream-test")
6764 );
6765 assert_eq!(head.cache_control.as_deref(), Some("public, max-age=120"));
6766 assert_eq!(
6767 head.metadata
6768 .as_ref()
6769 .and_then(|metadata| metadata.get("stream-marker"))
6770 .map(String::as_str),
6771 Some("multipart-builder")
6772 );
6773 }
6774 let mut writer = Vec::new();
6775 let code = bucket
6776 .get_object_to_writer(&remote_path, &mut writer)
6777 .await
6778 .unwrap();
6779 assert_eq!(code, 200);
6780 assert_eq!(content, writer);
6781
6782 let response_data = bucket.delete_object(&remote_path).await.unwrap();
6783 assert_eq!(response_data.status_code(), 204);
6784 }
6785
6786 #[cfg(all(
6787 not(feature = "sync"),
6788 any(feature = "with-tokio", feature = "with-async-std")
6789 ))]
6790 fn streaming_builder_test_headers() -> HeaderMap {
6791 let mut headers = HeaderMap::new();
6792 headers.insert(
6793 CACHE_CONTROL,
6794 HeaderValue::from_static("public, max-age=120"),
6795 );
6796 headers.insert(
6797 HeaderName::from_static("x-amz-meta-stream-marker"),
6798 HeaderValue::from_static("multipart-builder"),
6799 );
6800 headers
6801 }
6802
6803 #[cfg(feature = "blocking")]
6804 fn put_head_get_list_delete_object_blocking(bucket: Bucket) {
6805 let s3_key = test_object_key("test_blocking.file");
6806 let s3_key_2 = test_object_key("test_blocking.file2");
6807 let s3_key_3 = test_object_key("test_blocking.file3");
6808 let s3_path = format!("/{s3_key}");
6809 let s3_path_2 = format!("/{s3_key_2}");
6810 let s3_path_3 = format!("/{s3_key_3}");
6811 let list_prefix = test_object_key("test_blocking.");
6812 let test: Vec<u8> = object(3072);
6813
6814 let response_data = bucket.put_object_blocking(&s3_path, &test).unwrap();
6816 assert_eq!(response_data.status_code(), 200);
6817
6818 let response_data = bucket.get_object_blocking(&s3_path).unwrap();
6820 assert_eq!(response_data.status_code(), 200);
6821 assert_eq!(test, response_data.as_slice());
6822
6823 let response_data = bucket
6825 .get_object_range_blocking(&s3_path, 100, Some(1000))
6826 .unwrap();
6827 assert_eq!(response_data.status_code(), 206);
6828 assert_eq!(test[100..1001].to_vec(), response_data.as_slice());
6829
6830 let response_data = bucket
6832 .get_object_range_blocking(&s3_path, 100, Some(100))
6833 .unwrap();
6834 assert_eq!(response_data.status_code(), 206);
6835 assert_eq!(vec![test[100]], response_data.as_slice());
6836
6837 let (head_object_result, code) = bucket.head_object_blocking(&s3_path).unwrap();
6839 assert_eq!(code, 200);
6840 assert_eq!(
6841 head_object_result.content_type.unwrap(),
6842 "application/octet-stream".to_owned()
6843 );
6844 let response_data = bucket.put_object_blocking(&s3_path_2, &test).unwrap();
6848 assert_eq!(response_data.status_code(), 200);
6849 let response_data = bucket.put_object_blocking(&s3_path_3, &test).unwrap();
6850 assert_eq!(response_data.status_code(), 200);
6851
6852 let (result, code) = bucket
6854 .list_page_blocking(
6855 list_prefix.clone(),
6856 Some("/".to_string()),
6857 None,
6858 None,
6859 Some(2),
6860 )
6861 .unwrap();
6862 assert_eq!(code, 200);
6863 assert_eq!(result.contents.len(), 2);
6864 assert!(result.is_truncated);
6865 let mut listed_keys: Vec<String> =
6866 result.contents.into_iter().map(|item| item.key).collect();
6867
6868 let cont_token = result
6869 .next_continuation_token
6870 .expect("truncated listing should include a continuation token");
6871
6872 let (result, code) = bucket
6873 .list_page_blocking(
6874 list_prefix,
6875 Some("/".to_string()),
6876 Some(cont_token),
6877 None,
6878 Some(2),
6879 )
6880 .unwrap();
6881 assert_eq!(code, 200);
6882 assert_eq!(result.contents.len(), 1);
6883 assert!(!result.is_truncated);
6884 assert!(result.next_continuation_token.is_none());
6885 listed_keys.extend(result.contents.into_iter().map(|item| item.key));
6886 listed_keys.sort();
6887 let mut expected_keys = vec![s3_key, s3_key_2, s3_key_3];
6888 expected_keys.sort();
6889 assert_eq!(listed_keys, expected_keys);
6890
6891 let response_data = bucket.delete_object_blocking(&s3_path).unwrap();
6893 assert_eq!(response_data.status_code(), 204);
6894 let response_data = bucket.delete_object_blocking(&s3_path_2).unwrap();
6895 assert_eq!(response_data.status_code(), 204);
6896 let response_data = bucket.delete_object_blocking(&s3_path_3).unwrap();
6897 assert_eq!(response_data.status_code(), 204);
6898 }
6899
6900 #[ignore]
6901 #[cfg(all(
6902 any(feature = "with-tokio", feature = "with-async-std"),
6903 feature = "blocking"
6904 ))]
6905 #[test]
6906 fn aws_put_head_get_delete_object_blocking() {
6907 put_head_get_list_delete_object_blocking(*test_aws_bucket())
6908 }
6909
6910 #[ignore]
6911 #[cfg(all(
6912 any(feature = "with-tokio", feature = "with-async-std"),
6913 feature = "blocking"
6914 ))]
6915 #[test]
6916 fn gc_put_head_get_delete_object_blocking() {
6917 put_head_get_list_delete_object_blocking(*test_gc_bucket())
6918 }
6919
6920 #[ignore]
6921 #[cfg(all(
6922 any(feature = "with-tokio", feature = "with-async-std"),
6923 feature = "blocking"
6924 ))]
6925 #[test]
6926 fn wasabi_put_head_get_delete_object_blocking() {
6927 put_head_get_list_delete_object_blocking(*test_wasabi_bucket())
6928 }
6929
6930 #[ignore]
6931 #[cfg(all(
6932 any(feature = "with-tokio", feature = "with-async-std"),
6933 feature = "blocking"
6934 ))]
6935 #[test]
6936 fn minio_put_head_get_delete_object_blocking() {
6937 put_head_get_list_delete_object_blocking(*test_minio_bucket())
6938 }
6939
6940 #[ignore]
6941 #[cfg(all(
6942 any(feature = "with-tokio", feature = "with-async-std"),
6943 feature = "blocking"
6944 ))]
6945 #[test]
6946 fn r2_put_head_get_delete_object_blocking() {
6947 put_head_get_list_delete_object_blocking(*test_r2_bucket())
6948 }
6949
6950 #[ignore]
6951 #[cfg(all(
6952 any(feature = "with-tokio", feature = "with-async-std"),
6953 feature = "blocking"
6954 ))]
6955 #[test]
6956 fn digital_ocean_put_head_get_delete_object_blocking() {
6957 put_head_get_list_delete_object_blocking(*test_digital_ocean_bucket())
6958 }
6959
6960 #[ignore]
6961 #[maybe_async::test(
6962 feature = "sync",
6963 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6964 async(
6965 all(not(feature = "sync"), feature = "with-async-std"),
6966 async_std::test
6967 )
6968 )]
6969 async fn aws_put_head_get_delete_object() {
6970 put_head_get_delete_object(*test_aws_bucket(), true).await;
6971 put_head_delete_object_with_headers(*test_aws_bucket()).await;
6972 }
6973
6974 #[ignore]
6975 #[maybe_async::test(
6976 feature = "sync",
6977 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6978 async(
6979 all(not(feature = "sync"), feature = "with-async-std"),
6980 async_std::test
6981 )
6982 )]
6983 async fn gc_test_put_head_get_delete_object() {
6984 put_head_get_delete_object(*test_gc_bucket(), true).await;
6985 put_head_delete_object_with_headers(*test_gc_bucket()).await;
6986 }
6987
6988 #[ignore]
6989 #[maybe_async::test(
6990 feature = "sync",
6991 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6992 async(
6993 all(not(feature = "sync"), feature = "with-async-std"),
6994 async_std::test
6995 )
6996 )]
6997 async fn wasabi_test_put_head_get_delete_object() {
6998 put_head_get_delete_object(*test_wasabi_bucket(), true).await;
6999 put_head_delete_object_with_headers(*test_wasabi_bucket()).await;
7000 }
7001
7002 #[ignore]
7003 #[maybe_async::test(
7004 feature = "sync",
7005 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7006 async(
7007 all(not(feature = "sync"), feature = "with-async-std"),
7008 async_std::test
7009 )
7010 )]
7011 async fn minio_test_put_head_get_delete_object() {
7012 let bucket = *test_minio_bucket();
7013 put_head_get_delete_object(bucket.clone(), true).await;
7014 put_head_delete_object_with_headers(bucket.clone()).await;
7015
7016 let prefix = test_object_key(&format!("+copy_roundtrip_{}", uuid::Uuid::new_v4()));
7017 let source = format!("{prefix}_source");
7018 let destination = format!("{prefix}_destination");
7019 let content = b"MinIO copy object content verification";
7020 let copy_result = match bucket.put_object(&source, content).await {
7021 Err(error) => Err(error),
7022 Ok(_) => match bucket.copy_object_internal(&source, &destination).await {
7023 Err(error) => Err(error),
7024 Ok(status) => bucket
7025 .get_object(&destination)
7026 .await
7027 .map(|copied| (status, copied.to_vec())),
7028 },
7029 };
7030 let source_cleanup = bucket.delete_object(&source).await;
7031 let destination_cleanup = bucket.delete_object(&destination).await;
7032
7033 assert_eq!(source_cleanup.unwrap().status_code(), 204);
7034 assert_eq!(destination_cleanup.unwrap().status_code(), 204);
7035 let (status, copied) = copy_result.unwrap();
7036 assert_eq!(status, 200);
7037 assert_eq!(copied.as_slice(), content);
7038 }
7039
7040 #[ignore]
7041 #[maybe_async::test(
7042 feature = "sync",
7043 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7044 async(
7045 all(not(feature = "sync"), feature = "with-async-std"),
7046 async_std::test
7047 )
7048 )]
7049 async fn minio_multipart_upload_pagination_uses_key_and_upload_id_markers() {
7050 let bucket = *test_minio_bucket();
7051 let key = test_object_key(&format!("multipart-pagination/{}", uuid::Uuid::new_v4()));
7052 let mut created_upload_ids = Vec::new();
7053 let listing_result =
7054 minio_list_multipart_pages(&bucket, &key, &mut created_upload_ids).await;
7055
7056 let mut cleanup_errors = Vec::new();
7057 for upload_id in &created_upload_ids {
7058 if let Err(error) = bucket.abort_upload(&key, upload_id).await {
7059 cleanup_errors.push(error.to_string());
7060 }
7061 }
7062
7063 let listed_ids = listing_result.unwrap_or_else(|primary| {
7064 panic!(
7065 "MinIO multipart pagination failed: {primary}; cleanup errors: {cleanup_errors:?}"
7066 )
7067 });
7068 assert!(
7069 cleanup_errors.is_empty(),
7070 "failed to abort test multipart uploads: {cleanup_errors:?}"
7071 );
7072 assert_eq!(listed_ids.len(), 2);
7073 }
7074
7075 #[maybe_async::maybe_async]
7076 async fn minio_list_multipart_pages(
7077 bucket: &Bucket,
7078 key: &str,
7079 created_upload_ids: &mut Vec<String>,
7080 ) -> Result<Vec<String>, String> {
7081 for _ in 0..2 {
7082 let upload = bucket
7083 .initiate_multipart_upload(key, "application/octet-stream")
7084 .await
7085 .map_err(|error| error.to_string())?;
7086 created_upload_ids.push(upload.upload_id);
7087 }
7088
7089 let (first_page, first_status) = bucket
7090 .list_multiparts_uploads_page(Some(key), None, None, None, Some(1))
7091 .await
7092 .map_err(|error| error.to_string())?;
7093 if first_status != 200 || !first_page.is_truncated || first_page.uploads.len() != 1 {
7094 return Err("first MinIO page did not contain one truncated upload".to_owned());
7095 }
7096 let next_key_marker = first_page
7097 .next_marker
7098 .clone()
7099 .ok_or_else(|| "first MinIO page omitted NextKeyMarker".to_owned())?;
7100 let next_upload_id_marker = first_page
7101 .next_upload_id_marker
7102 .clone()
7103 .ok_or_else(|| "first MinIO page omitted NextUploadIdMarker".to_owned())?;
7104
7105 let (second_page, second_status) = bucket
7106 .list_multiparts_uploads_page(
7107 Some(key),
7108 None,
7109 Some(next_key_marker),
7110 Some(next_upload_id_marker),
7111 Some(1),
7112 )
7113 .await
7114 .map_err(|error| error.to_string())?;
7115 if second_status != 200 || second_page.is_truncated || second_page.uploads.len() != 1 {
7116 return Err("second MinIO page did not contain one upload".to_owned());
7117 }
7118
7119 let mut listed_ids = first_page
7120 .uploads
7121 .iter()
7122 .chain(second_page.uploads.iter())
7123 .map(|upload| upload.id.clone())
7124 .collect::<Vec<_>>();
7125 listed_ids.sort();
7126 let mut expected_ids = created_upload_ids.clone();
7127 expected_ids.sort();
7128 if listed_ids != expected_ids {
7129 return Err(
7130 "MinIO pagination did not return each created upload exactly once".to_owned(),
7131 );
7132 }
7133 Ok(listed_ids)
7134 }
7135
7136 #[ignore]
7137 #[maybe_async::test(
7138 feature = "sync",
7139 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7140 async(
7141 all(not(feature = "sync"), feature = "with-async-std"),
7142 async_std::test
7143 )
7144 )]
7145 async fn digital_ocean_test_put_head_get_delete_object() {
7146 let bucket = *test_digital_ocean_bucket();
7147 put_head_get_delete_object(bucket.clone(), true).await;
7148 put_head_delete_object_with_headers(bucket).await;
7149 }
7150
7151 #[ignore]
7152 #[maybe_async::test(
7153 feature = "sync",
7154 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7155 async(
7156 all(not(feature = "sync"), feature = "with-async-std"),
7157 async_std::test
7158 )
7159 )]
7160 async fn r2_test_put_head_get_delete_object() {
7161 put_head_get_delete_object(*test_r2_bucket(), false).await;
7162 put_head_delete_object_with_headers(*test_r2_bucket()).await;
7163 }
7164
7165 #[maybe_async::maybe_async]
7166 async fn put_delete_objects(bucket: Bucket) {
7167 use crate::serde_types::ObjectIdentifier;
7168
7169 let paths = [
7170 test_object_path("+bulk_delete_1.file"),
7171 test_object_path("+bulk_delete_2.file"),
7172 test_object_path("+bulk_delete_3.file"),
7173 ];
7174 let test: Vec<u8> = object(128);
7175
7176 for path in &paths {
7178 let response_data = bucket.put_object(path, &test).await.unwrap();
7179 assert_eq!(response_data.status_code(), 200);
7180 }
7181
7182 let objects: Vec<ObjectIdentifier> = paths
7184 .iter()
7185 .map(|path| ObjectIdentifier::new(path.as_str()))
7186 .collect();
7187 let result = bucket.delete_objects(objects).await.unwrap();
7188
7189 assert_eq!(result.deleted.len(), 3);
7190 assert!(result.errors.is_empty());
7191
7192 for path in &paths {
7194 let exists = bucket.object_exists(path).await.unwrap();
7195 assert!(!exists);
7196 }
7197 }
7198
7199 #[ignore]
7200 #[maybe_async::test(
7201 feature = "sync",
7202 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7203 async(
7204 all(not(feature = "sync"), feature = "with-async-std"),
7205 async_std::test
7206 )
7207 )]
7208 async fn aws_test_delete_objects() {
7209 put_delete_objects(*test_aws_bucket()).await;
7210 }
7211
7212 #[ignore]
7213 #[maybe_async::test(
7214 feature = "sync",
7215 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7216 async(
7217 all(not(feature = "sync"), feature = "with-async-std"),
7218 async_std::test
7219 )
7220 )]
7221 async fn minio_test_delete_objects() {
7222 put_delete_objects(*test_minio_bucket()).await;
7223 }
7224
7225 #[maybe_async::test(
7226 feature = "sync",
7227 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7228 async(
7229 all(not(feature = "sync"), feature = "with-async-std"),
7230 async_std::test
7231 )
7232 )]
7233 async fn test_presign_put() {
7234 let s3_path = "/test/test.file";
7235 let bucket = test_presign_bucket();
7236
7237 let mut custom_headers = HeaderMap::new();
7238 custom_headers.insert(
7239 HeaderName::from_static("custom_header"),
7240 "custom_value".parse().unwrap(),
7241 );
7242
7243 let url = bucket
7244 .presign_put(s3_path, 86400, Some(custom_headers), None)
7245 .await
7246 .unwrap();
7247
7248 assert!(url.contains("custom_header%3Bhost"));
7249 assert!(url.contains("/test/test.file"))
7250 }
7251
7252 #[maybe_async::test(
7253 feature = "sync",
7254 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7255 async(
7256 all(not(feature = "sync"), feature = "with-async-std"),
7257 async_std::test
7258 )
7259 )]
7260 async fn test_presign_post() {
7261 use std::borrow::Cow;
7262
7263 let bucket = test_presign_bucket();
7264
7265 let policy = PostPolicy::new(86400)
7267 .condition(
7268 PostPolicyField::Key,
7269 PostPolicyValue::StartsWith(Cow::from("user/user1/")),
7270 )
7271 .unwrap();
7272
7273 let data = bucket.presign_post(policy).await.unwrap();
7274
7275 assert_eq!(data.url, "http://localhost:9000/rust-s3");
7276 assert_eq!(data.fields.len(), 6);
7277 assert_eq!(data.dynamic_fields.len(), 1);
7278 }
7279
7280 #[maybe_async::test(
7281 feature = "sync",
7282 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7283 async(
7284 all(not(feature = "sync"), feature = "with-async-std"),
7285 async_std::test
7286 )
7287 )]
7288 async fn test_presign_get() {
7289 let s3_path = "/test/test.file";
7290 let bucket = test_presign_bucket();
7291
7292 let url = bucket.presign_get(s3_path, 86400, None).await.unwrap();
7293 assert!(url.contains("/test/test.file?"))
7294 }
7295
7296 #[maybe_async::test(
7297 feature = "sync",
7298 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7299 async(
7300 all(not(feature = "sync"), feature = "with-async-std"),
7301 async_std::test
7302 )
7303 )]
7304 async fn test_presign_delete() {
7305 let s3_path = "/test/test.file";
7306 let bucket = test_presign_bucket();
7307
7308 let url = bucket.presign_delete(s3_path, 86400).await.unwrap();
7309 assert!(url.contains("/test/test.file?"))
7310 }
7311
7312 #[maybe_async::test(
7313 feature = "sync",
7314 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7315 async(
7316 all(not(feature = "sync"), feature = "with-async-std"),
7317 async_std::test
7318 )
7319 )]
7320 async fn test_presign_url_standard_ports() {
7321 let region_http_80 = Region::Custom {
7326 region: "eu-central-1".to_owned(),
7327 endpoint: "http://minio:80".to_owned(),
7328 };
7329 let credentials = Credentials::new(
7330 Some("test_access_key"),
7331 Some("test_secret_key"),
7332 None,
7333 None,
7334 None,
7335 )
7336 .unwrap();
7337 let bucket_http_80 = Bucket::new("test-bucket", region_http_80, credentials.clone())
7338 .unwrap()
7339 .with_path_style();
7340
7341 let presigned_url_80 = bucket_http_80
7342 .presign_get("/test.file", 3600, None)
7343 .await
7344 .unwrap();
7345 println!("Presigned URL with port 80: {}", presigned_url_80);
7346
7347 assert!(
7349 presigned_url_80.starts_with("http://minio:80/"),
7350 "URL must preserve port 80, got: {}",
7351 presigned_url_80
7352 );
7353
7354 let region_https_443 = Region::Custom {
7356 region: "eu-central-1".to_owned(),
7357 endpoint: "https://minio:443".to_owned(),
7358 };
7359 let bucket_https_443 = Bucket::new("test-bucket", region_https_443, credentials.clone())
7360 .unwrap()
7361 .with_path_style();
7362
7363 let presigned_url_443 = bucket_https_443
7364 .presign_get("/test.file", 3600, None)
7365 .await
7366 .unwrap();
7367 println!("Presigned URL with port 443: {}", presigned_url_443);
7368
7369 assert!(
7371 presigned_url_443.starts_with("https://minio:443/"),
7372 "URL must preserve port 443, got: {}",
7373 presigned_url_443
7374 );
7375
7376 let region_http_9000 = Region::Custom {
7378 region: "eu-central-1".to_owned(),
7379 endpoint: "http://minio:9000".to_owned(),
7380 };
7381 let bucket_http_9000 = Bucket::new("test-bucket", region_http_9000, credentials)
7382 .unwrap()
7383 .with_path_style();
7384
7385 let presigned_url_9000 = bucket_http_9000
7386 .presign_get("/test.file", 3600, None)
7387 .await
7388 .unwrap();
7389 assert!(
7390 presigned_url_9000.contains("minio:9000"),
7391 "Non-standard port should be preserved in URL"
7392 );
7393 }
7394
7395 #[maybe_async::test(
7396 feature = "sync",
7397 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7398 async(
7399 all(not(feature = "sync"), feature = "with-async-std"),
7400 async_std::test
7401 )
7402 )]
7403 #[ignore]
7404 async fn test_bucket_create_delete_default_region() {
7405 let config = BucketConfiguration::default();
7406 let response = Bucket::create(
7407 &isolated_test_bucket_name(),
7408 "us-east-1".parse().unwrap(),
7409 test_aws_credentials(),
7410 config,
7411 )
7412 .await
7413 .unwrap();
7414
7415 let bucket = response.bucket;
7416 let result = check_status(response.response_code, 200, "create bucket").and_then(|()| {
7417 if response.response_text.is_empty() {
7418 Ok(())
7419 } else {
7420 Err(format!(
7421 "create bucket response body was {:?}",
7422 response.response_text
7423 ))
7424 }
7425 });
7426 if let Err(error) = result {
7427 report_bucket_cleanup(&bucket, "test_bucket_create_delete_default_region").await;
7428 panic!("{error}");
7429 }
7430 let response_code = bucket.delete().await.unwrap();
7431 assert!(response_code < 300);
7432 }
7433
7434 #[ignore]
7435 #[maybe_async::test(
7436 feature = "sync",
7437 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7438 async(
7439 all(not(feature = "sync"), feature = "with-async-std"),
7440 async_std::test
7441 )
7442 )]
7443 async fn test_bucket_create_delete_non_default_region() {
7444 let config = BucketConfiguration::default();
7445 let response = Bucket::create(
7446 &isolated_test_bucket_name(),
7447 "eu-central-1".parse().unwrap(),
7448 test_aws_credentials(),
7449 config,
7450 )
7451 .await
7452 .unwrap();
7453
7454 let bucket = response.bucket;
7455 let result = check_status(response.response_code, 200, "create bucket").and_then(|()| {
7456 if response.response_text.is_empty() {
7457 Ok(())
7458 } else {
7459 Err(format!(
7460 "create bucket response body was {:?}",
7461 response.response_text
7462 ))
7463 }
7464 });
7465 if let Err(error) = result {
7466 report_bucket_cleanup(&bucket, "test_bucket_create_delete_non_default_region").await;
7467 panic!("{error}");
7468 }
7469 let response_code = bucket.delete().await.unwrap();
7470 assert!(response_code < 300);
7471 }
7472
7473 #[ignore]
7474 #[maybe_async::test(
7475 feature = "sync",
7476 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7477 async(
7478 all(not(feature = "sync"), feature = "with-async-std"),
7479 async_std::test
7480 )
7481 )]
7482 async fn test_bucket_create_delete_non_default_region_public() {
7483 let config = BucketConfiguration::public();
7484 let response = Bucket::create(
7485 &isolated_test_bucket_name(),
7486 "eu-central-1".parse().unwrap(),
7487 test_aws_credentials(),
7488 config,
7489 )
7490 .await
7491 .unwrap();
7492
7493 let bucket = response.bucket;
7494 let result = check_status(response.response_code, 200, "create bucket").and_then(|()| {
7495 if response.response_text.is_empty() {
7496 Ok(())
7497 } else {
7498 Err(format!(
7499 "create bucket response body was {:?}",
7500 response.response_text
7501 ))
7502 }
7503 });
7504 if let Err(error) = result {
7505 report_bucket_cleanup(
7506 &bucket,
7507 "test_bucket_create_delete_non_default_region_public",
7508 )
7509 .await;
7510 panic!("{error}");
7511 }
7512 let response_code = bucket.delete().await.unwrap();
7513 assert!(response_code < 300);
7514 }
7515
7516 #[test]
7517 fn test_tag_has_key_and_value_functions() {
7518 let key = "key".to_owned();
7519 let value = "value".to_owned();
7520 let tag = Tag { key, value };
7521 assert_eq!["key", tag.key()];
7522 assert_eq!["value", tag.value()];
7523 }
7524
7525 #[test]
7526 #[ignore]
7527 fn test_builder_composition() {
7528 use std::time::Duration;
7529
7530 let bucket = Bucket::new(
7531 "test-bucket",
7532 "eu-central-1".parse().unwrap(),
7533 test_aws_credentials(),
7534 )
7535 .unwrap()
7536 .with_request_timeout(Duration::from_secs(10))
7537 .unwrap();
7538
7539 assert_eq!(bucket.request_timeout(), Some(Duration::from_secs(10)));
7540 }
7541
7542 #[cfg(all(
7543 feature = "with-tokio",
7544 any(
7545 feature = "tokio-native-tls",
7546 feature = "tokio-rustls-tls",
7547 feature = "tokio-rustls-tls-ring"
7548 )
7549 ))]
7550 #[test]
7551 fn with_request_timeout_preserves_tokio_client_options() {
7552 let bucket = Bucket::new(
7553 "test-bucket",
7554 "us-east-1".parse().unwrap(),
7555 Credentials::anonymous().unwrap(),
7556 )
7557 .unwrap()
7558 .set_dangerous_config(true, true)
7559 .unwrap()
7560 .set_proxy(reqwest::Proxy::all("http://127.0.0.1:1234").unwrap())
7561 .unwrap();
7562
7563 let updated = bucket
7564 .with_request_timeout(Duration::from_millis(125))
7565 .unwrap();
7566
7567 assert_eq!(updated.request_timeout(), Some(Duration::from_millis(125)));
7568 assert!(updated.client_options.proxy.is_some());
7569 assert!(updated.client_options.accept_invalid_certs);
7570 assert!(updated.client_options.accept_invalid_hostnames);
7571 }
7572
7573 #[maybe_async::test(
7574 feature = "sync",
7575 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7576 async(
7577 all(not(feature = "sync"), feature = "with-async-std"),
7578 async_std::test
7579 )
7580 )]
7581 #[ignore]
7582 async fn test_bucket_cors() {
7583 let expected_bucket_owner = std::env::var("RUST_S3_TEST_EXPECTED_BUCKET_OWNER")
7584 .expect("set RUST_S3_TEST_EXPECTED_BUCKET_OWNER to the account owning the test bucket");
7585 let created = Bucket::create(
7586 &isolated_test_bucket_name(),
7587 "eu-central-1".parse().unwrap(),
7588 test_aws_credentials(),
7589 BucketConfiguration::default(),
7590 )
7591 .await
7592 .unwrap();
7593 let bucket = created.bucket;
7594 let rule = CorsRule::new(
7595 None,
7596 vec!["GET".to_string()],
7597 vec!["*".to_string()],
7598 None,
7599 None,
7600 None,
7601 );
7602 let cors_config = CorsConfiguration::new(vec![rule]);
7603 let result: Result<(), String> = 'test: {
7604 if let Err(error) = check_status(created.response_code, 200, "create CORS test bucket")
7605 {
7606 break 'test Err(error);
7607 }
7608 let response = match bucket
7609 .put_bucket_cors(&expected_bucket_owner, &cors_config)
7610 .await
7611 {
7612 Ok(response) => response,
7613 Err(error) => break 'test Err(format!("put bucket CORS failed: {error}")),
7614 };
7615 if let Err(error) = check_status(response.status_code(), 200, "put bucket CORS") {
7616 break 'test Err(error);
7617 }
7618
7619 let cors_response = match bucket.get_bucket_cors(&expected_bucket_owner).await {
7620 Ok(response) => response,
7621 Err(error) => break 'test Err(format!("get bucket CORS failed: {error}")),
7622 };
7623 if cors_response != cors_config {
7624 break 'test Err("get bucket CORS returned a different configuration".to_owned());
7625 }
7626
7627 let response = match bucket.delete_bucket_cors(&expected_bucket_owner).await {
7628 Ok(response) => response,
7629 Err(error) => break 'test Err(format!("delete bucket CORS failed: {error}")),
7630 };
7631 check_status(response.status_code(), 204, "delete bucket CORS")
7632 };
7633 if let Err(error) = result {
7634 match bucket.delete_bucket_cors(&expected_bucket_owner).await {
7635 Ok(response) if response.status_code() == 204 => {}
7636 Ok(response) => eprintln!(
7637 "test_bucket_cors: CORS cleanup returned HTTP {}",
7638 response.status_code()
7639 ),
7640 Err(cleanup_error) => {
7641 eprintln!("test_bucket_cors: CORS cleanup failed: {cleanup_error}")
7642 }
7643 }
7644 report_bucket_cleanup(&bucket, "test_bucket_cors").await;
7645 panic!("{error}");
7646 }
7647 let response_code = bucket.delete().await.unwrap();
7648 assert!(response_code < 300);
7649 }
7650
7651 #[maybe_async::test(
7652 feature = "sync",
7653 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7654 async(
7655 all(not(feature = "sync"), feature = "with-async-std"),
7656 async_std::test
7657 )
7658 )]
7659 #[ignore]
7660 async fn test_bucket_lifecycle() {
7661 let created = Bucket::create(
7662 &isolated_test_bucket_name(),
7663 "eu-central-1".parse().unwrap(),
7664 test_aws_credentials(),
7665 BucketConfiguration::default(),
7666 )
7667 .await
7668 .unwrap();
7669 let bucket = created.bucket;
7670
7671 let rule = LifecycleRule::builder("Enabled")
7673 .id("test-rule")
7674 .filter(LifecycleFilter {
7675 prefix: Some("test/".to_string()),
7676 ..Default::default()
7677 })
7678 .expiration(Expiration {
7679 days: Some(1),
7680 ..Default::default()
7681 })
7682 .build();
7683
7684 let lifecycle_config = BucketLifecycleConfiguration::new(vec![rule]);
7685
7686 let result: Result<(), String> = 'test: {
7687 if let Err(error) =
7688 check_status(created.response_code, 200, "create lifecycle test bucket")
7689 {
7690 break 'test Err(error);
7691 }
7692 let response = match bucket.put_bucket_lifecycle(lifecycle_config.clone()).await {
7693 Ok(response) => response,
7694 Err(error) => break 'test Err(format!("put bucket lifecycle failed: {error}")),
7695 };
7696 if let Err(error) = check_status(response.status_code(), 200, "put bucket lifecycle") {
7697 break 'test Err(error);
7698 }
7699
7700 let retrieved_config = match bucket.get_bucket_lifecycle().await {
7701 Ok(config) => config,
7702 Err(error) => break 'test Err(format!("get bucket lifecycle failed: {error}")),
7703 };
7704 if retrieved_config.rules.len() != 1
7705 || retrieved_config.rules[0].id != Some("test-rule".to_string())
7706 || retrieved_config.rules[0].status != "Enabled"
7707 {
7708 break 'test Err(format!(
7709 "get bucket lifecycle returned unexpected rules: {retrieved_config:?}"
7710 ));
7711 }
7712
7713 let response = match bucket.delete_bucket_lifecycle().await {
7714 Ok(response) => response,
7715 Err(error) => break 'test Err(format!("delete bucket lifecycle failed: {error}")),
7716 };
7717 check_status(response.status_code(), 204, "delete bucket lifecycle")
7718 };
7719 if let Err(error) = result {
7720 match bucket.delete_bucket_lifecycle().await {
7721 Ok(response) if response.status_code() == 204 => {}
7722 Ok(response) => eprintln!(
7723 "test_bucket_lifecycle: lifecycle cleanup returned HTTP {}",
7724 response.status_code()
7725 ),
7726 Err(cleanup_error) => {
7727 eprintln!("test_bucket_lifecycle: lifecycle cleanup failed: {cleanup_error}")
7728 }
7729 }
7730 report_bucket_cleanup(&bucket, "test_bucket_lifecycle").await;
7731 panic!("{error}");
7732 }
7733 let response_code = bucket.delete().await.unwrap();
7734 assert!(response_code < 300);
7735 }
7736
7737 #[ignore]
7738 #[cfg(any(
7739 feature = "tokio-native-tls",
7740 feature = "tokio-rustls-tls",
7741 feature = "tokio-rustls-tls-ring"
7742 ))]
7743 #[maybe_async::test(
7744 feature = "sync",
7745 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7746 async(
7747 all(not(feature = "sync"), feature = "with-async-std"),
7748 async_std::test
7749 )
7750 )]
7751 async fn test_bucket_exists_with_dangerous_config() {
7752 init();
7753
7754 let credentials = test_aws_credentials();
7762 let region = "eu-central-1".parse().unwrap();
7763 let bucket_name = "rust-s3-test";
7764
7765 let bucket = Bucket::new(bucket_name, region, credentials)
7767 .unwrap()
7768 .with_path_style();
7769
7770 let bucket = bucket.set_dangerous_config(true, true).unwrap();
7772
7773 let exists_result = bucket.exists().await;
7776
7777 assert!(
7779 exists_result.is_ok(),
7780 "Bucket::exists() failed with dangerous config"
7781 );
7782 let exists = exists_result.unwrap();
7783 assert!(exists, "Test bucket should exist");
7784
7785 let list_result = bucket.list("".to_string(), Some("/".to_string())).await;
7788 assert!(
7789 list_result.is_ok(),
7790 "List operation should work with dangerous config"
7791 );
7792 }
7793
7794 #[ignore]
7795 #[maybe_async::test(
7796 feature = "sync",
7797 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7798 async(
7799 all(not(feature = "sync"), feature = "with-async-std"),
7800 async_std::test
7801 )
7802 )]
7803 async fn test_bucket_exists_without_dangerous_config() {
7804 init();
7805
7806 let credentials = test_aws_credentials();
7808 let region = "eu-central-1".parse().unwrap();
7809 let bucket_name = "rust-s3-test";
7810
7811 let bucket = Bucket::new(bucket_name, region, credentials)
7813 .unwrap()
7814 .with_path_style();
7815
7816 let exists_result = bucket.exists().await;
7818 assert!(
7819 exists_result.is_ok(),
7820 "Bucket::exists() should work without dangerous config"
7821 );
7822 let exists = exists_result.unwrap();
7823 assert!(exists, "Test bucket should exist");
7824 }
7825
7826 #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
7827 #[maybe_async::test(
7828 feature = "sync",
7829 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7830 async(
7831 all(not(feature = "sync"), feature = "with-async-std"),
7832 async_std::test
7833 )
7834 )]
7835 async fn credentials_refresh_is_singleflight_and_does_not_hold_read_lock() {
7836 use std::sync::Arc;
7837 use std::sync::atomic::{AtomicUsize, Ordering};
7838 use std::sync::{Mutex, mpsc};
7839 use std::time::Duration;
7840
7841 let mut initial =
7842 Credentials::new(Some("old-access"), Some("old-secret"), None, None, None).unwrap();
7843 initial.expiration = Some(time::OffsetDateTime::from_unix_timestamp(0).unwrap().into());
7844 let bucket = Bucket::new("test-bucket", "us-east-1".parse().unwrap(), initial).unwrap();
7845 let cloned = (*bucket).clone();
7846
7847 let refresh_count = Arc::new(AtomicUsize::new(0));
7848 let (started_sender, started_receiver) = async_std::channel::bounded::<()>(1);
7849 let (release_sender, release_receiver) = mpsc::channel();
7850 let release_receiver = Arc::new(Mutex::new(release_receiver));
7851
7852 let first_count = refresh_count.clone();
7853 let first_started = started_sender.clone();
7854 let first_release = release_receiver.clone();
7855 let first_refresh = bucket.credentials_refresh_with(move |mut credentials| {
7856 first_count.fetch_add(1, Ordering::SeqCst);
7857 let _ = first_started.try_send(());
7858 first_release
7859 .lock()
7860 .unwrap()
7861 .recv_timeout(Duration::from_secs(5))
7862 .expect("test should release the blocked refresh");
7863 credentials.access_key = Some("refreshed-access".into());
7864 credentials.expiration =
7865 Some((time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).into());
7866 Ok(credentials)
7867 });
7868
7869 let second_count = refresh_count.clone();
7870 let second_started = started_sender;
7871 let second_release = release_receiver;
7872 let second_refresh = cloned.credentials_refresh_with(move |mut credentials| {
7873 second_count.fetch_add(1, Ordering::SeqCst);
7874 let _ = second_started.try_send(());
7875 second_release
7876 .lock()
7877 .unwrap()
7878 .recv_timeout(Duration::from_secs(5))
7879 .expect("test should release the blocked refresh");
7880 credentials.access_key = Some("refreshed-access".into());
7881 credentials.expiration =
7882 Some((time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).into());
7883 Ok(credentials)
7884 });
7885
7886 let read_bucket = (*bucket).clone();
7887 let read_during_refresh = async move {
7888 started_receiver.recv().await.unwrap();
7889 let credentials =
7890 async_std::future::timeout(Duration::from_secs(2), read_bucket.credentials())
7891 .await
7892 .expect("credential reads should not wait for the blocking refresh")
7893 .unwrap();
7894 assert_eq!(credentials.access_key.as_deref(), Some("old-access"));
7895 release_sender.send(()).unwrap();
7896 release_sender.send(()).unwrap();
7897 };
7898
7899 let refreshes = futures_util::future::join(first_refresh, second_refresh);
7900 let (refresh_results, ()) = async_std::future::timeout(
7901 Duration::from_secs(8),
7902 futures_util::future::join(refreshes, read_during_refresh),
7903 )
7904 .await
7905 .expect("refresh coordination should finish within the test deadline");
7906 refresh_results.0.unwrap();
7907 refresh_results.1.unwrap();
7908
7909 assert_eq!(refresh_count.load(Ordering::SeqCst), 1);
7910 assert_eq!(
7911 bucket.credentials().await.unwrap().access_key.as_deref(),
7912 Some("refreshed-access")
7913 );
7914 }
7915
7916 #[derive(Clone, Copy)]
7917 enum BucketPolicyOperation {
7918 Get,
7919 Put,
7920 Delete,
7921 #[cfg(feature = "blocking")]
7922 BlockingGet,
7923 }
7924
7925 struct BucketPolicyWireRequest {
7926 method: String,
7927 target: String,
7928 headers: HashMap<String, String>,
7929 body: Vec<u8>,
7930 }
7931
7932 #[maybe_async::maybe_async]
7933 async fn run_bucket_policy_wire_case(
7934 operation: BucketPolicyOperation,
7935 policy: &str,
7936 response_status: u16,
7937 response_body: &str,
7938 ) -> (
7939 Result<ResponseData, S3Error>,
7940 Result<BucketPolicyWireRequest, String>,
7941 ) {
7942 use std::io::{Read, Write};
7943 use std::net::TcpListener;
7944 use std::thread;
7945 use std::time::{Duration, Instant};
7946
7947 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
7948 listener.set_nonblocking(true).unwrap();
7949 let endpoint = format!("http://{}", listener.local_addr().unwrap());
7950 let body = response_body.as_bytes().to_vec();
7951 let server = thread::spawn(move || -> Result<BucketPolicyWireRequest, String> {
7952 let deadline = Instant::now() + Duration::from_secs(10);
7953 let (mut stream, _) = loop {
7954 match listener.accept() {
7955 Ok((stream, _)) => break (stream, ()),
7956 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
7957 if Instant::now() >= deadline {
7958 return Err("bucket policy request accept deadline".to_owned());
7959 }
7960 thread::sleep(Duration::from_millis(2));
7961 }
7962 Err(error) => return Err(format!("accept policy request: {error}")),
7963 }
7964 };
7965 stream.set_nonblocking(false).map_err(|e| e.to_string())?;
7966 stream
7967 .set_read_timeout(Some(Duration::from_secs(5)))
7968 .map_err(|e| e.to_string())?;
7969 stream
7970 .set_write_timeout(Some(Duration::from_secs(5)))
7971 .map_err(|e| e.to_string())?;
7972
7973 let mut raw_headers = Vec::new();
7974 let mut byte = [0u8; 1];
7975 while !raw_headers.ends_with(b"\r\n\r\n") {
7976 if raw_headers.len() >= 16 * 1024 || Instant::now() >= deadline {
7977 return Err("bucket policy request headers exceeded bounds".to_owned());
7978 }
7979 stream.read_exact(&mut byte).map_err(|e| e.to_string())?;
7980 raw_headers.push(byte[0]);
7981 }
7982 let header_text = String::from_utf8_lossy(&raw_headers);
7983 let mut lines = header_text.lines();
7984 let request_line = lines.next().unwrap_or_default();
7985 let mut fields = HashMap::new();
7986 for line in lines {
7987 if let Some((name, value)) = line.split_once(':') {
7988 fields.insert(name.trim().to_ascii_lowercase(), value.trim().to_owned());
7989 }
7990 }
7991 let content_length = fields
7992 .get("content-length")
7993 .and_then(|value| value.parse::<usize>().ok())
7994 .unwrap_or(0);
7995 if content_length > 16 * 1024 {
7996 return Err("bucket policy request body exceeded bound".to_owned());
7997 }
7998 let mut request_body = vec![0; content_length];
7999 stream
8000 .read_exact(&mut request_body)
8001 .map_err(|e| e.to_string())?;
8002
8003 let reason = match response_status {
8004 403 => "Forbidden",
8005 404 => "Not Found",
8006 204 => "No Content",
8007 _ => "OK",
8008 };
8009 write!(
8010 stream,
8011 "HTTP/1.1 {response_status} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
8012 body.len()
8013 )
8014 .map_err(|e| e.to_string())?;
8015 stream.write_all(&body).map_err(|e| e.to_string())?;
8016
8017 let mut first = request_line.split_whitespace();
8018 Ok(BucketPolicyWireRequest {
8019 method: first.next().unwrap_or_default().to_owned(),
8020 target: first.next().unwrap_or_default().to_owned(),
8021 headers: fields,
8022 body: request_body,
8023 })
8024 });
8025
8026 let credentials = Credentials::new(
8027 Some("test-access-key"),
8028 Some("test-secret-key"),
8029 None,
8030 None,
8031 None,
8032 )
8033 .unwrap();
8034 let bucket = Bucket::new(
8035 "test-bucket",
8036 Region::Custom {
8037 region: "us-east-1".to_owned(),
8038 endpoint,
8039 },
8040 credentials,
8041 )
8042 .unwrap()
8043 .with_path_style()
8044 .with_request_timeout(Duration::from_secs(5))
8045 .unwrap();
8046
8047 let result = match operation {
8048 BucketPolicyOperation::Get => bucket.get_bucket_policy().await,
8049 BucketPolicyOperation::Put => bucket.put_bucket_policy(policy).await,
8050 BucketPolicyOperation::Delete => bucket.delete_bucket_policy().await,
8051 #[cfg(feature = "blocking")]
8052 BucketPolicyOperation::BlockingGet => {
8053 let bucket = bucket.clone();
8054 thread::spawn(move || bucket.get_bucket_policy_blocking())
8055 .join()
8056 .expect("blocking bucket policy call panicked")
8057 }
8058 };
8059 let captured = server.join().expect("bucket policy server panicked");
8060 (result, captured)
8061 }
8062
8063 #[maybe_async::test(
8064 feature = "sync",
8065 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
8066 async(
8067 all(not(feature = "sync"), feature = "with-async-std"),
8068 async_std::test
8069 )
8070 )]
8071 async fn bucket_policy_wire_contract_and_errors() {
8072 use base64::Engine as _;
8073 use sha2::Digest as _;
8074
8075 let policy = r#"{"Version":"2012-10-17","Note":"café"}"#;
8076 let json_body = r#"{"Version":"2012-10-17","Statement":[]}"#;
8077 let missing_body = "<Error><Code>NoSuchBucketPolicy</Code></Error>";
8078 let forbidden_body = "<Error><Code>AccessDenied</Code></Error>";
8079
8080 let (get, capture) =
8081 run_bucket_policy_wire_case(BucketPolicyOperation::Get, "", 200, json_body).await;
8082 let get = get.unwrap();
8083 let capture = capture.unwrap();
8084 assert_eq!(get.status_code(), 200);
8085 assert_eq!(get.as_slice(), json_body.as_bytes());
8086 assert_eq!(capture.method, "GET");
8087 assert_eq!(capture.target, "/test-bucket/?policy");
8088 assert!(capture.body.is_empty());
8089 assert_eq!(
8090 capture
8091 .headers
8092 .get("x-amz-content-sha256")
8093 .map(String::as_str),
8094 Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
8095 );
8096 assert!(!capture.headers.contains_key("content-md5"));
8097 let get_authorization = capture.headers.get("authorization").unwrap();
8098 assert!(!get_authorization.contains("content-length"));
8099 assert!(!get_authorization.contains("content-type"));
8100 assert!(capture.headers.contains_key("authorization"));
8101
8102 let (put, capture) =
8103 run_bucket_policy_wire_case(BucketPolicyOperation::Put, policy, 204, "").await;
8104 assert_eq!(put.unwrap().status_code(), 204);
8105 let capture = capture.unwrap();
8106 assert_eq!(capture.method, "PUT");
8107 assert_eq!(capture.target, "/test-bucket/?policy");
8108 assert_eq!(capture.body, policy.as_bytes());
8109 assert_eq!(
8110 capture.headers.get("content-length").map(String::as_str),
8111 Some(policy.len().to_string().as_str())
8112 );
8113 assert_eq!(
8114 capture.headers.get("content-type").map(String::as_str),
8115 Some("application/json")
8116 );
8117 assert_eq!(
8118 capture.headers.get("content-md5").map(String::as_str),
8119 Some(
8120 base64::engine::general_purpose::STANDARD
8121 .encode(md5::compute(policy.as_bytes()).0)
8122 .as_str()
8123 )
8124 );
8125 let payload_hash = hex::encode(sha2::Sha256::digest(policy.as_bytes()));
8126 assert_eq!(
8127 capture.headers.get("x-amz-content-sha256"),
8128 Some(&payload_hash)
8129 );
8130 let authorization = capture.headers.get("authorization").unwrap();
8131 assert!(authorization.starts_with("AWS4-HMAC-SHA256 Credential=test-access-key/"));
8132 assert!(
8133 authorization.contains(
8134 "SignedHeaders=content-length;content-md5;content-type;host;x-amz-content-sha256;x-amz-date"
8135 ),
8136 "unexpected SigV4 signed headers: {authorization}"
8137 );
8138
8139 let (delete, capture) =
8140 run_bucket_policy_wire_case(BucketPolicyOperation::Delete, "", 204, "").await;
8141 assert_eq!(delete.unwrap().status_code(), 204);
8142 let capture = capture.unwrap();
8143 assert_eq!(capture.method, "DELETE");
8144 assert_eq!(capture.target, "/test-bucket/?policy");
8145 assert!(capture.body.is_empty());
8146 assert_eq!(
8147 capture
8148 .headers
8149 .get("x-amz-content-sha256")
8150 .map(String::as_str),
8151 Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
8152 );
8153 assert!(!capture.headers.contains_key("content-md5"));
8154 let delete_authorization = capture.headers.get("authorization").unwrap();
8155 assert!(!delete_authorization.contains("content-length"));
8156 assert!(!delete_authorization.contains("content-type"));
8157
8158 let (missing, capture) =
8159 run_bucket_policy_wire_case(BucketPolicyOperation::Get, "", 404, missing_body).await;
8160 if cfg!(feature = "fail-on-err") {
8161 assert!(
8162 matches!(missing, Err(S3Error::HttpFailWithBody(404, body)) if body.contains("NoSuchBucketPolicy"))
8163 );
8164 } else {
8165 let missing = missing.unwrap();
8166 assert_eq!(missing.status_code(), 404);
8167 assert!(missing.as_str().unwrap().contains("NoSuchBucketPolicy"));
8168 }
8169 assert_eq!(capture.unwrap().target, "/test-bucket/?policy");
8170
8171 let (forbidden, _) =
8172 run_bucket_policy_wire_case(BucketPolicyOperation::Get, "", 403, forbidden_body).await;
8173 if cfg!(feature = "fail-on-err") {
8174 assert!(
8175 matches!(forbidden, Err(S3Error::HttpFailWithBody(403, body)) if body.contains("AccessDenied"))
8176 );
8177 } else {
8178 let forbidden = forbidden.unwrap();
8179 assert_eq!(forbidden.status_code(), 403);
8180 assert!(forbidden.as_str().unwrap().contains("AccessDenied"));
8181 }
8182
8183 #[cfg(feature = "blocking")]
8184 {
8185 let (blocking, capture) =
8186 run_bucket_policy_wire_case(BucketPolicyOperation::BlockingGet, "", 200, json_body)
8187 .await;
8188 assert_eq!(blocking.unwrap().status_code(), 200);
8189 assert_eq!(capture.unwrap().target, "/test-bucket/?policy");
8190 }
8191 }
8192
8193 #[maybe_async::test(
8194 feature = "sync",
8195 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
8196 async(
8197 all(not(feature = "sync"), feature = "with-async-std"),
8198 async_std::test
8199 )
8200 )]
8201 async fn bucket_policy_virtual_host_signing_uses_policy_query() {
8202 use super::RequestImpl;
8203 use crate::command::Command;
8204 use crate::request::Request as _;
8205 use http::header::{AUTHORIZATION, HOST};
8206
8207 let credentials = Credentials::new(
8208 Some("test-access-key"),
8209 Some("test-secret-key"),
8210 None,
8211 None,
8212 None,
8213 )
8214 .unwrap();
8215 let bucket = Bucket::new(
8216 "test-bucket",
8217 Region::Custom {
8218 region: "us-east-1".to_owned(),
8219 endpoint: "http://example.test:9000/base".to_owned(),
8220 },
8221 credentials,
8222 )
8223 .unwrap();
8224 let request = RequestImpl::new(&bucket, "", Command::GetBucketPolicy)
8225 .await
8226 .unwrap();
8227 let headers = request.headers().await.unwrap();
8228 assert_eq!(headers.get(HOST).unwrap(), "test-bucket.example.test:9000");
8229 let authorization = headers.get(AUTHORIZATION).unwrap().to_str().unwrap();
8230 assert!(authorization.starts_with("AWS4-HMAC-SHA256 Credential=test-access-key/"));
8231 assert!(authorization.contains("SignedHeaders=host;x-amz-content-sha256;x-amz-date"));
8232
8233 let canonical = request.canonical_request(&headers).unwrap();
8234 assert!(canonical.contains("GET\n/base/\npolicy=\n"), "{canonical}");
8235 }
8236
8237 #[ignore = "requires a disposable pre-created bucket and explicit provider credentials"]
8238 #[maybe_async::test(
8239 feature = "sync",
8240 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
8241 async(
8242 all(not(feature = "sync"), feature = "with-async-std"),
8243 async_std::test
8244 )
8245 )]
8246 async fn bucket_policy_provider_roundtrip() {
8247 let bucket_name = env::var("RUST_S3_POLICY_BUCKET")
8248 .expect("RUST_S3_POLICY_BUCKET must name a disposable, pre-created bucket");
8249 let region = env::var("RUST_S3_POLICY_REGION").expect("RUST_S3_POLICY_REGION is required");
8250 let endpoint =
8251 env::var("RUST_S3_POLICY_ENDPOINT").expect("RUST_S3_POLICY_ENDPOINT is required");
8252 let access_key =
8253 env::var("RUST_S3_POLICY_ACCESS_KEY").expect("RUST_S3_POLICY_ACCESS_KEY is required");
8254 let secret_key =
8255 env::var("RUST_S3_POLICY_SECRET_KEY").expect("RUST_S3_POLICY_SECRET_KEY is required");
8256 let policy_json = env::var("RUST_S3_POLICY_JSON").expect("RUST_S3_POLICY_JSON is required");
8257 let expected: serde_json::Value = serde_json::from_str(&policy_json)
8258 .expect("RUST_S3_POLICY_JSON must contain valid JSON");
8259 let credentials =
8260 Credentials::new(Some(&access_key), Some(&secret_key), None, None, None).unwrap();
8261 let mut bucket = Bucket::new(
8262 &bucket_name,
8263 Region::Custom { region, endpoint },
8264 credentials,
8265 )
8266 .unwrap()
8267 .with_path_style();
8268 if let Ok(owner) = env::var("RUST_S3_POLICY_EXPECTED_OWNER") {
8269 bucket.add_header("x-amz-expected-bucket-owner", &owner);
8270 }
8271
8272 let initial = bucket.get_bucket_policy().await;
8273 if cfg!(feature = "fail-on-err") {
8274 assert!(
8275 matches!(initial, Err(S3Error::HttpFailWithBody(404, body)) if body.contains("NoSuchBucketPolicy"))
8276 );
8277 } else {
8278 let initial = initial.unwrap();
8279 assert_eq!(initial.status_code(), 404);
8280 assert!(initial.as_str().unwrap().contains("NoSuchBucketPolicy"));
8281 }
8282
8283 let put = bucket.put_bucket_policy(&policy_json).await.unwrap();
8284 assert!(matches!(put.status_code(), 200 | 204));
8285 let fetched = bucket.get_bucket_policy().await.unwrap();
8286 assert_eq!(fetched.status_code(), 200);
8287 let actual: serde_json::Value = serde_json::from_slice(fetched.as_slice()).unwrap();
8288 assert_eq!(actual, expected);
8289
8290 let deleted = bucket.delete_bucket_policy().await.unwrap();
8291 assert!(matches!(deleted.status_code(), 200 | 204));
8292 let missing = bucket.get_bucket_policy().await;
8293 if cfg!(feature = "fail-on-err") {
8294 assert!(
8295 matches!(missing, Err(S3Error::HttpFailWithBody(404, body)) if body.contains("NoSuchBucketPolicy"))
8296 );
8297 } else {
8298 let missing = missing.unwrap();
8299 assert_eq!(missing.status_code(), 404);
8300 assert!(missing.as_str().unwrap().contains("NoSuchBucketPolicy"));
8301 }
8302 }
8303}