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 bdf64d514 feat(core): complete copy_with_if_match implementation 
(#7627)
bdf64d514 is described below

commit bdf64d514ccd7b5a27f2f61a879d2f05cbdb4fcf
Author: Yuang Gao <[email protected]>
AuthorDate: Thu May 28 00:40:05 2026 -0700

    feat(core): complete copy_with_if_match implementation (#7627)
---
 core/core/src/types/operator/operator_futures.rs | 18 +++++
 core/layers/capability-check/src/lib.rs          |  7 ++
 core/tests/behavior/async_copy.rs                | 85 ++++++++++++++++++++++++
 3 files changed, 110 insertions(+)

diff --git a/core/core/src/types/operator/operator_futures.rs 
b/core/core/src/types/operator/operator_futures.rs
index 093395fb3..074c6838f 100644
--- a/core/core/src/types/operator/operator_futures.rs
+++ b/core/core/src/types/operator/operator_futures.rs
@@ -1437,6 +1437,15 @@ impl<F: Future<Output = Result<Metadata>>> FutureCopy<F> 
{
         self
     }
 
+    /// Sets the condition that copy operation will succeed only if the
+    /// destination object currently has the given ETag.
+    ///
+    /// Refer to [`options::CopyOptions::if_match`] for more details.
+    pub fn if_match(mut self, etag: &str) -> Self {
+        self.args.0.if_match = Some(etag.to_string());
+        self
+    }
+
     /// Sets concurrent copy operations for this copy.
     ///
     /// Refer to [`options::CopyOptions::concurrent`] for more details.
@@ -1476,6 +1485,15 @@ impl<F: Future<Output = Result<Copier>>> FutureCopier<F> 
{
         self
     }
 
+    /// Sets the condition that copy operation will succeed only if the
+    /// destination object currently has the given ETag.
+    ///
+    /// Refer to [`options::CopyOptions::if_match`] for more details.
+    pub fn if_match(mut self, etag: &str) -> Self {
+        self.args.0.if_match = Some(etag.to_string());
+        self
+    }
+
     /// Sets known source content length as a hint for this `copier`.
     ///
     /// Refer to [`options::CopyOptions::source_content_length_hint`] for more 
details.
diff --git a/core/layers/capability-check/src/lib.rs 
b/core/layers/capability-check/src/lib.rs
index 96f1c8cde..4897b1c5e 100644
--- a/core/layers/capability-check/src/lib.rs
+++ b/core/layers/capability-check/src/lib.rs
@@ -162,6 +162,13 @@ impl<A: Access> LayeredAccess for CapabilityAccessor<A> {
                 "if_not_exists",
             ));
         }
+        if args.if_match().is_some() && !capability.copy_with_if_match {
+            return Err(new_unsupported_error(
+                self.info.as_ref(),
+                Operation::Copy,
+                "if_match",
+            ));
+        }
 
         self.inner.copy(from, to, args, opts).await
     }
diff --git a/core/tests/behavior/async_copy.rs 
b/core/tests/behavior/async_copy.rs
index d043992a2..c9983e1b8 100644
--- a/core/tests/behavior/async_copy.rs
+++ b/core/tests/behavior/async_copy.rs
@@ -56,6 +56,14 @@ pub fn tests(op: &Operator, tests: &mut Vec<Trial>) {
             test_copier_with_if_not_exists_to_existing_file
         ))
     }
+
+    if cap.read && cap.write && cap.copy && cap.copy_with_if_match {
+        tests.extend(async_trials!(
+            op,
+            test_copy_with_if_match_match,
+            test_copy_with_if_match_mismatch
+        ))
+    }
 }
 
 fn copy_multi_chunk_size(cap: Capability) -> Option<(usize, usize)> {
@@ -349,6 +357,83 @@ pub async fn 
test_copy_with_if_not_exists_to_existing_file(op: Operator) -> Resu
     Ok(())
 }
 
+/// Copy with if_match matching the destination ETag should overwrite it.
+pub async fn test_copy_with_if_match_match(op: Operator) -> Result<()> {
+    if !op.info().full_capability().copy_with_if_match {
+        return Ok(());
+    }
+
+    let source_path = uuid::Uuid::new_v4().to_string();
+    let (source_content, _) = gen_bytes(op.info().full_capability());
+    op.write(&source_path, source_content.clone()).await?;
+
+    let target_path = uuid::Uuid::new_v4().to_string();
+    let (target_content, _) = gen_bytes(op.info().full_capability());
+    assert_ne!(source_content, target_content);
+    op.write(&target_path, target_content.clone()).await?;
+
+    let Some(etag) = op.stat(&target_path).await?.etag().map(|s| 
s.to_string()) else {
+        op.delete(&source_path).await.expect("delete must succeed");
+        op.delete(&target_path).await.expect("delete must succeed");
+        return Ok(());
+    };
+
+    op.copy_with(&source_path, &target_path)
+        .if_match(&etag)
+        .await?;
+
+    let current_content = op
+        .read(&target_path)
+        .await
+        .expect("read must succeed")
+        .to_bytes();
+    assert_eq!(
+        sha256_digest(current_content),
+        sha256_digest(&source_content),
+    );
+
+    op.delete(&source_path).await.expect("delete must succeed");
+    op.delete(&target_path).await.expect("delete must succeed");
+    Ok(())
+}
+
+/// Copy with if_match not matching the destination ETag should fail.
+pub async fn test_copy_with_if_match_mismatch(op: Operator) -> Result<()> {
+    if !op.info().full_capability().copy_with_if_match {
+        return Ok(());
+    }
+
+    let source_path = uuid::Uuid::new_v4().to_string();
+    let (source_content, _) = gen_bytes(op.info().full_capability());
+    op.write(&source_path, source_content.clone()).await?;
+
+    let target_path = uuid::Uuid::new_v4().to_string();
+    let (target_content, _) = gen_bytes(op.info().full_capability());
+    assert_ne!(source_content, target_content);
+    op.write(&target_path, target_content.clone()).await?;
+
+    let err = op
+        .copy_with(&source_path, &target_path)
+        .if_match("\"00000000000000000000000000000000\"")
+        .await
+        .expect_err("copy must fail");
+    assert_eq!(err.kind(), ErrorKind::ConditionNotMatch);
+
+    let current_content = op
+        .read(&target_path)
+        .await
+        .expect("read must succeed")
+        .to_bytes();
+    assert_eq!(
+        sha256_digest(current_content),
+        sha256_digest(&target_content),
+    );
+
+    op.delete(&source_path).await.expect("delete must succeed");
+    op.delete(&target_path).await.expect("delete must succeed");
+    Ok(())
+}
+
 /// Copy with chunk should copy a file successfully.
 pub async fn test_copy_with_chunk(op: Operator) -> Result<()> {
     let cap = op.info().full_capability();

Reply via email to