This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new fa54e5707b Fix moveTo not working in all situations, fixes #5936
(#7918)
fa54e5707b is described below
commit fa54e5707b282299929451bb0135ee3cc17b61f3
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Aug 13 08:40:04 2026 +0200
Fix moveTo not working in all situations, fixes #5936 (#7918)
---
.../hop/core/config/ConfigFileSerializer.java | 2 +-
.../main/java/org/apache/hop/core/vfs/HopVfs.java | 33 +++
.../apache/hop/core/vfs/HopVfsMoveFileTest.java | 142 ++++++++++
.../hop/junit/vfs/CrossDeviceFileProvider.java | 76 +++++
.../apache/hop/execution/caching/CacheEntry.java | 2 +-
.../workflow/actions/checkfilelocked/LockFile.java | 2 +
.../ActionCopyMoveResultFilenames.java | 2 +-
.../actions/movefiles/ActionMoveFiles.java | 12 +-
.../pgpdecryptfiles/ActionPGPDecryptFiles.java | 6 +-
.../pgpencryptfiles/ActionPGPEncryptFiles.java | 6 +-
.../hop/workflow/actions/unzip/ActionUnZip.java | 2 +-
.../workflow/actions/zipfile/ActionZipFile.java | 2 +-
.../hop/pipeline/transforms/sftpput/SftpPut.java | 34 +--
.../workflow/actions/sftpput/ActionSftpPut.java | 2 +-
.../actions/sftpput/ActionSftpPutMoveTest.java | 308 +++++++++++++++++++++
.../javascript/ScriptValuesAddedFunctions.java | 2 +-
.../transforms/processfiles/ProcessFiles.java | 2 +-
.../hop/pipeline/transforms/zipfile/ZipFile.java | 2 +-
.../apache/hop/ui/core/vfs/HopVfsFileDialog.java | 2 +-
.../perspective/explorer/ExplorerPerspective.java | 8 +-
20 files changed, 599 insertions(+), 48 deletions(-)
diff --git
a/core/src/main/java/org/apache/hop/core/config/ConfigFileSerializer.java
b/core/src/main/java/org/apache/hop/core/config/ConfigFileSerializer.java
index 08598ced73..4c6d9c8cc7 100644
--- a/core/src/main/java/org/apache/hop/core/config/ConfigFileSerializer.java
+++ b/core/src/main/java/org/apache/hop/core/config/ConfigFileSerializer.java
@@ -73,7 +73,7 @@ public class ConfigFileSerializer implements
IHopConfigSerializer {
// Now rename the new file to the final value...
//
- newFile.moveTo(file);
+ HopVfs.moveFile(newFile, file);
} catch (Exception e) {
throw new HopException("Error writing to Hop configuration file : " +
filename, e);
}
diff --git a/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
b/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
index 205ca8e0be..6eb3f7d575 100644
--- a/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
+++ b/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
@@ -35,6 +35,7 @@ import org.apache.commons.vfs2.FileContent;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.Selectors;
import org.apache.commons.vfs2.cache.SoftRefFilesCache;
import org.apache.commons.vfs2.impl.DefaultFileReplicator;
import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
@@ -510,6 +511,38 @@ public class HopVfs {
}
}
+ /**
+ * Move a file or a folder, also when the source and the destination sit on
two different file
+ * systems of the operating system.
+ *
+ * <p>{@link FileObject#moveTo(FileObject)} only copies and deletes when the
two files belong to
+ * two different VFS file systems. Two local folders are one and the same
VFS file system
+ * whichever disks they're on, so it always picks a rename, and a rename
across a mount point is
+ * exactly what the operating system refuses. Everything in Hop that moves a
file to a folder the
+ * user picked has to go through here, or it breaks the moment those two
folders are two different
+ * mounts. See <a href="https://github.com/apache/hop/issues/5936">issue
#5936</a>.
+ *
+ * @param source the file or folder to move
+ * @param destination where to move it to
+ * @throws FileSystemException when the move failed. When the copy which
follows a failed rename
+ * fails as well, this is the original rename failure with the copy
failure suppressed.
+ */
+ public static void moveFile(FileObject source, FileObject destination)
+ throws FileSystemException {
+ try {
+ source.moveTo(destination);
+ } catch (FileSystemException renameFailed) {
+ try {
+ // SELECT_ALL rather than SELECT_SELF: a folder has to arrive with its
children.
+ destination.copyFrom(source, Selectors.SELECT_ALL);
+ source.deleteAll();
+ } catch (FileSystemException copyFailed) {
+ renameFailed.addSuppressed(copyFailed);
+ throw renameFailed;
+ }
+ }
+ }
+
/**
* Utility to normalize file name depending on OS.
*
diff --git a/core/src/test/java/org/apache/hop/core/vfs/HopVfsMoveFileTest.java
b/core/src/test/java/org/apache/hop/core/vfs/HopVfsMoveFileTest.java
new file mode 100644
index 0000000000..4734aa2005
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/vfs/HopVfsMoveFileTest.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.core.vfs;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.hop.junit.vfs.CrossDeviceFileProvider;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * {@link HopVfs#moveFile(FileObject, FileObject)} is what everything in Hop
which moves a file has
+ * to use, because {@link FileObject#moveTo(FileObject)} on its own can't move
a file out of one
+ * mount into another - see <a
href="https://github.com/apache/hop/issues/5936">issue #5936</a>.
+ *
+ * <p>{@link CrossDeviceFileProvider} stands in for those two mounts: one VFS
file system, on which
+ * a rename never succeeds.
+ */
+class HopVfsMoveFileTest {
+
+ private static final String CROSS_DEVICE = "xdev";
+ private static final String PAYLOAD = "id;amount\n1;42\n";
+
+ @TempDir private Path tempDir;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ HopVfs.getFileSystemManager().addProvider(CROSS_DEVICE, new
CrossDeviceFileProvider());
+ }
+
+ @AfterEach
+ void tearDown() {
+ // The provider above is registered on the one and only file system
manager.
+ HopVfs.reset();
+ }
+
+ @Test
+ @DisplayName("a file lands in the destination folder when the rename fails")
+ void movesAFileWhenTheRenameFails() throws Exception {
+ FileObject source = write("xdev:///work/sales.csv", PAYLOAD);
+ FileObject destination = HopVfs.getFileObject("xdev:///archive/sales.csv");
+
+ HopVfs.moveFile(source, destination);
+
+ assertTrue(destination.exists(), "the file was not moved");
+ assertFalse(source.exists(), "the file is still in the source folder");
+ assertEquals(PAYLOAD, read(destination));
+ }
+
+ /** What the file system manager hands out for the destination folder is the
folder itself. */
+ @Test
+ @DisplayName("a folder arrives with its children when the rename fails")
+ void movesAFolderWithItsChildren() throws Exception {
+ write("xdev:///work/orders/january.csv", PAYLOAD);
+ write("xdev:///work/orders/details/lines.csv", PAYLOAD);
+ FileObject source = HopVfs.getFileObject("xdev:///work/orders");
+ FileObject destination = HopVfs.getFileObject("xdev:///archive/orders");
+
+ HopVfs.moveFile(source, destination);
+
+ assertFalse(source.exists(), "the folder is still in the source folder");
+ assertEquals(PAYLOAD,
read(HopVfs.getFileObject("xdev:///archive/orders/january.csv")));
+ assertEquals(PAYLOAD,
read(HopVfs.getFileObject("xdev:///archive/orders/details/lines.csv")));
+ }
+
+ /** A rename which does work is left alone: no copy, no second file. */
+ @Test
+ @DisplayName("a file is renamed when the file system can rename")
+ void renamesAFileWhenTheFileSystemCan() throws Exception {
+ Path work = Files.createDirectory(tempDir.resolve("work"));
+ Path archive = Files.createDirectory(tempDir.resolve("archive"));
+ Files.writeString(work.resolve("sales.csv"), PAYLOAD);
+
+ HopVfs.moveFile(
+ HopVfs.getFileObject(work.resolve("sales.csv").toString()),
+ HopVfs.getFileObject(archive.resolve("sales.csv").toString()));
+
+ assertTrue(Files.exists(archive.resolve("sales.csv")), "the file was not
moved");
+ assertFalse(Files.exists(work.resolve("sales.csv")), "the file is still in
the source folder");
+ assertArrayEquals(
+ PAYLOAD.getBytes(UTF_8),
Files.readAllBytes(archive.resolve("sales.csv")), "content");
+ }
+
+ /**
+ * When the copy can't save the move either, the caller gets the failure of
the move it asked for,
+ * with the failure of the copy alongside it.
+ */
+ @Test
+ @DisplayName("the rename failure is what's reported when the copy fails too")
+ void reportsTheRenameFailureWhenTheCopyFailsToo() throws Exception {
+ FileObject source = HopVfs.getFileObject("xdev:///work/gone.csv");
+ FileObject destination = HopVfs.getFileObject("xdev:///archive/gone.csv");
+
+ FileSystemException e =
+ assertThrows(FileSystemException.class, () -> HopVfs.moveFile(source,
destination));
+
+ assertTrue(e.getMessage().contains("rename"), e.getMessage());
+ assertEquals(1, e.getSuppressed().length, "the copy failure should come
along");
+ }
+
+ private static FileObject write(String uri, String content) throws Exception
{
+ FileObject fileObject = HopVfs.getFileObject(uri);
+ try (OutputStream outputStream =
fileObject.getContent().getOutputStream()) {
+ outputStream.write(content.getBytes(UTF_8));
+ }
+ return fileObject;
+ }
+
+ private static String read(FileObject fileObject) throws Exception {
+ try (InputStream inputStream = fileObject.getContent().getInputStream()) {
+ return new String(inputStream.readAllBytes(), UTF_8);
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/hop/junit/vfs/CrossDeviceFileProvider.java
b/core/src/test/java/org/apache/hop/junit/vfs/CrossDeviceFileProvider.java
new file mode 100644
index 0000000000..f39ea83826
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/junit/vfs/CrossDeviceFileProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.junit.vfs;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystem;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.AbstractFileName;
+import org.apache.commons.vfs2.provider.ram.RamFileObject;
+import org.apache.commons.vfs2.provider.ram.RamFileProvider;
+import org.apache.commons.vfs2.provider.ram.RamFileSystem;
+
+/**
+ * An in-memory file system on which a rename never succeeds, which is what
the operating system
+ * does with a rename across a mount point. Everything else - reading,
writing, listing, deleting -
+ * behaves as usual, so the only thing which can go wrong is the move itself.
+ *
+ * <p>Two folders on this file system stand in for two local folders on two
different mounts: one
+ * VFS file system, so {@link FileObject#moveTo(FileObject)} picks a rename,
and a rename which
+ * fails. Register it on the file system manager under a scheme of your own
and resolve through it:
+ *
+ * <pre>
+ * HopVfs.getFileSystemManager().addProvider("xdev", new
CrossDeviceFileProvider());
+ * HopVfs.getFileObject("xdev:///work/sales.csv");
+ * </pre>
+ *
+ * Registering is for the one and only file system manager, so call {@code
HopVfs.reset()} when the
+ * test is done. See <a href="https://github.com/apache/hop/issues/5936">issue
#5936</a>.
+ */
+public class CrossDeviceFileProvider extends RamFileProvider {
+
+ @Override
+ protected FileSystem doCreateFileSystem(FileName rootName, FileSystemOptions
fileSystemOptions) {
+ return new CrossDeviceFileSystem(rootName, fileSystemOptions);
+ }
+
+ private static class CrossDeviceFileSystem extends RamFileSystem {
+ CrossDeviceFileSystem(FileName rootName, FileSystemOptions
fileSystemOptions) {
+ super(rootName, fileSystemOptions);
+ }
+
+ @Override
+ protected FileObject createFile(AbstractFileName name) {
+ return new CrossDeviceFileObject(name, this);
+ }
+ }
+
+ private static class CrossDeviceFileObject extends RamFileObject {
+ CrossDeviceFileObject(AbstractFileName name, RamFileSystem fileSystem) {
+ super(name, fileSystem);
+ }
+
+ @Override
+ protected void doRename(FileObject newFile) throws FileSystemException {
+ // Errno EXDEV, which LocalFile reports the same way: File.renameTo()
returns false.
+ throw new FileSystemException(
+ "vfs.provider.local/rename-file.error", getName(),
newFile.getName());
+ }
+ }
+}
diff --git
a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
index 43b792e30b..68bb027de8 100644
--- a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
+++ b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
@@ -129,7 +129,7 @@ public class CacheEntry {
targetFileObject.delete();
}
FileObject fileObject = HopVfs.getFileObject(filename, variables);
- fileObject.moveTo(targetFileObject);
+ HopVfs.moveFile(fileObject, targetFileObject);
} catch (Exception e) {
throw new HopException(
"Error renaming execution information to file '" + targetFilename +
"'", e);
diff --git
a/plugins/actions/checkfilelocked/src/main/java/org/apache/hop/workflow/actions/checkfilelocked/LockFile.java
b/plugins/actions/checkfilelocked/src/main/java/org/apache/hop/workflow/actions/checkfilelocked/LockFile.java
index a6e897f2b6..1752041715 100644
---
a/plugins/actions/checkfilelocked/src/main/java/org/apache/hop/workflow/actions/checkfilelocked/LockFile.java
+++
b/plugins/actions/checkfilelocked/src/main/java/org/apache/hop/workflow/actions/checkfilelocked/LockFile.java
@@ -52,6 +52,8 @@ public class LockFile {
if (file.exists()) {
dummyfile = HopVfs.getFileObject(filename, variables);
// move file to itself!
+ // Not HopVfs.moveFile(): here the failing rename is the answer we
came for, and its
+ // fallback would copy this file onto itself and then delete it.
file.moveTo(dummyfile);
}
} catch (Exception e) {
diff --git
a/plugins/actions/copymoveresultfilenames/src/main/java/org/apache/hop/workflow/actions/copymoveresultfilenames/ActionCopyMoveResultFilenames.java
b/plugins/actions/copymoveresultfilenames/src/main/java/org/apache/hop/workflow/actions/copymoveresultfilenames/ActionCopyMoveResultFilenames.java
index d425336ca7..4db726d90f 100644
---
a/plugins/actions/copymoveresultfilenames/src/main/java/org/apache/hop/workflow/actions/copymoveresultfilenames/ActionCopyMoveResultFilenames.java
+++
b/plugins/actions/copymoveresultfilenames/src/main/java/org/apache/hop/workflow/actions/copymoveresultfilenames/ActionCopyMoveResultFilenames.java
@@ -410,7 +410,7 @@ public class ActionCopyMoveResultFilenames extends
ActionBase implements Cloneab
}
} else {
// Move file
- sourceFile.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFile, destinationFile);
nrSuccess++;
if (isDetailed()) {
logDetailed(
diff --git
a/plugins/actions/movefiles/src/main/java/org/apache/hop/workflow/actions/movefiles/ActionMoveFiles.java
b/plugins/actions/movefiles/src/main/java/org/apache/hop/workflow/actions/movefiles/ActionMoveFiles.java
index fe12175f68..1182d6df37 100644
---
a/plugins/actions/movefiles/src/main/java/org/apache/hop/workflow/actions/movefiles/ActionMoveFiles.java
+++
b/plugins/actions/movefiles/src/main/java/org/apache/hop/workflow/actions/movefiles/ActionMoveFiles.java
@@ -716,7 +716,7 @@ public class ActionMoveFiles extends ActionBase implements
Cloneable, IAction {
if (!simulate) {
Long moved = trackBytesMoved(sourceFileFolder, result);
destinationFilename.createFile();
- sourceFileFolder.moveTo(destinationFilename);
+ HopVfs.moveFile(sourceFileFolder, destinationFilename);
emitMoveLineage(parentWorkflow, sourceFileFolder,
destinationFilename, moved);
}
@@ -748,7 +748,7 @@ public class ActionMoveFiles extends ActionBase implements
Cloneable, IAction {
case "overwrite_file" -> {
if (!simulate) {
Long moved = trackBytesMoved(sourceFileFolder, result);
- sourceFileFolder.moveTo(destinationFilename);
+ HopVfs.moveFile(sourceFileFolder, destinationFilename);
emitMoveLineage(parentWorkflow, sourceFileFolder,
destinationFilename, moved);
}
if (isDetailed()) {
@@ -794,7 +794,7 @@ public class ActionMoveFiles extends ActionBase implements
Cloneable, IAction {
if (!simulate) {
Long moved = trackBytesMoved(sourceFileFolder, result);
- sourceFileFolder.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileFolder, destinationFile);
emitMoveLineage(parentWorkflow, sourceFileFolder,
destinationFile, moved);
}
if (isDetailed()) {
@@ -853,7 +853,7 @@ public class ActionMoveFiles extends ActionBase implements
Cloneable, IAction {
if (!destinationFile.exists()) {
if (!simulate) {
Long moved = trackBytesMoved(sourceFileFolder, result);
- sourceFileFolder.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileFolder, destinationFile);
emitMoveLineage(parentWorkflow, sourceFileFolder,
destinationFile, moved);
}
if (isDetailed()) {
@@ -875,7 +875,7 @@ public class ActionMoveFiles extends ActionBase implements
Cloneable, IAction {
case "overwrite_file" -> {
if (!simulate) {
Long moved = trackBytesMoved(sourceFileFolder, result);
- sourceFileFolder.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileFolder, destinationFile);
emitMoveLineage(parentWorkflow, sourceFileFolder,
destinationFile, moved);
}
if (isDetailed()) {
@@ -909,7 +909,7 @@ public class ActionMoveFiles extends ActionBase implements
Cloneable, IAction {
if (!simulate) {
Long moved = trackBytesMoved(sourceFileFolder, result);
- sourceFileFolder.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileFolder, destinationFile);
emitMoveLineage(parentWorkflow, sourceFileFolder,
destinationFile, moved);
}
if (isDetailed()) {
diff --git
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpdecryptfiles/ActionPGPDecryptFiles.java
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpdecryptfiles/ActionPGPDecryptFiles.java
index 084472e471..de6254ca78 100644
---
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpdecryptfiles/ActionPGPDecryptFiles.java
+++
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpdecryptfiles/ActionPGPDecryptFiles.java
@@ -805,7 +805,7 @@ public class ActionPGPDecryptFiles extends ActionBase
implements Cloneable, IAct
moveToFolderFolder.toString() + Const.FILE_SEPARATOR +
shortFilename;
destinationFile = HopVfs.getFileObject(moveToFileNameFull,
getVariables());
if (!destinationFile.exists()) {
- sourceFileName.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileName, destinationFile);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
@@ -825,7 +825,7 @@ public class ActionPGPDecryptFiles extends ActionBase
implements Cloneable, IAct
} else {
switch (ifMovedFileExists) {
case CONST_OVERWRITE_FILE -> {
- sourceFileName.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileName, destinationFile);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
@@ -854,7 +854,7 @@ public class ActionPGPDecryptFiles extends ActionBase
implements Cloneable, IAct
moveToFolderFolder + Const.FILE_SEPARATOR +
shortFilename;
destinationFile =
HopVfs.getFileObject(destinationFileNameFull, getVariables());
- sourceFileName.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileName, destinationFile);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
diff --git
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
index 294ae8e5f5..3fa261c892 100644
---
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
+++
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
@@ -854,7 +854,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
moveToFolderFolder.toString() + Const.FILE_SEPARATOR +
shortFilename;
destinationFile = HopVfs.getFileObject(moveToFileNameFull,
getVariables());
if (!destinationFile.exists()) {
- sourceFileName.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileName, destinationFile);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
@@ -874,7 +874,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
} else {
switch (ifMovedFileExists) {
case "overwrite_file" -> {
- sourceFileName.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileName, destinationFile);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
@@ -903,7 +903,7 @@ public class ActionPGPEncryptFiles extends ActionBase
implements Cloneable, IAct
moveToFolderFolder.toString() + Const.FILE_SEPARATOR +
shortFilename;
destinationFile =
HopVfs.getFileObject(destinationfilenamefull, getVariables());
- sourceFileName.moveTo(destinationFile);
+ HopVfs.moveFile(sourceFileName, destinationFile);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
diff --git
a/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java
b/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java
index 921357d62d..3bef8898a2 100644
---
a/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java
+++
b/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java
@@ -907,7 +907,7 @@ public class ActionUnZip extends ActionBase implements
Cloneable, IAction {
true,
null);
- sourceFileObject.moveTo(destFile);
+ HopVfs.moveFile(sourceFileObject, destFile);
if (archiveSize > 0) {
result.setBytesWrittenThisAction(result.getBytesWrittenThisAction()
+ archiveSize);
diff --git
a/plugins/actions/zipfile/src/main/java/org/apache/hop/workflow/actions/zipfile/ActionZipFile.java
b/plugins/actions/zipfile/src/main/java/org/apache/hop/workflow/actions/zipfile/ActionZipFile.java
index 74763a70bd..d0717e3bd8 100644
---
a/plugins/actions/zipfile/src/main/java/org/apache/hop/workflow/actions/zipfile/ActionZipFile.java
+++
b/plugins/actions/zipfile/src/main/java/org/apache/hop/workflow/actions/zipfile/ActionZipFile.java
@@ -683,7 +683,7 @@ public class ActionZipFile extends ActionBase implements
Cloneable, IAction {
realMoveToDirectory + Const.FILE_SEPARATOR +
fileObjectd.getName().getBaseName(),
getVariables())) {
- fileObjectd.moveTo(fileObjectm);
+ HopVfs.moveFile(fileObjectd, fileObjectm);
} catch (Exception e) {
logError(
BaseMessages.getString(PKG, "ActionZipFile.Cant_Move_File1.Label")
diff --git
a/plugins/tech/sftp/src/main/java/org/apache/hop/pipeline/transforms/sftpput/SftpPut.java
b/plugins/tech/sftp/src/main/java/org/apache/hop/pipeline/transforms/sftpput/SftpPut.java
index afa0ebcad9..29ce23bf3e 100644
---
a/plugins/tech/sftp/src/main/java/org/apache/hop/pipeline/transforms/sftpput/SftpPut.java
+++
b/plugins/tech/sftp/src/main/java/org/apache/hop/pipeline/transforms/sftpput/SftpPut.java
@@ -22,7 +22,6 @@ import java.io.OutputStream;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.NameScope;
-import org.apache.commons.vfs2.Selectors;
import org.apache.hop.core.Const;
import org.apache.hop.core.ResultFile;
import org.apache.hop.core.exception.HopException;
@@ -369,30 +368,21 @@ public class SftpPut extends BaseTransform<SftpPutMeta,
SftpPutData> {
}
/**
- * Move a file, also when the two folders sit on different file systems of
the operating system.
- *
- * <p>Two local folders are one and the same file system as far as VFS is
concerned, whichever
- * disks they're on, so {@link FileObject#moveTo(FileObject)} always picks a
rename. A rename
- * across a mount point is exactly what the OS refuses to do, so fall back
to a copy followed by a
- * delete. See <a href="https://github.com/apache/hop/issues/5936">issue
#5936</a>.
+ * Move a file, saying which file it was about when that doesn't work. {@link
+ * HopVfs#moveFile(FileObject, FileObject)} is what gets it across two file
systems of the
+ * operating system.
*/
private void move(FileObject sourceFile, FileObject destination) throws
HopException {
try {
- sourceFile.moveTo(destination);
- } catch (FileSystemException renameFailed) {
- try {
- destination.copyFrom(sourceFile, Selectors.SELECT_SELF);
- sourceFile.delete();
- } catch (Exception copyFailed) {
- renameFailed.addSuppressed(copyFailed);
- throw new HopException(
- BaseMessages.getString(
- PKG,
- "SftpPut.Error.UnableToMove",
- sourceFile.getPublicURIString(),
- destination.getPublicURIString()),
- renameFailed);
- }
+ HopVfs.moveFile(sourceFile, destination);
+ } catch (FileSystemException moveFailed) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG,
+ "SftpPut.Error.UnableToMove",
+ sourceFile.getPublicURIString(),
+ destination.getPublicURIString()),
+ moveFailed);
}
}
diff --git
a/plugins/tech/sftp/src/main/java/org/apache/hop/workflow/actions/sftpput/ActionSftpPut.java
b/plugins/tech/sftp/src/main/java/org/apache/hop/workflow/actions/sftpput/ActionSftpPut.java
index d469e09e57..27fb89a95c 100644
---
a/plugins/tech/sftp/src/main/java/org/apache/hop/workflow/actions/sftpput/ActionSftpPut.java
+++
b/plugins/tech/sftp/src/main/java/org/apache/hop/workflow/actions/sftpput/ActionSftpPut.java
@@ -543,7 +543,7 @@ public class ActionSftpPut extends ActionBase implements
Cloneable, IAction {
realDestinationFolder
+ Const.FILE_SEPARATOR
+ myFile.getName().getBaseName());
- myFile.moveTo(destination);
+ HopVfs.moveFile(myFile, destination);
if (isDetailed()) {
logDetailed(
BaseMessages.getString(
diff --git
a/plugins/tech/sftp/src/test/java/org/apache/hop/workflow/actions/sftpput/ActionSftpPutMoveTest.java
b/plugins/tech/sftp/src/test/java/org/apache/hop/workflow/actions/sftpput/ActionSftpPutMoveTest.java
new file mode 100644
index 0000000000..ab7143ffcb
--- /dev/null
+++
b/plugins/tech/sftp/src/test/java/org/apache/hop/workflow/actions/sftpput/ActionSftpPutMoveTest.java
@@ -0,0 +1,308 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.workflow.actions.sftpput;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.FileStore;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.Collections;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.Selectors;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.junit.vfs.CrossDeviceFileProvider;
+import org.apache.hop.workflow.Workflow;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.actions.sftpput.ActionSftpPut.AfterFtpAction;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
+import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.apache.sshd.sftp.server.SftpSubsystemFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Issue <a href="https://github.com/apache/hop/issues/5936">#5936</a>: "move
the file after the
+ * upload" only worked when the source folder and the destination folder
happened to sit on the same
+ * file system of the operating system. On a machine where the working folder
and the archive folder
+ * are two different mounts the action ended with
+ *
+ * <pre>Could not rename "file:///files/work/x.csv" to
"file:///files/arc/x.csv"</pre>
+ *
+ * <p>{@link FileObject#moveTo(FileObject)} only falls back to a copy followed
by a delete when the
+ * two files belong to two different VFS file systems, and two local folders
are one and the same
+ * VFS file system whichever disk they're on - so it always picks a rename,
which is exactly what
+ * the operating system refuses across a mount point. Hence {@link
HopVfs#moveFile(FileObject,
+ * FileObject)}, which the action goes through.
+ *
+ * <p>The tests upload to an embedded Apache MINA SSHD server, so they
exercise the real action from
+ * end to end.
+ */
+class ActionSftpPutMoveTest {
+
+ /** Registers the plugins and initializes the engine, which the action needs
to run. */
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new
RestoreHopEngineEnvironmentExtension();
+
+ private static final String LOCALHOST = "127.0.0.1";
+ private static final String USER = "alice";
+ private static final String PASSWORD = "secret";
+ private static final String FILE_NAME = "sales.csv";
+ private static final String PAYLOAD = "id;amount\n1;42\n";
+
+ /**
+ * A scheme served by {@link CrossDeviceFileProvider}: one VFS file system,
on which a rename
+ * never succeeds. That is the situation of two local folders on two
different mounts.
+ */
+ private static final String CROSS_DEVICE = "xdev";
+
+ private static final String WORK_FOLDER = CROSS_DEVICE + ":///work";
+ private static final String ARCHIVE_FOLDER = CROSS_DEVICE + ":///archive";
+
+ /** Points at a folder on a different file system than the temporary folder,
when there is one. */
+ private static final String CROSS_DEVICE_DIR_PROPERTY =
"hop.test.cross.device.dir";
+
+ @TempDir private Path serverRoot;
+ @TempDir private Path localRoot;
+
+ private SshServer sshServer;
+
+ @BeforeEach
+ void startServer() throws IOException {
+ sshServer = SshServer.setUpDefaultServer();
+ sshServer.setHost(LOCALHOST);
+ sshServer.setPort(0);
+ sshServer.setKeyPairProvider(
+ new SimpleGeneratorHostKeyProvider(serverRoot.resolve("hostkey.ser")));
+
sshServer.setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
+ sshServer.setFileSystemFactory(new VirtualFileSystemFactory(serverRoot));
+ sshServer.setSubsystemFactories(Collections.singletonList(new
SftpSubsystemFactory()));
+ // The client keeps its session open, and the server's 10 minute default
idle timeout would
+ // hold the test open that long.
+ CoreModuleProperties.IDLE_TIMEOUT.set(sshServer, Duration.ofSeconds(5));
+ sshServer.start();
+
+ HopVfs.getFileSystemManager().addProvider(CROSS_DEVICE, new
CrossDeviceFileProvider());
+ }
+
+ @AfterEach
+ void stopServer() throws IOException {
+ if (sshServer != null) {
+ sshServer.stop();
+ }
+ // The provider above is registered on the one and only file system
manager.
+ HopVfs.reset();
+ }
+
+ /**
+ * The reproduction of the report: the file is uploaded and then has to be
moved to a folder on
+ * another file system, which is where the action used to give up.
+ */
+ @Test
+ @DisplayName("#5936: the uploaded file is moved to a folder on another file
system")
+ void movesTheUploadedFileToAnotherFileSystem() throws Exception {
+ writeFile(WORK_FOLDER + "/" + FILE_NAME);
+ HopVfs.getFileObject(ARCHIVE_FOLDER).createFolder();
+
+ ActionSftpPut action = action(WORK_FOLDER, ARCHIVE_FOLDER);
+ Result result = action.execute(new Result(), 0);
+
+ assertEquals(0, result.getNrErrors(), () -> "the action reported an
error:\n" + log(action));
+ assertTrue(Files.exists(serverRoot.resolve(FILE_NAME)), "the file never
reached the server");
+ assertTrue(
+ HopVfs.getFileObject(ARCHIVE_FOLDER + "/" + FILE_NAME).exists(),
+ () -> "the uploaded file was not moved to the destination folder:\n" +
log(action));
+ assertFalse(
+ HopVfs.getFileObject(WORK_FOLDER + "/" + FILE_NAME).exists(),
+ "the file is still in the source folder");
+ }
+
+ /**
+ * The same run with both folders on one file system, where the rename does
work. It tells apart
+ * "the move is broken" from "the move across a mount point is broken".
+ */
+ @Test
+ @DisplayName("the uploaded file is moved to a folder on the same file
system")
+ void movesTheUploadedFileWithinOneFileSystem() throws Exception {
+ Path work = Files.createDirectory(localRoot.resolve("work"));
+ Path archive = Files.createDirectory(localRoot.resolve("archive"));
+ Files.writeString(work.resolve(FILE_NAME), PAYLOAD);
+
+ ActionSftpPut action = action(work.toString(), archive.toString());
+ Result result = action.execute(new Result(), 0);
+
+ assertEquals(0, result.getNrErrors(), () -> "the action reported an
error:\n" + log(action));
+ assertTrue(Files.exists(serverRoot.resolve(FILE_NAME)), "the file never
reached the server");
+ assertTrue(Files.exists(archive.resolve(FILE_NAME)), "the uploaded file
was not moved");
+ assertFalse(Files.exists(work.resolve(FILE_NAME)), "the file is still in
the source folder");
+ }
+
+ /**
+ * The reproduction above once more, but on two genuinely different mounts
instead of a file
+ * system which refuses to rename. It needs a writable folder on a second
file store: {@code
+ * /dev/shm} on Linux, or any folder handed in through the {@code
hop.test.cross.device.dir}
+ * system property (a mounted RAM disk, a network share, a second volume).
Without one there is
+ * nothing to prove here and the test is skipped.
+ */
+ @Test
+ @DisplayName("#5936: the uploaded file is moved to a folder on a second real
mount")
+ void movesTheUploadedFileToASecondMount() throws Exception {
+ Path secondMount = secondFileStore();
+ assumeTrue(
+ secondMount != null,
+ "no writable folder on a second file store; set -D" +
CROSS_DEVICE_DIR_PROPERTY);
+
+ Path work = Files.createTempDirectory(secondMount, "hop-5936-work");
+ Path archive = Files.createDirectory(localRoot.resolve("archive"));
+ Files.writeString(work.resolve(FILE_NAME), PAYLOAD);
+
+ try {
+ ActionSftpPut action = action(work.toString(), archive.toString());
+ Result result = action.execute(new Result(), 0);
+
+ assertEquals(0, result.getNrErrors(), () -> "the action reported an
error:\n" + log(action));
+ assertTrue(Files.exists(serverRoot.resolve(FILE_NAME)), "the file never
reached the server");
+ assertTrue(Files.exists(archive.resolve(FILE_NAME)), "the uploaded file
was not moved");
+ assertFalse(Files.exists(work.resolve(FILE_NAME)), "the file is still in
the source folder");
+ } finally {
+ Files.deleteIfExists(work.resolve(FILE_NAME));
+ Files.deleteIfExists(work);
+ }
+ }
+
+ /**
+ * Why the action can't leave the move to VFS: two folders of one file
system always take the
+ * rename branch of {@link FileObject#moveTo(FileObject)}, and a failing
rename is the end of it -
+ * the copy plus delete branch is only for two different VFS file systems.
Two local folders are
+ * one VFS file system whichever disks they are on, so the fallback is never
reached, however far
+ * apart the two folders are.
+ */
+ @Test
+ @DisplayName("moveTo() renames within one file system and does not fall back
to a copy")
+ void moveToWithinOneFileSystemNeverFallsBackToACopy() throws Exception {
+ FileObject source = writeFile(WORK_FOLDER + "/" + FILE_NAME);
+ FileObject destination = HopVfs.getFileObject(ARCHIVE_FOLDER + "/" +
FILE_NAME);
+
+ assertTrue(
+ source.canRenameTo(destination),
+ "the two folders are one VFS file system, so moveTo() renames");
+
+ FileSystemException e =
+ assertThrows(FileSystemException.class, () ->
source.moveTo(destination));
+ assertTrue(e.getMessage().contains("Could not rename"), e.getMessage());
+ assertFalse(destination.exists(), "moveTo() did not fall back to a copy");
+
+ // Which is all HopVfs.moveFile() has to do to get the file across.
+ destination.copyFrom(source, Selectors.SELECT_SELF);
+ source.delete();
+
+ assertTrue(destination.exists());
+ assertFalse(source.exists());
+ }
+
+ /** Two local folders are one VFS file system, whichever disk each of them
is on. */
+ @Test
+ @DisplayName("two local folders are one VFS file system")
+ void twoLocalFoldersAreOneVfsFileSystem() throws Exception {
+ FileObject here = HopVfs.getFileObject(localRoot.toString());
+ Path secondMount = secondFileStore();
+ Path elsewhere = secondMount != null ? secondMount :
Paths.get(System.getProperty("user.dir"));
+
+ assertEquals(
+ here.getFileSystem(),
+ HopVfs.getFileObject(elsewhere.toString()).getFileSystem(),
+ "VFS sees one file system, so moveTo() will rename between these two
folders");
+ }
+
+ private ActionSftpPut action(String localDirectory, String
destinationFolder) {
+ ActionSftpPut action = new ActionSftpPut("SFTP Put");
+ action.setServerName(LOCALHOST);
+ action.setServerPort(String.valueOf(sshServer.getPort()));
+ action.setUserName(USER);
+ action.setPassword(PASSWORD);
+ action.setLocalDirectory(localDirectory);
+ action.setAfterSftpAction(AfterFtpAction.MOVE);
+ action.setDestinationFolder(destinationFolder);
+ action.setCreateDestinationFolder(true);
+
+ IWorkflowEngine<WorkflowMeta> parentWorkflow = mock(Workflow.class);
+ doReturn(false).when(parentWorkflow).isStopped();
+ doReturn(LogLevel.BASIC).when(parentWorkflow).getLogLevel();
+ doReturn("unit test").when(parentWorkflow).getWorkflowName();
+ action.setParentWorkflow(parentWorkflow);
+ action.setParentWorkflowMeta(mock(WorkflowMeta.class));
+ return action;
+ }
+
+ private static FileObject writeFile(String uri) throws Exception {
+ FileObject fileObject = HopVfs.getFileObject(uri);
+ try (OutputStream outputStream =
fileObject.getContent().getOutputStream()) {
+ outputStream.write(PAYLOAD.getBytes(UTF_8));
+ }
+ return fileObject;
+ }
+
+ /** What the action logged, so a failing assertion says why the action gave
up. */
+ private static String log(ActionSftpPut action) {
+ return HopLogStore.getAppender().getBuffer(action.getLogChannelId(),
false).toString();
+ }
+
+ /**
+ * A writable folder on a different file store than the temporary folder, or
null when this
+ * machine has none to offer.
+ */
+ private Path secondFileStore() throws IOException {
+ FileStore local = Files.getFileStore(localRoot);
+ for (String candidate :
+ new String[] {System.getProperty(CROSS_DEVICE_DIR_PROPERTY),
"/dev/shm"}) {
+ if (candidate == null) {
+ continue;
+ }
+ Path path = Paths.get(candidate);
+ if (Files.isDirectory(path)
+ && Files.isWritable(path)
+ && !local.equals(Files.getFileStore(path))) {
+ return path;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
index 889d8882eb..6d50460095 100644
---
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
+++
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
@@ -2671,7 +2671,7 @@ public class ScriptValuesAddedFunctions extends
ScriptableObject {
boolean destinationExists = fileDestination.exists();
// Let's move the file...
if ((destinationExists && overwrite) || !destinationExists) {
- fileSource.moveTo(fileDestination);
+ HopVfs.moveFile(fileSource, fileDestination);
}
}
} else {
diff --git
a/plugins/transforms/processfiles/src/main/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFiles.java
b/plugins/transforms/processfiles/src/main/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFiles.java
index 70f369dfa3..96ad4a2090 100644
---
a/plugins/transforms/processfiles/src/main/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFiles.java
+++
b/plugins/transforms/processfiles/src/main/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFiles.java
@@ -406,7 +406,7 @@ public class ProcessFiles extends
BaseTransform<ProcessFilesMeta, ProcessFilesDa
if (isDataVolumeMetricEnabled()) {
movedBytes = safeContentSize(data.sourceFile);
}
- data.sourceFile.moveTo(HopVfs.getFileObject(targetFilename, variables));
+ HopVfs.moveFile(data.sourceFile, HopVfs.getFileObject(targetFilename,
variables));
if (movedBytes > 0) {
dataVolumeIn = (dataVolumeIn != null ? dataVolumeIn : 0L) + movedBytes;
dataVolumeOut = (dataVolumeOut != null ? dataVolumeOut : 0L) +
movedBytes;
diff --git
a/plugins/transforms/zipfile/src/main/java/org/apache/hop/pipeline/transforms/zipfile/ZipFile.java
b/plugins/transforms/zipfile/src/main/java/org/apache/hop/pipeline/transforms/zipfile/ZipFile.java
index c523deb0db..0b4387ff4a 100644
---
a/plugins/transforms/zipfile/src/main/java/org/apache/hop/pipeline/transforms/zipfile/ZipFile.java
+++
b/plugins/transforms/zipfile/src/main/java/org/apache/hop/pipeline/transforms/zipfile/ZipFile.java
@@ -288,7 +288,7 @@ public class ZipFile extends BaseTransform<ZipFileMeta,
ZipFileData> {
file = HopVfs.getFileObject(targetfilename, variables);
// Move file
- data.sourceFile.moveTo(file);
+ HopVfs.moveFile(data.sourceFile, file);
} finally {
if (file != null) {
diff --git a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
index 76ad5d0cf9..862e896672 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
@@ -1739,7 +1739,7 @@ public class HopVfsFileDialog implements IFileDialog,
IDirectoryDialog {
HopVfs.getFileObject(
HopVfs.getFilename(file.getParent()) + "/" +
renameText.getText(),
variables);
- file.moveTo(newFile);
+ HopVfs.moveFile(file, newFile);
} catch (Exception e) {
showError(
BaseMessages.getString(
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 5c99e2bcc1..94b5251eed 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -1508,7 +1508,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
// Rename the folder
- sourceFile.moveTo(targetFile);
+ HopVfs.moveFile(sourceFile, targetFile);
// Update all opened impacted file type handlers
for (String filename : filenames) {
@@ -1541,7 +1541,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
String newPath = sourceExists ? pathFromTarget : pathFromSource;
// Rename the file
- sourceFile.moveTo(targetFile);
+ HopVfs.moveFile(sourceFile, targetFile);
// Update opened file type handler (use old path to find the handler)
TabItemHandler handler = findTabItemHandler(oldPath);
@@ -1577,7 +1577,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
// Move file
- sourceFile.moveTo(targetFile);
+ HopVfs.moveFile(sourceFile, targetFile);
// Update all opened impacted file type handlers
for (String filename : filenames) {
@@ -1607,7 +1607,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
String newPath = sourceExists ? pathFromTarget : pathFromSource;
// Move file
- sourceFile.moveTo(targetFile);
+ HopVfs.moveFile(sourceFile, targetFile);
// Update opened file type handler
TabItemHandler handler = findTabItemHandler(oldPath);