From 0b131ec6a239b586ac4037cba7b93b2d625711bb Mon Sep 17 00:00:00 2001 From: Keming Date: Sun, 10 Nov 2024 18:14:10 +0800 Subject: [PATCH] feat(core): add `if_not_exist` in `OpWrite` Signed-off-by: Keming --- core/src/raw/ops.rs | 12 +++++++++++ core/src/services/s3/backend.rs | 1 + core/src/services/s3/core.rs | 6 ++++++ core/src/types/capability.rs | 2 ++ core/src/types/operator/operator.rs | 19 ++++++++++++++++++ core/src/types/operator/operator_futures.rs | 5 +++++ core/tests/behavior/async_write.rs | 22 +++++++++++++++++++++ 7 files changed, 67 insertions(+) diff --git a/core/src/raw/ops.rs b/core/src/raw/ops.rs index d61b0f3b1b6f..a51b0b144e02 100644 --- a/core/src/raw/ops.rs +++ b/core/src/raw/ops.rs @@ -601,6 +601,7 @@ pub struct OpWrite { cache_control: Option, executor: Option, if_none_match: Option, + if_not_exist: Option, user_metadata: Option>, } @@ -697,6 +698,17 @@ impl OpWrite { self.if_none_match.as_deref() } + /// Set the If-Not-Exist of the option + pub fn with_if_not_exist(mut self, b: bool) -> Self { + self.if_not_exist = Some(b); + self + } + + /// Get If-Not-Exist from option + pub fn if_not_exist(&self) -> Option { + self.if_not_exist + } + /// Merge given executor into option. /// /// If executor has already been set, this will do nothing. diff --git a/core/src/services/s3/backend.rs b/core/src/services/s3/backend.rs index 5178b930f109..e18125eece12 100644 --- a/core/src/services/s3/backend.rs +++ b/core/src/services/s3/backend.rs @@ -925,6 +925,7 @@ impl Access for S3Backend { write_with_cache_control: true, write_with_content_type: true, write_with_if_none_match: true, + write_with_if_not_exist: true, write_with_user_metadata: true, // The min multipart size of S3 is 5 MiB. diff --git a/core/src/services/s3/core.rs b/core/src/services/s3/core.rs index 944dac8921b5..99cf3090e40f 100644 --- a/core/src/services/s3/core.rs +++ b/core/src/services/s3/core.rs @@ -480,6 +480,12 @@ impl S3Core { req = req.header(IF_NONE_MATCH, if_none_match); } + if let Some(if_not_exist) = args.if_not_exist() { + if if_not_exist { + req = req.header(IF_NONE_MATCH, "*"); + } + } + // Set body let req = req.body(body).map_err(new_request_build_error)?; diff --git a/core/src/types/capability.rs b/core/src/types/capability.rs index 514f7722f19b..88b551b54177 100644 --- a/core/src/types/capability.rs +++ b/core/src/types/capability.rs @@ -101,6 +101,8 @@ pub struct Capability { pub write_with_cache_control: bool, /// If operator supports write with if none match. pub write_with_if_none_match: bool, + /// If operator supports write with if not exist. + pub write_with_if_not_exist: bool, /// If operator supports write with user defined metadata pub write_with_user_metadata: bool, /// write_multi_max_size is the max size that services support in write_multi. diff --git a/core/src/types/operator/operator.rs b/core/src/types/operator/operator.rs index e58c60303f2b..6a76ccdf77f1 100644 --- a/core/src/types/operator/operator.rs +++ b/core/src/types/operator/operator.rs @@ -1254,6 +1254,25 @@ impl Operator { /// # } /// ``` /// + /// ## `if_not_exist` + /// + /// Set `if_not_exist` for this `write` request. This can be treated as a simplified version + /// of [`OpWrite::if_none_match`]. + /// + /// This feature is used to write a file only when it doesn't exist. If the file already + /// exists, an error with kind [`ErrorKind::ConditionNotMatch`] will be returned. + /// + /// ```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_not_exist(true).await; + /// assert!(res.is_err()); + /// assert_eq!(res.unwrap_err().kind(), ErrorKind::ConditionNotMatch); + /// # Ok(())} + /// ``` + /// /// # Examples /// /// ``` diff --git a/core/src/types/operator/operator_futures.rs b/core/src/types/operator/operator_futures.rs index fa689417d8f7..f1652bf191fb 100644 --- a/core/src/types/operator/operator_futures.rs +++ b/core/src/types/operator/operator_futures.rs @@ -329,6 +329,11 @@ impl>> FutureWrite { self.map(|(args, options, bs)| (args.with_if_none_match(s), options, bs)) } + /// Set the If-Not-Exist for this operation. + pub fn if_not_exist(self, b: bool) -> Self { + self.map(|(args, options, bs)| (args.with_if_not_exist(b), options, bs)) + } + /// Set the user defined metadata of the op /// /// ## Notes diff --git a/core/tests/behavior/async_write.rs b/core/tests/behavior/async_write.rs index 3ccf42e71527..e96cb166eeb7 100644 --- a/core/tests/behavior/async_write.rs +++ b/core/tests/behavior/async_write.rs @@ -45,6 +45,7 @@ pub fn tests(op: &Operator, tests: &mut Vec) { test_write_with_content_type, test_write_with_content_disposition, test_write_with_if_none_match, + test_write_with_if_not_exist, test_write_with_user_metadata, test_writer_write, test_writer_write_with_overwrite, @@ -646,3 +647,24 @@ pub async fn test_write_with_if_none_match(op: Operator) -> Result<()> { Ok(()) } + +/// Write an file with if_not_exist will get a ConditionNotMatch error if file exists. +pub async fn test_write_with_if_not_exist(op: Operator) -> Result<()> { + if !op.info().full_capability().write_with_if_not_exist { + return Ok(()); + } + + let (path, content, _) = TEST_FIXTURE.new_file(op.clone()); + + op.write(&path, content.clone()) + .await + .expect("write must succeed"); + let res = op + .write_with(&path, content.clone()) + .if_not_exist(true) + .await; + assert!(res.is_err()); + assert_eq!(res.unwrap_err().kind(), ErrorKind::ConditionNotMatch); + + Ok(()) +}