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();