1use async_trait::async_trait;
35use futures_util::stream::BoxStream;
36use futures_util::{StreamExt, TryStreamExt};
37use http::header::{HeaderName, IF_MATCH, IF_NONE_MATCH};
38use http::{Method, StatusCode};
39use std::{sync::Arc, time::Duration};
40use url::Url;
41
42use crate::aws::client::{CompleteMultipartMode, PutPartPayload, RequestError, S3Client};
43use crate::client::CredentialProvider;
44use crate::client::get::GetClientExt;
45use crate::client::list::{ListClient, ListClientExt};
46use crate::multipart::{MultipartStore, PartId};
47use crate::signer::Signer;
48use crate::util::STRICT_ENCODE_SET;
49use crate::{
50 CopyMode, CopyOptions, Error, GetOptions, GetResult, ListResult, MultipartId, MultipartUpload,
51 ObjectMeta, ObjectStore, Path, PutMode, PutMultipartOptions, PutOptions, PutPayload, PutResult,
52 Result, UploadPart,
53};
54
55static TAGS_HEADER: HeaderName = HeaderName::from_static("x-amz-tagging");
56static COPY_SOURCE_HEADER: HeaderName = HeaderName::from_static("x-amz-copy-source");
57
58mod builder;
59mod checksum;
60mod client;
61mod credential;
62mod precondition;
63
64#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
65mod resolve;
66
67pub use builder::{AmazonS3Builder, AmazonS3ConfigKey};
68pub use checksum::Checksum;
69pub use precondition::{S3ConditionalPut, S3CopyIfNotExists};
70
71#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
72pub use resolve::resolve_bucket_region;
73
74const STRICT_PATH_ENCODE_SET: percent_encoding::AsciiSet = STRICT_ENCODE_SET.remove(b'/');
76
77const STORE: &str = "S3";
78
79pub type AwsCredentialProvider = Arc<dyn CredentialProvider<Credential = AwsCredential>>;
81use crate::client::parts::Parts;
82use crate::list::{PaginatedListOptions, PaginatedListResult, PaginatedListStore};
83pub use credential::{AwsAuthorizer, AwsCredential};
84
85#[derive(Debug, Clone)]
87pub struct AmazonS3 {
88 client: Arc<S3Client>,
89}
90
91impl std::fmt::Display for AmazonS3 {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "AmazonS3({})", self.client.config.bucket)
94 }
95}
96
97impl AmazonS3 {
98 pub fn credentials(&self) -> &AwsCredentialProvider {
100 &self.client.config.credentials
101 }
102
103 fn path_url(&self, path: &Path) -> String {
105 self.client.config.path_url(path)
106 }
107}
108
109#[async_trait]
110impl Signer for AmazonS3 {
111 async fn signed_url(&self, method: Method, path: &Path, expires_in: Duration) -> Result<Url> {
144 let crypto = self.client.config.crypto()?;
145 let credential = self.credentials().get_credential().await?;
146 let authorizer = AwsAuthorizer::new(&credential, "s3", &self.client.config.region)
147 .with_request_payer(self.client.config.request_payer)
148 .with_crypto(crypto);
149
150 let path_url = self.path_url(path);
151 let mut url = path_url.parse().map_err(|e| Error::Generic {
152 store: STORE,
153 source: format!("Unable to parse url {path_url}: {e}").into(),
154 })?;
155
156 authorizer.sign(method, &mut url, expires_in)?;
157
158 Ok(url)
159 }
160}
161
162#[async_trait]
163impl ObjectStore for AmazonS3 {
164 async fn put_opts(
165 &self,
166 location: &Path,
167 payload: PutPayload,
168 opts: PutOptions,
169 ) -> Result<PutResult> {
170 let PutOptions {
171 mode,
172 tags,
173 attributes,
174 extensions,
175 } = opts;
176
177 let request = self
178 .client
179 .request(Method::PUT, location)
180 .with_payload(payload)?
181 .with_attributes(attributes)
182 .with_tags(tags)
183 .with_extensions(extensions)
184 .with_encryption_headers();
185
186 match (mode, &self.client.config.conditional_put) {
187 (PutMode::Overwrite, _) => request.idempotent(true).do_put().await,
188 (PutMode::Create, S3ConditionalPut::Disabled) => Err(Error::NotImplemented {
189 operation:
190 "`put_opts` with mode `PutMode::Create` when conditional put is disabled".into(),
191 implementer: self.to_string(),
192 }),
193 (PutMode::Create, S3ConditionalPut::ETagMatch) => {
194 match request.header(&IF_NONE_MATCH, "*").do_put().await {
195 Err(e @ Error::NotModified { .. } | e @ Error::Precondition { .. }) => {
199 Err(Error::AlreadyExists {
200 path: location.to_string(),
201 source: Box::new(e),
202 })
203 }
204 r => r,
205 }
206 }
207 (PutMode::Update(v), put) => {
208 let etag = v.e_tag.ok_or_else(|| Error::Generic {
209 store: STORE,
210 source: "ETag required for conditional put".to_string().into(),
211 })?;
212 match put {
213 S3ConditionalPut::ETagMatch => {
214 match request
215 .header(&IF_MATCH, etag.as_str())
216 .retry_on_conflict(true)
221 .do_put()
222 .await
223 {
224 Err(Error::NotFound { path, source }) => {
229 Err(Error::Precondition { path, source })
230 }
231 r => r,
232 }
233 }
234 S3ConditionalPut::Disabled => Err(Error::NotImplemented {
235 operation:
236 "`put_opts` with mode `PutMode::Update` when conditional put is disabled"
237 .into(),
238 implementer: self.to_string(),
239 }),
240 }
241 }
242 }
243 }
244
245 async fn put_multipart_opts(
246 &self,
247 location: &Path,
248 opts: PutMultipartOptions,
249 ) -> Result<Box<dyn MultipartUpload>> {
250 let upload_id = self.client.create_multipart(location, opts).await?;
251
252 Ok(Box::new(S3MultiPartUpload {
253 part_idx: 0,
254 state: Arc::new(UploadState {
255 client: Arc::clone(&self.client),
256 location: location.clone(),
257 upload_id: upload_id.clone(),
258 parts: Default::default(),
259 }),
260 }))
261 }
262
263 async fn get_opts(&self, location: &Path, options: GetOptions) -> Result<GetResult> {
264 self.client.get_opts(location, options).await
265 }
266
267 fn delete_stream(
268 &self,
269 locations: BoxStream<'static, Result<Path>>,
270 ) -> BoxStream<'static, Result<Path>> {
271 let client = Arc::clone(&self.client);
272
273 if client.config.disable_bulk_delete {
278 return locations
279 .map(move |location| {
280 let client = Arc::clone(&client);
281 async move {
282 let location = location?;
283 client.delete_request(&location).await?;
284 Ok(location)
285 }
286 })
287 .buffered(20)
288 .boxed();
289 }
290
291 locations
292 .try_chunks(1_000)
293 .map(move |locations| {
294 let client = Arc::clone(&client);
295 async move {
296 let locations = locations.map_err(|e| e.1)?;
299 client
300 .bulk_delete_request(locations)
301 .await
302 .map(futures_util::stream::iter)
303 }
304 })
305 .buffered(20)
306 .try_flatten()
307 .boxed()
308 }
309
310 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
311 self.client.list(prefix)
312 }
313
314 fn list_with_offset(
315 &self,
316 prefix: Option<&Path>,
317 offset: &Path,
318 ) -> BoxStream<'static, Result<ObjectMeta>> {
319 if self.client.config.is_s3_express() {
320 let offset = offset.clone();
321 return self
323 .client
324 .list(prefix)
325 .try_filter(move |f| futures_util::future::ready(f.location > offset))
326 .boxed();
327 }
328
329 self.client.list_with_offset(prefix, offset)
330 }
331
332 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> Result<ListResult> {
333 self.client.list_with_delimiter(prefix).await
334 }
335
336 async fn copy_opts(&self, from: &Path, to: &Path, options: CopyOptions) -> Result<()> {
337 let CopyOptions {
338 mode,
339 extensions: _,
340 } = options;
341
342 match mode {
343 CopyMode::Overwrite => {
344 self.client
345 .copy_request(from, to)
346 .idempotent(true)
347 .send()
348 .await?;
349 Ok(())
350 }
351 CopyMode::Create => {
352 let (k, v, status) = match &self.client.config.copy_if_not_exists {
353 Some(S3CopyIfNotExists::Header(k, v)) => {
354 (k, v, StatusCode::PRECONDITION_FAILED)
355 }
356 Some(S3CopyIfNotExists::HeaderWithStatus(k, v, status)) => (k, v, *status),
357 Some(S3CopyIfNotExists::Multipart) => {
358 let upload_id = self
359 .client
360 .create_multipart(to, PutMultipartOptions::default())
361 .await?;
362
363 let res = async {
364 let part_id = self
365 .client
366 .put_part(to, &upload_id, 0, PutPartPayload::Copy(from))
367 .await?;
368 match self
369 .client
370 .complete_multipart(
371 to,
372 &upload_id,
373 vec![part_id],
374 CompleteMultipartMode::Create,
375 )
376 .await
377 {
378 Err(e @ Error::Precondition { .. }) => Err(Error::AlreadyExists {
379 path: to.to_string(),
380 source: Box::new(e),
381 }),
382 Ok(_) => Ok(()),
383 Err(e) => Err(e),
384 }
385 }
386 .await;
387
388 if res.is_err() {
393 let _ = self.client.abort_multipart(to, &upload_id).await;
394 }
395
396 return res;
397 }
398 None => {
399 return Err(Error::NotSupported {
400 source: "S3 does not support copy-if-not-exists".to_string().into(),
401 });
402 }
403 };
404
405 let req = self.client.copy_request(from, to);
406 match req.header(k, v).send().await {
407 Err(RequestError::Retry { source, path })
408 if source.status() == Some(status) =>
409 {
410 Err(Error::AlreadyExists {
411 source: Box::new(source),
412 path,
413 })
414 }
415 Err(e) => Err(e.into()),
416 Ok(_) => Ok(()),
417 }
418 }
419 }
420 }
421}
422
423#[derive(Debug)]
424struct S3MultiPartUpload {
425 part_idx: usize,
426 state: Arc<UploadState>,
427}
428
429#[derive(Debug)]
430struct UploadState {
431 parts: Parts,
432 location: Path,
433 upload_id: String,
434 client: Arc<S3Client>,
435}
436
437#[async_trait]
438impl MultipartUpload for S3MultiPartUpload {
439 fn put_part(&mut self, data: PutPayload) -> UploadPart {
440 let idx = self.part_idx;
441 self.part_idx += 1;
442 let state = Arc::clone(&self.state);
443 Box::pin(async move {
444 let part = state
445 .client
446 .put_part(
447 &state.location,
448 &state.upload_id,
449 idx,
450 PutPartPayload::Part(data),
451 )
452 .await?;
453 state.parts.put(idx, part);
454 Ok(())
455 })
456 }
457
458 async fn complete(&mut self) -> Result<PutResult> {
459 let parts = self.state.parts.finish(self.part_idx)?;
460
461 self.state
462 .client
463 .complete_multipart(
464 &self.state.location,
465 &self.state.upload_id,
466 parts,
467 CompleteMultipartMode::Overwrite,
468 )
469 .await
470 }
471
472 async fn abort(&mut self) -> Result<()> {
473 self.state
474 .client
475 .request(Method::DELETE, &self.state.location)
476 .query(&[("uploadId", &self.state.upload_id)])
477 .idempotent(true)
478 .send()
479 .await?;
480
481 Ok(())
482 }
483}
484
485#[async_trait]
486impl MultipartStore for AmazonS3 {
487 async fn create_multipart(&self, path: &Path) -> Result<MultipartId> {
488 self.client
489 .create_multipart(path, PutMultipartOptions::default())
490 .await
491 }
492
493 async fn create_multipart_opts(
494 &self,
495 path: &Path,
496 opts: PutMultipartOptions,
497 ) -> Result<MultipartId> {
498 self.client.create_multipart(path, opts).await
499 }
500
501 async fn put_part(
502 &self,
503 path: &Path,
504 id: &MultipartId,
505 part_idx: usize,
506 data: PutPayload,
507 ) -> Result<PartId> {
508 self.client
509 .put_part(path, id, part_idx, PutPartPayload::Part(data))
510 .await
511 }
512
513 async fn complete_multipart(
514 &self,
515 path: &Path,
516 id: &MultipartId,
517 parts: Vec<PartId>,
518 ) -> Result<PutResult> {
519 self.client
520 .complete_multipart(path, id, parts, CompleteMultipartMode::Overwrite)
521 .await
522 }
523
524 async fn abort_multipart(&self, path: &Path, id: &MultipartId) -> Result<()> {
525 self.client
526 .request(Method::DELETE, path)
527 .query(&[("uploadId", id)])
528 .send()
529 .await?;
530 Ok(())
531 }
532}
533
534#[async_trait]
535impl PaginatedListStore for AmazonS3 {
536 async fn list_paginated(
537 &self,
538 prefix: Option<&str>,
539 opts: PaginatedListOptions,
540 ) -> Result<PaginatedListResult> {
541 self.client.list_request(prefix, opts).await
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use crate::ClientOptions;
549 use crate::ObjectStoreExt;
550 #[cfg(feature = "reqwest")]
551 use crate::client::SpawnedReqwestConnector;
552 use crate::client::get::GetClient;
553 use crate::client::retry::RetryContext;
554 use crate::integration::*;
555 use crate::tests::*;
556 use base64::Engine;
557 use base64::prelude::BASE64_STANDARD;
558 use http::HeaderMap;
559
560 const NON_EXISTENT_NAME: &str = "nonexistentname";
561
562 #[tokio::test]
563 async fn write_multipart_file_with_signature() {
564 maybe_skip_integration!();
565
566 let bucket = "test-bucket-for-checksum";
567 for checksum in [Checksum::SHA256, Checksum::CRC64NVME] {
568 let store = AmazonS3Builder::from_env()
569 .with_bucket_name(bucket)
570 .with_checksum_algorithm(checksum)
571 .build()
572 .unwrap();
573
574 let str = "test.bin";
575 let path = Path::parse(str).unwrap();
576 let opts = PutMultipartOptions::default();
577 let mut upload = store.put_multipart_opts(&path, opts).await.unwrap();
578
579 upload
580 .put_part(PutPayload::from(vec![0u8; 10_000_000]))
581 .await
582 .unwrap();
583 upload
584 .put_part(PutPayload::from(vec![0u8; 5_000_000]))
585 .await
586 .unwrap();
587
588 let res = upload.complete().await.unwrap();
589 assert!(res.e_tag.is_some(), "Should have valid etag");
590
591 store.delete(&path).await.unwrap();
592 }
593 }
594
595 #[tokio::test]
596 async fn copy_multipart_file_with_signature() {
597 maybe_skip_integration!();
598
599 let bucket = "test-bucket-for-copy-if-not-exists";
600 for checksum in [Checksum::SHA256, Checksum::CRC64NVME] {
601 let store = AmazonS3Builder::from_env()
602 .with_bucket_name(bucket)
603 .with_checksum_algorithm(checksum)
604 .with_copy_if_not_exists(S3CopyIfNotExists::Multipart)
605 .build()
606 .unwrap();
607
608 let src = Path::parse("src.bin").unwrap();
609 let dst = Path::parse("dst.bin").unwrap();
610 store
611 .put(&src, PutPayload::from(vec![0u8; 100_000]))
612 .await
613 .unwrap();
614 if store.head(&dst).await.is_ok() {
615 store.delete(&dst).await.unwrap();
616 }
617 store.copy_if_not_exists(&src, &dst).await.unwrap();
618 store.delete(&src).await.unwrap();
619 store.delete(&dst).await.unwrap();
620 }
621 }
622
623 #[tokio::test]
624 async fn copy_multipart_file_with_signature_change_checksum() {
625 maybe_skip_integration!();
626
627 let bucket = "test-bucket-for-copy-if-not-exists";
628 let checksum_src = Checksum::SHA256;
629 let checksum_dst = Checksum::CRC64NVME;
630
631 let src = Path::parse("change_checksum_src.bin").unwrap();
632 let dst = Path::parse("change_checksum_dst.bin").unwrap();
633
634 let store = AmazonS3Builder::from_env()
635 .with_bucket_name(bucket)
636 .with_checksum_algorithm(checksum_src)
637 .build()
638 .unwrap();
639
640 store
641 .put(&src, PutPayload::from(vec![0u8; 100_000]))
642 .await
643 .unwrap();
644 if store.head(&dst).await.is_ok() {
645 store.delete(&dst).await.unwrap();
646 }
647
648 let store = AmazonS3Builder::from_env()
649 .with_bucket_name(bucket)
650 .with_checksum_algorithm(checksum_dst)
651 .with_copy_if_not_exists(S3CopyIfNotExists::Multipart)
652 .build()
653 .unwrap();
654
655 store.copy_if_not_exists(&src, &dst).await.unwrap();
656 store.delete(&src).await.unwrap();
657 store.delete(&dst).await.unwrap();
658 }
659
660 #[tokio::test]
661 async fn write_multipart_file_with_signature_object_lock() {
662 maybe_skip_integration!();
663
664 for checksum in [Checksum::SHA256, Checksum::CRC64NVME] {
665 let bucket = "test-object-lock";
666 let store = AmazonS3Builder::from_env()
667 .with_bucket_name(bucket)
668 .with_checksum_algorithm(checksum)
669 .build()
670 .unwrap();
671
672 let str = "test.bin";
673 let path = Path::parse(str).unwrap();
674 let opts = PutMultipartOptions::default();
675 let mut upload = store.put_multipart_opts(&path, opts).await.unwrap();
676
677 upload
678 .put_part(PutPayload::from(vec![0u8; 10_000_000]))
679 .await
680 .unwrap();
681 upload
682 .put_part(PutPayload::from(vec![0u8; 5_000_000]))
683 .await
684 .unwrap();
685
686 let res = upload.complete().await.unwrap();
687 assert!(res.e_tag.is_some(), "Should have valid etag");
688
689 store.delete(&path).await.unwrap();
690 }
691 }
692
693 #[tokio::test]
694 async fn s3_test() {
695 maybe_skip_integration!();
696 let config =
699 AmazonS3Builder::from_env().with_http_connector(MarkerHttpConnector::default());
700
701 let integration = config.build().unwrap();
702 let config = &integration.client.config;
703 let test_not_exists = config.copy_if_not_exists.is_some();
704 let test_conditional_put = config.conditional_put != S3ConditionalPut::Disabled;
705
706 put_get_delete_list(&integration).await;
707 list_with_offset_exclusivity(&integration).await;
708 get_opts(&integration).await;
709 list_uses_directories_correctly(&integration).await;
710 list_with_delimiter(&integration).await;
711 rename_and_copy(&integration).await;
712 stream_get(&integration).await;
713 multipart(&integration, &integration).await;
714 multipart_with_opts(&integration, &integration).await;
715 multipart_put_part_out_of_order(&integration, &integration).await;
716 multipart_race_condition(&integration, true).await;
717 multipart_out_of_order(&integration).await;
718 signing(&integration).await;
719 s3_encryption(&integration).await;
720 put_get_attributes(&integration).await;
721 list_paginated(&integration, &integration).await;
722 response_extensions(&integration, true).await;
723
724 if config.session_provider.is_none() {
726 tagging(
727 Arc::new(AmazonS3 {
728 client: Arc::clone(&integration.client),
729 }),
730 !config.disable_tagging,
731 |p| {
732 let client = Arc::clone(&integration.client);
733 async move { client.get_object_tagging(&p).await }
734 },
735 )
736 .await;
737 }
738
739 if test_not_exists {
740 copy_if_not_exists(&integration).await;
741 }
742 if test_conditional_put {
743 put_opts(&integration, true).await;
744 }
745
746 let builder = AmazonS3Builder::from_env().with_unsigned_payload(true);
748 let integration = builder.build().unwrap();
749 put_get_delete_list(&integration).await;
750
751 let builder = AmazonS3Builder::from_env().with_checksum_algorithm(Checksum::SHA256);
753 let integration = builder.build().unwrap();
754 put_get_delete_list(&integration).await;
755
756 let builder = AmazonS3Builder::from_env().with_checksum_algorithm(Checksum::CRC64NVME);
758 let integration = builder.build().unwrap();
759 put_get_delete_list(&integration).await;
760 }
761
762 #[tokio::test]
763 async fn s3_test_get_nonexistent_location() {
764 maybe_skip_integration!();
765 let integration = AmazonS3Builder::from_env().build().unwrap();
766
767 let location = Path::from_iter([NON_EXISTENT_NAME]);
768
769 let err = get_nonexistent_object(&integration, Some(location))
770 .await
771 .unwrap_err();
772 assert!(matches!(err, crate::Error::NotFound { .. }), "{}", err);
773 }
774
775 #[tokio::test]
776 async fn s3_test_get_nonexistent_bucket() {
777 maybe_skip_integration!();
778 let config = AmazonS3Builder::from_env().with_bucket_name(NON_EXISTENT_NAME);
779 let integration = config.build().unwrap();
780
781 let location = Path::from_iter([NON_EXISTENT_NAME]);
782
783 let err = integration.get(&location).await.unwrap_err();
784 assert!(matches!(err, crate::Error::NotFound { .. }), "{}", err);
785 }
786
787 #[tokio::test]
788 async fn s3_test_put_nonexistent_bucket() {
789 maybe_skip_integration!();
790 let config = AmazonS3Builder::from_env().with_bucket_name(NON_EXISTENT_NAME);
791 let integration = config.build().unwrap();
792
793 let location = Path::from_iter([NON_EXISTENT_NAME]);
794 let data = PutPayload::from("arbitrary data");
795
796 let err = integration.put(&location, data).await.unwrap_err();
797 assert!(matches!(err, crate::Error::NotFound { .. }), "{}", err);
798 }
799
800 #[tokio::test]
801 async fn s3_test_delete_nonexistent_location() {
802 maybe_skip_integration!();
803 let integration = AmazonS3Builder::from_env().build().unwrap();
804
805 let location = Path::from_iter([NON_EXISTENT_NAME]);
806
807 integration.delete(&location).await.unwrap();
808 }
809
810 #[tokio::test]
811 async fn s3_test_delete_nonexistent_bucket() {
812 maybe_skip_integration!();
813 let config = AmazonS3Builder::from_env().with_bucket_name(NON_EXISTENT_NAME);
814 let integration = config.build().unwrap();
815
816 let location = Path::from_iter([NON_EXISTENT_NAME]);
817
818 let err = integration.delete(&location).await.unwrap_err();
819 assert!(matches!(err, crate::Error::NotFound { .. }), "{}", err);
820 }
821
822 #[tokio::test]
823 #[ignore = "Tests shouldn't call use remote services by default"]
824 async fn test_disable_creds() {
825 let v1 = AmazonS3Builder::new()
827 .with_bucket_name("daylight-map-distribution")
828 .with_region("us-west-1")
829 .with_access_key_id("local")
830 .with_secret_access_key("development")
831 .build()
832 .unwrap();
833
834 let prefix = Path::from("release");
835
836 v1.list_with_delimiter(Some(&prefix)).await.unwrap_err();
837
838 let v2 = AmazonS3Builder::new()
839 .with_bucket_name("daylight-map-distribution")
840 .with_region("us-west-1")
841 .with_skip_signature(true)
842 .build()
843 .unwrap();
844
845 v2.list_with_delimiter(Some(&prefix)).await.unwrap();
846 }
847
848 async fn s3_encryption(store: &AmazonS3) {
849 maybe_skip_integration!();
850
851 let data = PutPayload::from(vec![3u8; 1024]);
852
853 let encryption_headers: HeaderMap = store.client.config.encryption_headers.clone().into();
854 let expected_encryption =
855 if let Some(encryption_type) = encryption_headers.get("x-amz-server-side-encryption") {
856 encryption_type
857 } else {
858 eprintln!("Skipping S3 encryption test - encryption not configured");
859 return;
860 };
861
862 let locations = [
863 Path::from("test-encryption-1"),
864 Path::from("test-encryption-2"),
865 Path::from("test-encryption-3"),
866 ];
867
868 store.put(&locations[0], data.clone()).await.unwrap();
869 store.copy(&locations[0], &locations[1]).await.unwrap();
870
871 let mut upload = store.put_multipart(&locations[2]).await.unwrap();
872 upload.put_part(data.clone()).await.unwrap();
873 upload.complete().await.unwrap();
874
875 for location in &locations {
876 let mut context = RetryContext::new(&store.client.config.retry_config);
877
878 let res = store
879 .client
880 .get_request(&mut context, location, GetOptions::default())
881 .await
882 .unwrap();
883
884 let headers = res.headers();
885 assert_eq!(
886 headers
887 .get("x-amz-server-side-encryption")
888 .expect("object is not encrypted"),
889 expected_encryption
890 );
891
892 store.delete(location).await.unwrap();
893 }
894 }
895
896 #[tokio::test]
898 async fn test_s3_ssec_encryption_with_minio() {
899 if std::env::var("TEST_S3_SSEC_ENCRYPTION").is_err() {
900 eprintln!("Skipping S3 SSE-C encryption test");
901 return;
902 }
903 eprintln!("Running S3 SSE-C encryption test");
904
905 let customer_key = "1234567890abcdef1234567890abcdef";
906 let expected_md5 = "JMwgiexXqwuPqIPjYFmIZQ==";
907
908 let store = AmazonS3Builder::from_env()
909 .with_ssec_encryption(BASE64_STANDARD.encode(customer_key))
910 .with_client_options(ClientOptions::default().with_allow_invalid_certificates(true))
911 .build()
912 .unwrap();
913
914 let data = PutPayload::from(vec![3u8; 1024]);
915
916 let locations = [
917 Path::from("test-encryption-1"),
918 Path::from("test-encryption-2"),
919 Path::from("test-encryption-3"),
920 ];
921
922 store.put(&locations[0], data.clone()).await.unwrap();
924
925 store.copy(&locations[0], &locations[1]).await.unwrap();
927
928 let mut upload = store.put_multipart(&locations[2]).await.unwrap();
930 upload.put_part(data.clone()).await.unwrap();
931 upload.complete().await.unwrap();
932
933 for location in &locations {
935 let mut context = RetryContext::new(&store.client.config.retry_config);
936
937 let res = store
938 .client
939 .get_request(&mut context, location, GetOptions::default())
940 .await
941 .unwrap();
942
943 let headers = res.headers();
944 assert_eq!(
945 headers
946 .get("x-amz-server-side-encryption-customer-algorithm")
947 .expect("object is not encrypted with SSE-C"),
948 "AES256"
949 );
950
951 assert_eq!(
952 headers
953 .get("x-amz-server-side-encryption-customer-key-MD5")
954 .expect("object is not encrypted with SSE-C"),
955 expected_md5
956 );
957
958 store.delete(location).await.unwrap();
959 }
960 }
961
962 #[cfg(feature = "reqwest")]
965 #[test]
966 fn s3_alternate_threadpool_spawned_request_connector() {
967 maybe_skip_integration!();
968 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
969
970 let io_runtime = tokio::runtime::Builder::new_current_thread()
972 .enable_all() .build()
974 .unwrap();
975
976 let non_io_runtime = tokio::runtime::Builder::new_current_thread()
978 .build()
980 .unwrap();
981
982 let io_handle = io_runtime.handle().clone();
984 let thread_handle = std::thread::spawn(move || {
985 io_runtime.block_on(async move {
986 shutdown_rx.await.unwrap();
987 });
988 });
989
990 let store = AmazonS3Builder::from_env()
991 .with_bucket_name("test-bucket-for-spawn")
993 .with_http_connector(SpawnedReqwestConnector::new(io_handle))
994 .build()
995 .unwrap();
996
997 non_io_runtime
1000 .block_on(async move {
1001 let path = Path::from("alternate_threadpool/test.txt");
1002 store.delete(&path).await.ok(); store.put(&path, "foo".into()).await?;
1004 let res = store.get(&path).await?.bytes().await?;
1005 assert_eq!(res.as_ref(), b"foo");
1006 store.delete(&path).await?; Ok(()) as Result<()>
1008 })
1009 .expect("failed to run request on non io runtime");
1010
1011 shutdown_tx.send(()).ok();
1013 thread_handle.join().expect("runtime thread panicked");
1014 }
1015}