diff --git a/core/src/raw/ops.rs b/core/src/raw/ops.rs index 69615e0c9466..754adf1e5e2d 100644 --- a/core/src/raw/ops.rs +++ b/core/src/raw/ops.rs @@ -578,6 +578,7 @@ pub struct OpWrite { cache_control: Option, executor: Option, if_none_match: Option, + if_match: Option, if_not_exists: bool, user_metadata: Option>, } @@ -675,6 +676,17 @@ impl OpWrite { self.if_none_match.as_deref() } + /// Set the If-Match of the option + pub fn with_if_match(mut self, s: &str) -> Self { + self.if_match = Some(s.to_string()); + self + } + + /// Get If-Match from option + pub fn if_match(&self) -> Option<&str> { + self.if_match.as_deref() + } + /// Set the If-Not-Exist of the option pub fn with_if_not_exists(mut self, b: bool) -> Self { self.if_not_exists = b; diff --git a/core/src/services/s3/backend.rs b/core/src/services/s3/backend.rs index 65dca0f449b2..b53ff2ddd38d 100644 --- a/core/src/services/s3/backend.rs +++ b/core/src/services/s3/backend.rs @@ -924,6 +924,7 @@ impl Access for S3Backend { write_can_multi: true, write_with_cache_control: true, write_with_content_type: true, + write_with_if_match: true, write_with_if_not_exists: true, write_with_user_metadata: true, diff --git a/core/src/services/s3/core.rs b/core/src/services/s3/core.rs index 745f198f67af..eed949819634 100644 --- a/core/src/services/s3/core.rs +++ b/core/src/services/s3/core.rs @@ -455,6 +455,10 @@ impl S3Core { req = req.header(CACHE_CONTROL, cache_control) } + if let Some(if_match) = args.if_match() { + req = req.header(IF_MATCH, if_match); + } + if args.if_not_exists() { req = req.header(IF_NONE_MATCH, "*"); } diff --git a/core/src/types/capability.rs b/core/src/types/capability.rs index b3a9e3af9d09..a10dd9b4b250 100644 --- a/core/src/types/capability.rs +++ b/core/src/types/capability.rs @@ -130,6 +130,8 @@ pub struct Capability { pub write_with_cache_control: bool, /// Indicates if conditional write operations using If-None-Match are supported. pub write_with_if_none_match: bool, + /// Indicates if conditional write operations using If-Match are supported. + pub write_with_if_match: bool, /// Indicates if write operations can be conditional on object non-existence. pub write_with_if_not_exists: bool, /// Indicates if custom user metadata can be attached during write operations. diff --git a/core/src/types/operator/operator.rs b/core/src/types/operator/operator.rs index 0e8af02989eb..c838cdcde7ad 100644 --- a/core/src/types/operator/operator.rs +++ b/core/src/types/operator/operator.rs @@ -1419,6 +1419,36 @@ impl Operator { /// # Ok(()) /// # } /// ``` + /// + /// ## `if_match` + /// + /// Sets an `if match` condition with specified ETag for this write request. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_if_match`] before using this feature. + /// + /// ### Behavior + /// + /// - If the target file's ETag does not match the specified one, returns [`ErrorKind::ConditionNotMatch`] + /// - If the target file's ETag matches the specified one, proceeds with the write operation + /// + /// This operation will succeed when the target's ETag matches the specified one, + /// providing a way for conditional writes. + /// + /// ### Example + /// + /// ```no_run + /// # use opendal::{ErrorKind, Result}; + /// use opendal::Operator; + /// # async fn test(op: Operator, etag: &str) -> Result<()> { + /// let bs = b"hello, world!".to_vec(); + /// let res = op.write_with("path/to/file", bs).if_match(etag).await; + /// assert!(res.is_err()); + /// assert_eq!(res.unwrap_err().kind(), ErrorKind::ConditionNotMatch); + /// # Ok(()) + /// # } + /// ``` pub fn write_with( &self, path: &str, diff --git a/core/src/types/operator/operator_futures.rs b/core/src/types/operator/operator_futures.rs index 53d89f594eb1..b0509254dafd 100644 --- a/core/src/types/operator/operator_futures.rs +++ b/core/src/types/operator/operator_futures.rs @@ -328,6 +328,11 @@ impl>> FutureWrite { self.map(|(args, options, bs)| (args.with_if_none_match(s), options, bs)) } + /// Set the If-Match for this operation. + pub fn if_match(self, s: &str) -> Self { + self.map(|(args, options, bs)| (args.with_if_match(s), options, bs)) + } + /// Set the If-Not-Exist for this operation. pub fn if_not_exists(self, b: bool) -> Self { self.map(|(args, options, bs)| (args.with_if_not_exists(b), options, bs)) diff --git a/core/tests/behavior/async_write.rs b/core/tests/behavior/async_write.rs index b51cc2234f38..5e0fc4ca6718 100644 --- a/core/tests/behavior/async_write.rs +++ b/core/tests/behavior/async_write.rs @@ -46,6 +46,7 @@ pub fn tests(op: &Operator, tests: &mut Vec) { test_write_with_content_disposition, test_write_with_if_none_match, test_write_with_if_not_exists, + test_write_with_if_match, test_write_with_user_metadata, test_writer_write, test_writer_write_with_overwrite, @@ -674,3 +675,22 @@ pub async fn test_write_with_if_not_exists(op: Operator) -> Result<()> { Ok(()) } + +/// Write an file with if_match will get a ConditionNotMatch error if file's etag does not match. +pub async fn test_write_with_if_match(op: Operator) -> Result<()> { + if !op.info().full_capability().write_with_if_match { + return Ok(()); + } + + let (path, content, _) = TEST_FIXTURE.new_file(op.clone()); + + op.write(&path, content.clone()).await?; + + let meta = op.stat(&path).await?; + let etag = meta.etag().expect("etag must exist"); + + let res = op.write_with(&path, content.clone()).if_match(etag).await; + assert!(res.is_ok()); + + Ok(()) +}