diff --git a/s3/src/bucket.rs b/s3/src/bucket.rs index 21bf4d40dc..5c6aa70de6 100644 --- a/s3/src/bucket.rs +++ b/s3/src/bucket.rs @@ -991,12 +991,41 @@ impl Bucket { #[maybe_async::maybe_async] pub async fn put_bucket_cors( &self, - cors_config: CorsConfiguration, + expected_bucket_owner: &str, + cors_config: &CorsConfiguration, ) -> Result { let command = Command::PutBucketCors { - configuration: cors_config, + expected_bucket_owner: expected_bucket_owner.to_string(), + configuration: cors_config.clone(), }; - let request = RequestImpl::new(self, "?cors", command).await?; + let request = RequestImpl::new(self, "", command).await?; + request.response_data(false).await + } + + #[maybe_async::maybe_async] + pub async fn get_bucket_cors( + &self, + expected_bucket_owner: &str, + ) -> Result { + let command = Command::GetBucketCors { + expected_bucket_owner: expected_bucket_owner.to_string(), + }; + let request = RequestImpl::new(self, "", command).await?; + let response = request.response_data(false).await?; + Ok(quick_xml::de::from_str::( + response.as_str()?, + )?) + } + + #[maybe_async::maybe_async] + pub async fn delete_bucket_cors( + &self, + expected_bucket_owner: &str, + ) -> Result { + let command = Command::DeleteBucketCors { + expected_bucket_owner: expected_bucket_owner.to_string(), + }; + let request = RequestImpl::new(self, "", command).await?; request.response_data(false).await } @@ -2802,8 +2831,8 @@ mod test { assert_eq!(response_data.status_code(), 206); assert_eq!(test[100..1001].to_vec(), response_data.as_slice()); if head { - let (head_object_result, code) = bucket.head_object(s3_path).await.unwrap(); - println!("{:?}", head_object_result); + let (_head_object_result, code) = bucket.head_object(s3_path).await.unwrap(); + // println!("{:?}", head_object_result); assert_eq!(code, 200); } @@ -3529,7 +3558,7 @@ mod test { ) )] #[ignore] - async fn test_put_bucket_cors() { + async fn test_bucket_cors() { let bucket = test_aws_bucket(); let rule = CorsRule::new( None, @@ -3539,8 +3568,18 @@ mod test { None, None, ); + let expected_bucket_owner = "904662384344"; let cors_config = CorsConfiguration::new(vec![rule]); - let response = bucket.put_bucket_cors(cors_config).await.unwrap(); - assert_eq!(response.status_code(), 200) + let response = bucket + .put_bucket_cors(expected_bucket_owner, &cors_config) + .await + .unwrap(); + assert_eq!(response.status_code(), 200); + + let cors_response = bucket.get_bucket_cors(expected_bucket_owner).await.unwrap(); + assert_eq!(cors_response, cors_config); + + let response = bucket.delete_bucket_cors(expected_bucket_owner).await.unwrap(); + assert_eq!(response.status_code(), 204); } } diff --git a/s3/src/command.rs b/s3/src/command.rs index 9220a0e7f5..47fbe1fb6f 100644 --- a/s3/src/command.rs +++ b/s3/src/command.rs @@ -150,9 +150,16 @@ pub enum Command<'a> { }, DeleteBucket, ListBuckets, + GetBucketCors { + expected_bucket_owner: String, + }, PutBucketCors { + expected_bucket_owner: String, configuration: CorsConfiguration, }, + DeleteBucketCors { + expected_bucket_owner: String, + }, GetBucketLifecycle, PutBucketLifecycle { configuration: BucketLifecycleConfiguration, @@ -165,6 +172,7 @@ impl<'a> Command<'a> { match *self { Command::GetObject | Command::GetObjectTorrent + | Command::GetBucketCors { .. } | Command::GetObjectRange { .. } | Command::ListBuckets | Command::ListObjects { .. } @@ -187,6 +195,7 @@ impl<'a> Command<'a> { | Command::AbortMultipartUpload { .. } | Command::PresignDelete { .. } | Command::DeleteBucket + | Command::DeleteBucketCors { .. } | Command::DeleteBucketLifecycle => HttpMethod::Delete, Command::InitiateMultipartUpload { .. } | Command::CompleteMultipartUpload { .. } => { HttpMethod::Post @@ -212,7 +221,31 @@ impl<'a> Command<'a> { Command::PutBucketLifecycle { configuration } => { quick_xml::se::to_string(configuration)?.as_bytes().len() } - _ => 0, + Command::PutBucketCors { configuration, .. } => { + configuration.to_string().as_bytes().len() + } + Command::HeadObject => 0, + Command::DeleteObject => 0, + Command::DeleteObjectTagging => 0, + Command::GetObject => 0, + Command::GetObjectTorrent => 0, + Command::GetObjectRange { .. } => 0, + Command::GetObjectTagging => 0, + Command::ListMultipartUploads { .. } => 0, + Command::ListObjects { .. } => 0, + Command::ListObjectsV2 { .. } => 0, + Command::GetBucketLocation => 0, + Command::PresignGet { .. } => 0, + Command::PresignPut { .. } => 0, + Command::PresignDelete { .. } => 0, + Command::InitiateMultipartUpload { .. } => 0, + Command::AbortMultipartUpload { .. } => 0, + Command::DeleteBucket => 0, + Command::ListBuckets => 0, + Command::GetBucketCors { .. } => 0, + Command::DeleteBucketCors { .. } => 0, + Command::GetBucketLifecycle => 0, + Command::DeleteBucketLifecycle { .. } => 0, }; Ok(result) } @@ -221,10 +254,34 @@ impl<'a> Command<'a> { match self { Command::InitiateMultipartUpload { content_type } => content_type.to_string(), Command::PutObject { content_type, .. } => content_type.to_string(), - Command::CompleteMultipartUpload { .. } | Command::PutBucketLifecycle { .. } => { - "application/xml".into() - } - _ => "text/plain".into(), + Command::CompleteMultipartUpload { .. } + | Command::PutBucketLifecycle { .. } + | Command::PutBucketCors { .. } => "application/xml".into(), + Command::HeadObject => "text/plain".into(), + Command::DeleteObject => "text/plain".into(), + Command::DeleteObjectTagging => "text/plain".into(), + Command::GetObject => "text/plain".into(), + Command::GetObjectTorrent => "text/plain".into(), + Command::GetObjectRange { .. } => "text/plain".into(), + Command::GetObjectTagging => "text/plain".into(), + Command::ListMultipartUploads { .. } => "text/plain".into(), + Command::ListObjects { .. } => "text/plain".into(), + Command::ListObjectsV2 { .. } => "text/plain".into(), + Command::GetBucketLocation => "text/plain".into(), + Command::PresignGet { .. } => "text/plain".into(), + Command::PresignPut { .. } => "text/plain".into(), + Command::PresignDelete { .. } => "text/plain".into(), + Command::AbortMultipartUpload { .. } => "text/plain".into(), + Command::DeleteBucket => "text/plain".into(), + Command::ListBuckets => "text/plain".into(), + Command::GetBucketCors { .. } => "text/plain".into(), + Command::DeleteBucketCors { .. } => "text/plain".into(), + Command::GetBucketLifecycle => "text/plain".into(), + Command::DeleteBucketLifecycle { .. } => "text/plain".into(), + Command::CopyObject { .. } => "text/plain".into(), + Command::PutObjectTagging { .. } => "text/plain".into(), + Command::UploadPart { .. } => "text/plain".into(), + Command::CreateBucket { .. } => "text/plain".into(), } } @@ -254,7 +311,40 @@ impl<'a> Command<'a> { EMPTY_PAYLOAD_SHA.into() } } - _ => EMPTY_PAYLOAD_SHA.into(), + Command::PutBucketLifecycle { configuration } => { + let mut sha = Sha256::default(); + sha.update(quick_xml::se::to_string(configuration)?.as_bytes()); + hex::encode(sha.finalize().as_slice()) + } + Command::PutBucketCors { configuration, .. } => { + let mut sha = Sha256::default(); + sha.update(configuration.to_string().as_bytes()); + hex::encode(sha.finalize().as_slice()) + } + Command::HeadObject => EMPTY_PAYLOAD_SHA.into(), + Command::DeleteObject => EMPTY_PAYLOAD_SHA.into(), + Command::DeleteObjectTagging => EMPTY_PAYLOAD_SHA.into(), + Command::GetObject => EMPTY_PAYLOAD_SHA.into(), + Command::GetObjectTorrent => EMPTY_PAYLOAD_SHA.into(), + Command::GetObjectRange { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::GetObjectTagging => EMPTY_PAYLOAD_SHA.into(), + Command::ListMultipartUploads { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::ListObjects { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::ListObjectsV2 { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::GetBucketLocation => EMPTY_PAYLOAD_SHA.into(), + Command::PresignGet { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::PresignPut { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::PresignDelete { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::AbortMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::DeleteBucket => EMPTY_PAYLOAD_SHA.into(), + Command::ListBuckets => EMPTY_PAYLOAD_SHA.into(), + Command::GetBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::DeleteBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::GetBucketLifecycle => EMPTY_PAYLOAD_SHA.into(), + Command::DeleteBucketLifecycle { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::CopyObject { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::UploadPart { .. } => EMPTY_PAYLOAD_SHA.into(), + Command::InitiateMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(), }; Ok(result) } diff --git a/s3/src/lib.rs b/s3/src/lib.rs index fe63a88de6..e97950bb09 100644 --- a/s3/src/lib.rs +++ b/s3/src/lib.rs @@ -76,20 +76,24 @@ pub fn get_retries() -> u8 { #[cfg(not(feature = "disable-call-for-funding"))] #[inline(always)] pub(crate) fn init_once() { - use ansi_term::Colour::{Blue, Yellow}; + use ansi_term::{ + Colour::{Blue, Yellow}, + Style, + }; if !INITIALIZED.load(std::sync::atomic::Ordering::Relaxed) { INITIALIZED.store(true, std::sync::atomic::Ordering::SeqCst); eprintln!( - " {0}------------------------------------------------------------------------------------------------{0}\n - Support {1} crate development by donating {2} to {3} \n - {0}--- {4} ---{0} \n - {0}------------------------------------------------------------------------------------------------{0}", + " {0}---------------------------------------------------------------------------------{0}\n + Support {1} crate -> {5} {2} to {3} \n + {0}--- {4} ---{0} \n + {0}---------------------------------------------------------------------------------{0}", Yellow.bold().paint("<>"), Yellow.bold().paint("rust-s3"), Yellow.bold().paint("BTC"), Yellow.bold().paint("bc1q7ukqe09zplg2sltgfrkukghpelfaz7qja8pw6u"), - Blue.bold().paint("Thank you!"), + Blue.bold().paint("Thank You!"), + Style::new().bold().underline().paint("donate"), ); } } diff --git a/s3/src/request/request_trait.rs b/s3/src/request/request_trait.rs index e80f0f0572..cc5f1b324e 100644 --- a/s3/src/request/request_trait.rs +++ b/s3/src/request/request_trait.rs @@ -178,8 +178,11 @@ pub trait Request { } else { Vec::new() } - } else if let Command::PutBucketLifecycle { configuration } = &self.command() { + } else if let Command::PutBucketLifecycle { configuration, .. } = &self.command() { quick_xml::se::to_string(configuration)?.as_bytes().to_vec() + } else if let Command::PutBucketCors { configuration, .. } = &self.command() { + let cors = configuration.to_string(); + cors.as_bytes().to_vec() } else { Vec::new() }; @@ -395,6 +398,11 @@ pub trait Request { | Command::DeleteBucketLifecycle => { url_str.push_str("?lifecycle"); } + Command::GetBucketCors { .. } + | Command::PutBucketCors { .. } + | Command::DeleteBucketCors { .. } => { + url_str.push_str("?cors"); + } _ => {} } @@ -607,6 +615,36 @@ pub trait Request { let hash = general_purpose::STANDARD.encode(digest.as_ref()); headers.insert(HeaderName::from_static("content-md5"), hash.parse()?); headers.remove("x-amz-content-sha256"); + } else if let Command::PutBucketCors { + expected_bucket_owner, + configuration, + .. + } = self.command() + { + let digest = md5::compute(configuration.to_string().as_bytes()); + let hash = general_purpose::STANDARD.encode(digest.as_ref()); + headers.insert(HeaderName::from_static("content-md5"), hash.parse()?); + + headers.insert( + HeaderName::from_static("x-amz-expected-bucket-owner"), + expected_bucket_owner.parse()?, + ); + } else if let Command::GetBucketCors { + expected_bucket_owner, + } = self.command() + { + headers.insert( + HeaderName::from_static("x-amz-expected-bucket-owner"), + expected_bucket_owner.parse()?, + ); + } else if let Command::DeleteBucketCors { + expected_bucket_owner, + } = self.command() + { + headers.insert( + HeaderName::from_static("x-amz-expected-bucket-owner"), + expected_bucket_owner.parse()?, + ); } // This must be last, as it signs the other headers, omitted if no secret key is provided diff --git a/s3/src/request/tokio_backend.rs b/s3/src/request/tokio_backend.rs index 9c43524851..ce88807659 100644 --- a/s3/src/request/tokio_backend.rs +++ b/s3/src/request/tokio_backend.rs @@ -102,7 +102,9 @@ impl<'a> Request for ReqwestRequest<'a> { .headers(headers) .body(self.request_body()?); - let response = client.execute(request.build()?).await?; + let request = request.build()?; + + let response = client.execute(request).await?; if cfg!(feature = "fail-on-err") && !response.status().is_success() { let status = response.status().as_u16(); diff --git a/s3/src/serde_types.rs b/s3/src/serde_types.rs index e93a723d4d..2ca0d439ea 100644 --- a/s3/src/serde_types.rs +++ b/s3/src/serde_types.rs @@ -63,7 +63,7 @@ pub struct MultipartUpload { pub id: String, } -use std::fmt; +use std::fmt::{self}; impl fmt::Display for CompleteMultipartUploadData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -319,7 +319,7 @@ pub struct AwsError { pub request_id: String, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename = "CORSConfiguration")] pub struct CorsConfiguration { #[serde(rename = "CORSRule")] @@ -332,7 +332,21 @@ impl CorsConfiguration { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +impl fmt::Display for CorsConfiguration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cors = quick_xml::se::to_string(&self).map_err(|_| fmt::Error)?; + let preamble = ""; + let cors = format!("{}{}", preamble, cors); + let cors = cors.replace( + "", + "", + ); + + write!(f, "{}", cors) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct CorsRule { #[serde(rename = "AllowedHeader")] #[serde(skip_serializing_if = "Option::is_none")] @@ -344,6 +358,7 @@ pub struct CorsRule { #[serde(rename = "ExposeHeader")] #[serde(skip_serializing_if = "Option::is_none")] expose_headers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "ID")] id: Option, #[serde(rename = "MaxAgeSeconds")]