diff --git a/bindings/java/src/blocking_operator.rs b/bindings/java/src/blocking_operator.rs index c7fb4a076e8d..1f4e9240d894 100644 --- a/bindings/java/src/blocking_operator.rs +++ b/bindings/java/src/blocking_operator.rs @@ -150,3 +150,31 @@ fn intern_create_dir(env: &mut JNIEnv, op: &mut BlockingOperator, path: JString) let path = jstring_to_string(env, &path)?; Ok(op.create_dir(&path)?) } + +/// # Safety +/// +/// This function should not be called before the Operator are ready. +#[no_mangle] +pub unsafe extern "system" fn Java_org_apache_opendal_BlockingOperator_copy( + mut env: JNIEnv, + _: JClass, + op: *mut BlockingOperator, + source_path: JString, + target_path: JString, +) { + intern_copy(&mut env, &mut *op, source_path, target_path).unwrap_or_else(|e| { + e.throw(&mut env); + }) +} + +fn intern_copy( + env: &mut JNIEnv, + op: &mut BlockingOperator, + source_path: JString, + target_path: JString, +) -> Result<()> { + let source_path = jstring_to_string(env, &source_path)?; + let target_path = jstring_to_string(env, &target_path)?; + + Ok(op.copy(&source_path, &target_path)?) +} diff --git a/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java b/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java index 1c30f3ebc555..811f3d6b9bc2 100644 --- a/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java +++ b/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java @@ -74,6 +74,10 @@ public void createDir(String path) { createDir(nativeHandle, path); } + public void copy(String sourcePath, String targetPath) { + copy(nativeHandle, sourcePath, targetPath); + } + @Override protected native void disposeInternal(long handle); @@ -86,4 +90,6 @@ public void createDir(String path) { private static native long stat(long nativeHandle, String path); private static native long createDir(long nativeHandle, String path); + + private static native long copy(long nativeHandle, String sourcePath, String targetPath); } diff --git a/bindings/java/src/main/java/org/apache/opendal/Operator.java b/bindings/java/src/main/java/org/apache/opendal/Operator.java index 9a1f46a7d80e..8c791aeddadf 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Operator.java +++ b/bindings/java/src/main/java/org/apache/opendal/Operator.java @@ -182,6 +182,11 @@ public CompletableFuture createDir(String path) { return AsyncRegistry.take(requestId); } + public CompletableFuture copy(String sourcePath, String targetPath) { + final long requestId = copy(nativeHandle, sourcePath, targetPath); + return AsyncRegistry.take(requestId); + } + @Override protected native void disposeInternal(long handle); @@ -208,4 +213,6 @@ public CompletableFuture createDir(String path) { private static native long makeBlockingOp(long nativeHandle); private static native long createDir(long nativeHandle, String path); + + private static native long copy(long nativeHandle, String sourcePath, String targetPath); } diff --git a/bindings/java/src/operator.rs b/bindings/java/src/operator.rs index 3244ac0adf33..74e0b9c301bb 100644 --- a/bindings/java/src/operator.rs +++ b/bindings/java/src/operator.rs @@ -331,6 +331,47 @@ async fn do_create_dir(op: &mut Operator, path: String) -> Result<()> { Ok(op.create_dir(&path).await?) } +/// # Safety +/// +/// This function should not be called before the Operator are ready. +#[no_mangle] +pub unsafe extern "system" fn Java_org_apache_opendal_Operator_copy( + mut env: JNIEnv, + _: JClass, + op: *mut Operator, + source_path: JString, + target_path: JString, +) -> jlong { + intern_copy(&mut env, op, source_path, target_path).unwrap_or_else(|e| { + e.throw(&mut env); + 0 + }) +} + +fn intern_copy( + env: &mut JNIEnv, + op: *mut Operator, + source_path: JString, + target_path: JString, +) -> Result { + let op = unsafe { &mut *op }; + let id = request_id(env)?; + + let source_path = jstring_to_string(env, &source_path)?; + let target_path = jstring_to_string(env, &target_path)?; + + unsafe { get_global_runtime() }.spawn(async move { + let result = do_copy(op, source_path, target_path).await; + complete_future(id, result.map(|_| JValueOwned::Void)) + }); + + Ok(id) +} + +async fn do_copy(op: &mut Operator, source_path: String, target_path: String) -> Result<()> { + Ok(op.copy(&source_path, &target_path).await?) +} + /// # Safety /// /// This function should not be called before the Operator are ready. diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java index df594fddb7d7..ee7c5721b2a5 100644 --- a/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java @@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assumptions.assumeTrue; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvEntry; @@ -218,6 +219,162 @@ public void testCreateDirExisting() { } } + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Nested + class AsyncCopyTest { + @BeforeAll + public void precondition() { + final Capability capability = operator.info.fullCapability; + assumeTrue(capability.read && capability.write && capability.copy); + } + + /** + * Copy a file with ascii name and test contents. + */ + @Test + public void testCopyFileWithAsciiName() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + operator.write(sourcePath, sourceContent).join(); + + final String targetPath = UUID.randomUUID().toString(); + + operator.copy(sourcePath, targetPath).join(); + + assertThat(operator.read(targetPath).join()).isEqualTo(sourceContent); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + + /** + * Copy a file with non ascii name and test contents. + */ + @Test + public void testCopyFileWithNonAsciiName() { + final String sourcePath = "🐂🍺中文.docx"; + final String targetPath = "😈🐅Français.docx"; + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + operator.copy(sourcePath, targetPath).join(); + + assertThat(operator.read(targetPath).join()).isEqualTo(content); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + + /** + * Copy a nonexistent source should return an error. + */ + @Test + public void testCopyNonExistingSource() { + final String sourcePath = UUID.randomUUID().toString(); + final String targetPath = UUID.randomUUID().toString(); + + assertThatThrownBy(() -> operator.copy(sourcePath, targetPath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + } + + /** + * Copy a dir as source should return an error. + */ + @Test + public void testCopySourceDir() { + final String sourcePath = String.format("%s/", UUID.randomUUID().toString()); + final String targetPath = UUID.randomUUID().toString(); + + assertThatThrownBy(() -> operator.copy(sourcePath, targetPath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + } + + /** + * Copy to a dir should return an error. + */ + @Test + public void testCopyTargetDir() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + + final String targetPath = String.format("%s/", UUID.randomUUID().toString()); + operator.createDir(targetPath).join(); + + assertThatThrownBy(() -> operator.copy(sourcePath, targetPath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + + /** + * Copy a file to self should return an error. + */ + @Test + public void testCopySelf() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + + assertThatThrownBy(() -> operator.copy(sourcePath, sourcePath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsSameFile)); + + operator.delete(sourcePath).join(); + } + + /** + * Copy to a nested path, parent path should be created successfully. + */ + @Test + public void testCopyNested() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + + final String targetPath = String.format( + "%s/%s/%s", + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + UUID.randomUUID().toString()); + + operator.copy(sourcePath, targetPath).join(); + + assertThat(operator.read(targetPath).join()).isEqualTo(content); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + + /** + * Copy to a exist path should overwrite successfully. + */ + @Test + public void testCopyOverwrite() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + operator.write(sourcePath, sourceContent).join(); + + final String targetPath = UUID.randomUUID().toString(); + final byte[] targetContent = generateBytes(); + assertNotEquals(sourceContent, targetContent); + + operator.write(targetPath, targetContent).join(); + + operator.copy(sourcePath, targetPath).join(); + + assertThat(operator.read(targetPath).join()).isEqualTo(sourceContent); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + } + @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Nested class BlockingWriteTest { @@ -305,6 +462,149 @@ public void testBlockingDirExisting() { } } + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Nested + class BlockingCopyTest { + @BeforeAll + public void precondition() { + final Capability capability = blockingOperator.info.fullCapability; + assumeTrue(capability.read && capability.write && capability.copy); + } + + /** + * Copy a file and test with stat. + */ + @Test + public void testBlockingCopyFile() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + final String targetPath = UUID.randomUUID().toString(); + + blockingOperator.copy(sourcePath, targetPath); + + assertThat(blockingOperator.read(targetPath)).isEqualTo(sourceContent); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + + /** + * Copy a nonexistent source should return an error. + */ + @Test + public void testBlockingCopyNonExistingSource() { + final String sourcePath = UUID.randomUUID().toString(); + final String targetPath = UUID.randomUUID().toString(); + + assertThatThrownBy(() -> blockingOperator.copy(sourcePath, targetPath)) + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.NotFound)); + } + + /** + * Copy a dir as source should return an error. + */ + @Test + public void testBlockingCopySourceDir() { + final String sourcePath = String.format("%s/", UUID.randomUUID().toString()); + final String targetPath = UUID.randomUUID().toString(); + + blockingOperator.createDir(sourcePath); + + assertThatThrownBy(() -> blockingOperator.copy(sourcePath, targetPath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + + blockingOperator.delete(sourcePath); + } + + /** + * Copy to a dir should return an error. + */ + @Test + public void testBlockingCopyTargetDir() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + final String targetPath = String.format("%s/", UUID.randomUUID().toString()); + + blockingOperator.createDir(targetPath); + + assertThatThrownBy(() -> operator.copy(sourcePath, targetPath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + + /** + * Copy a file to self should return an error. + */ + @Test + public void testBlockingCopySelf() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + assertThatThrownBy(() -> blockingOperator.copy(sourcePath, sourcePath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsSameFile)); + + blockingOperator.delete(sourcePath); + } + + /** + * Copy to a nested path, parent path should be created successfully. + */ + @Test + public void testBlockingCopyNested() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + blockingOperator.write(sourcePath, content); + + final String targetPath = String.format( + "%s/%s/%s", + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + UUID.randomUUID().toString()); + + blockingOperator.copy(sourcePath, targetPath); + + assertThat(blockingOperator.read(targetPath)).isEqualTo(content); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + + /** + * Copy to a exist path should overwrite successfully. + */ + @Test + public void testBlockingCopyOverwrite() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + final String targetPath = UUID.randomUUID().toString(); + final byte[] targetContent = generateBytes(); + assertNotEquals(sourceContent, targetContent); + + blockingOperator.write(targetPath, targetContent); + + blockingOperator.copy(sourcePath, targetPath); + + assertThat(blockingOperator.read(targetPath)).isEqualTo(sourceContent); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + } + /** * Generates a byte array of random content. */