diff --git a/core/core/src/types/options.rs b/core/core/src/types/options.rs index 4ef109c61a29..8a32b56c31eb 100644 --- a/core/core/src/types/options.rs +++ b/core/core/src/types/options.rs @@ -426,7 +426,7 @@ pub struct WriteOptions { pub if_match: Option, /// Sets If-None-Match header for this write request. /// - /// Note: Certain services, like `s3`, support `if_not_exists` but not `if_none_match`. + /// Note: Certain services support `if_not_exists` but not `if_none_match`. /// Use `if_not_exists` if you only want to check whether a file exists. /// /// ### Capability diff --git a/core/services/s3/src/backend.rs b/core/services/s3/src/backend.rs index 3413fd5e81d6..4095326e2bfc 100644 --- a/core/services/s3/src/backend.rs +++ b/core/services/s3/src/backend.rs @@ -908,6 +908,7 @@ impl Builder for S3Builder { write_with_content_disposition: true, write_with_content_encoding: true, write_with_if_match: !config.disable_write_with_if_match, + write_with_if_none_match: true, write_with_if_not_exists: true, write_with_user_metadata: true, diff --git a/core/services/s3/src/core.rs b/core/services/s3/src/core.rs index 9bd98cf92df7..488b1d0867a7 100644 --- a/core/services/s3/src/core.rs +++ b/core/services/s3/src/core.rs @@ -318,7 +318,9 @@ impl S3Core { req = req.header(IF_MATCH, if_match); } - if args.if_not_exists() { + if let Some(if_none_match) = args.if_none_match() { + req = req.header(IF_NONE_MATCH, if_none_match); + } else if args.if_not_exists() { req = req.header(IF_NONE_MATCH, "*"); } @@ -902,6 +904,18 @@ impl S3Core { parts: Vec, args: &OpWrite, ) -> Result> { + let req = self.s3_complete_multipart_upload_request(path, upload_id, parts, args)?; + + self.send(req).await + } + + fn s3_complete_multipart_upload_request( + &self, + path: &str, + upload_id: &str, + parts: Vec, + args: &OpWrite, + ) -> Result> { let p = build_abs_path(&self.root, path); let url = format!( @@ -927,7 +941,9 @@ impl S3Core { if let Some(if_match) = args.if_match() { req = req.header(IF_MATCH, if_match); } - if args.if_not_exists() { + if let Some(if_none_match) = args.if_none_match() { + req = req.header(IF_NONE_MATCH, if_none_match); + } else if args.if_not_exists() { req = req.header(IF_NONE_MATCH, "*"); } @@ -941,7 +957,7 @@ impl S3Core { .body(Buffer::from(Bytes::from(content))) .map_err(new_request_build_error)?; - self.send(req).await + Ok(req) } /// Abort an on-going multipart upload. @@ -1287,11 +1303,108 @@ impl Display for ChecksumAlgorithm { #[cfg(test)] mod tests { + use std::sync::Arc; + use bytes::Buf; use bytes::Bytes; + use reqsign_aws_v4::RequestSigner as AwsV4Signer; + use reqsign_aws_v4::StaticCredentialProvider; + use reqsign_core::Context; + use reqsign_core::ProvideCredentialChain; + use reqsign_core::Signer; use super::*; + fn test_core() -> S3Core { + let provider = + ProvideCredentialChain::new().push(StaticCredentialProvider::new("ak", "sk")); + + S3Core { + info: Arc::new(AccessorInfo::default()), + bucket: "bucket".to_string(), + endpoint: "https://s3.amazonaws.com".to_string(), + root: "/root/".to_string(), + server_side_encryption: None, + server_side_encryption_aws_kms_key_id: None, + server_side_encryption_customer_algorithm: None, + server_side_encryption_customer_key: None, + server_side_encryption_customer_key_md5: None, + default_storage_class: None, + allow_anonymous: true, + disable_list_objects_v2: false, + enable_request_payer: false, + default_acl: None, + signer: Signer::new( + Context::new(), + provider, + AwsV4Signer::new("s3", "us-east-1"), + ), + checksum_algorithm: None, + } + } + + #[test] + fn test_put_object_request_sets_if_none_match() { + let core = test_core(); + let op = OpWrite::default().with_if_none_match("\"etag\""); + + let req = core + .s3_put_object_request("path/to/file", Some(0), &op, Buffer::new()) + .expect("request must build"); + + assert_eq!( + req.headers() + .get(IF_NONE_MATCH) + .expect("If-None-Match must be set") + .to_str() + .expect("header must be valid"), + "\"etag\"" + ); + } + + #[test] + fn test_put_object_request_keeps_if_not_exists_header() { + let core = test_core(); + let op = OpWrite::default().with_if_not_exists(true); + + let req = core + .s3_put_object_request("path/to/file", Some(0), &op, Buffer::new()) + .expect("request must build"); + + assert_eq!( + req.headers() + .get(IF_NONE_MATCH) + .expect("If-None-Match must be set") + .to_str() + .expect("header must be valid"), + "*" + ); + } + + #[test] + fn test_complete_multipart_upload_request_sets_if_none_match() { + let core = test_core(); + let op = OpWrite::default().with_if_none_match("\"etag\""); + let parts = vec![CompleteMultipartUploadRequestPart { + part_number: 1, + etag: "\"part-etag\"".to_string(), + ..Default::default() + }]; + + let req = core + .s3_complete_multipart_upload_request("path/to/file", "upload-id", parts, &op) + .expect("request must build"); + + assert_eq!( + req.headers() + .get(IF_NONE_MATCH) + .expect("If-None-Match must be set") + .to_str() + .expect("header must be valid"), + "\"etag\"" + ); + } + /// This example is from https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html#API_CreateMultipartUpload_Examples #[test] fn test_deserialize_initiate_multipart_upload_result() {