This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new b92cfc41ba feat: Add if_none_match for write (#5129)
b92cfc41ba is described below

commit b92cfc41ba0a93acc5ff519b48af262bd91585d9
Author: LeeHao <[email protected]>
AuthorDate: Wed Oct 9 00:00:58 2024 +0800

    feat: Add if_none_match for write (#5129)
    
    * feat:add if_none_match for write
    
    Signed-off-by: LeeHao <[email protected]>
    
    * fix:add ut for write option if_none_match
    
    Signed-off-by: LeeHao <[email protected]>
    
    ---------
    
    Signed-off-by: LeeHao <[email protected]>
---
 core/src/raw/ops.rs                         | 12 ++++++++++++
 core/src/services/s3/backend.rs             |  1 +
 core/src/services/s3/core.rs                |  4 ++++
 core/src/types/capability.rs                |  2 ++
 core/src/types/operator/operator.rs         | 23 +++++++++++++++++++++++
 core/src/types/operator/operator_futures.rs |  5 +++++
 core/tests/behavior/async_write.rs          | 22 ++++++++++++++++++++++
 7 files changed, 69 insertions(+)

diff --git a/core/src/raw/ops.rs b/core/src/raw/ops.rs
index a9b7cd3712..d61b0f3b1b 100644
--- a/core/src/raw/ops.rs
+++ b/core/src/raw/ops.rs
@@ -600,6 +600,7 @@ pub struct OpWrite {
     content_disposition: Option<String>,
     cache_control: Option<String>,
     executor: Option<Executor>,
+    if_none_match: Option<String>,
     user_metadata: Option<HashMap<String, String>>,
 }
 
@@ -685,6 +686,17 @@ impl OpWrite {
         self
     }
 
+    /// Set the If-None-Match of the option
+    pub fn with_if_none_match(mut self, s: &str) -> Self {
+        self.if_none_match = Some(s.to_string());
+        self
+    }
+
+    /// Get If-None-Match from option
+    pub fn if_none_match(&self) -> Option<&str> {
+        self.if_none_match.as_deref()
+    }
+
     /// 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 8073cbfbb9..c441f8ac45 100644
--- a/core/src/services/s3/backend.rs
+++ b/core/src/services/s3/backend.rs
@@ -917,6 +917,7 @@ impl Access for S3Backend {
                 write_can_multi: true,
                 write_with_cache_control: true,
                 write_with_content_type: true,
+                write_with_if_none_match: 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 38ff146872..66bcb14e24 100644
--- a/core/src/services/s3/core.rs
+++ b/core/src/services/s3/core.rs
@@ -475,6 +475,10 @@ impl S3Core {
             req = self.insert_checksum_header(req, &checksum);
         }
 
+        if let Some(if_none_match) = args.if_none_match() {
+            req = req.header(IF_NONE_MATCH, 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 4a874b2387..514f7722f1 100644
--- a/core/src/types/capability.rs
+++ b/core/src/types/capability.rs
@@ -99,6 +99,8 @@ pub struct Capability {
     pub write_with_content_disposition: bool,
     /// If operator supports write with cache control.
     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 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 cf8d65844c..09b09a401d 100644
--- a/core/src/types/operator/operator.rs
+++ b/core/src/types/operator/operator.rs
@@ -1201,6 +1201,29 @@ impl Operator {
     /// # }
     /// ```
     ///
+    /// ## `if_none_match`
+    ///
+    /// Set `if_none_match` for this `write` request.
+    ///
+    /// This feature can be used to check if the file already exists.
+    /// This prevents overwriting of existing objects with identical key names.
+    /// Users can use *(asterisk) to verify if a file already exists by 
matching with any ETag.
+    /// Note: S3 only support use *(asterisk).
+    ///
+    /// If file 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_none_match("*").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 b5ddd1d4c3..fa689417d8 100644
--- a/core/src/types/operator/operator_futures.rs
+++ b/core/src/types/operator/operator_futures.rs
@@ -324,6 +324,11 @@ impl<F: Future<Output = Result<()>>> FutureWrite<F> {
         self.map(|(args, options, bs)| (args.with_executor(executor), options, 
bs))
     }
 
+    /// Set the If-None-Match for this operation.
+    pub fn if_none_match(self, s: &str) -> Self {
+        self.map(|(args, options, bs)| (args.with_if_none_match(s), 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 81aee0d405..9bfdc15059 100644
--- a/core/tests/behavior/async_write.rs
+++ b/core/tests/behavior/async_write.rs
@@ -44,6 +44,7 @@ pub fn tests(op: &Operator, tests: &mut Vec<Trial>) {
             test_write_with_cache_control,
             test_write_with_content_type,
             test_write_with_content_disposition,
+            test_write_with_if_none_match,
             test_write_with_user_metadata,
             test_writer_write,
             test_writer_write_with_overwrite,
@@ -624,3 +625,24 @@ pub async fn test_writer_write_with_overwrite(op: 
Operator) -> Result<()> {
     op.delete(&path).await.expect("delete must succeed");
     Ok(())
 }
+
+/// Write an exists file with if_none_match should match, else get a 
ConditionNotMatch error.
+pub async fn test_write_with_if_none_match(op: Operator) -> Result<()> {
+    if !op.info().full_capability().write_with_if_none_match {
+        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_none_match("*")
+        .await;
+    assert!(res.is_err());
+    assert_eq!(res.unwrap_err().kind(), ErrorKind::ConditionNotMatch);
+
+    Ok(())
+}

Reply via email to