You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by te...@apache.org on 2026/04/27 17:54:53 UTC
(opendal) 01/01: fix(services/s3): support write if-none-match
This is an automated email from the ASF dual-hosted git repository.
TennyZhuang pushed a commit to branch codex/s3-if-none-match-write
in repository https://gitbox.apache.org/repos/asf/opendal.git
commit f214be19286288e8d935068a95f324257d3db296
Author: TennyZhuang <91...@users.noreply.github.com>
AuthorDate: Tue Apr 28 01:54:39 2026 +0800
fix(services/s3): support write if-none-match
---
core/core/src/types/options.rs | 2 +-
core/services/s3/src/backend.rs | 1 +
core/services/s3/src/core.rs | 119 +++++++++++++++++++++++++++++++++++++++-
3 files changed, 118 insertions(+), 4 deletions(-)
diff --git a/core/core/src/types/options.rs b/core/core/src/types/options.rs
index 4ef109c61..8a32b56c3 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<String>,
/// 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 3413fd5e8..4095326e2 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 9bd98cf92..488b1d086 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<CompleteMultipartUploadRequestPart>,
args: &OpWrite,
) -> Result<Response<Buffer>> {
+ 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<CompleteMultipartUploadRequestPart>,
+ args: &OpWrite,
+ ) -> Result<Request<Buffer>> {
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() {