This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new fb46777b5efe CAMEL-26081: camel-ftp, camel-mina-sftp, camel-smb -
${file:name} in moveExisting is the name of the file in its directory (#28589)
fb46777b5efe is described below
commit fb46777b5efeab2d3b613095daf452001d99d91e
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Oct 28 21:34:58 2026 +0530
CAMEL-26081: camel-ftp, camel-mina-sftp, camel-smb - ${file:name} in
moveExisting is the name of the file in its directory (#28589)
For the ftp, mina-sftp and smb moveExisting strategies the file name
expression is completed with the directory by whole path segments, so a file
such as movement.txt in the directory move is not mistaken for a path that
already starts with the directory. camel-file is unchanged. Documented in the
4.24 upgrade guide.
Co-Authored-By: Claude <[email protected]>
---
.../file/MoveExistingFileStrategyUtils.java | 45 +++++-
.../file/MoveExistingFileStrategyUtilsTest.java | 60 ++++++++
.../SftpDefaultMoveExistingFileStrategy.java | 10 +-
.../remote/FtpDefaultMoveExistingFileStrategy.java | 10 +-
.../FtpProducerMoveExistingFileNameIT.java | 165 +++++++++++++++++++++
.../SftpDefaultMoveExistingFileStrategyTest.java | 56 +++++++
.../SftpProducerMoveExistingFileNameIT.java | 145 ++++++++++++++++++
.../sftp/MinaSftpMoveExistingFileStrategyTest.java | 56 +++++++
.../MinaSftpProducerMoveExistingFileNameIT.java | 135 +++++++++++++++++
.../smb/SmbDefaultMoveExistingFileStrategy.java | 12 +-
.../SmbDefaultMoveExistingFileStrategyTest.java | 64 ++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 24 +++
12 files changed, 772 insertions(+), 10 deletions(-)
diff --git
a/components/camel-file/src/main/java/org/apache/camel/component/file/MoveExistingFileStrategyUtils.java
b/components/camel-file/src/main/java/org/apache/camel/component/file/MoveExistingFileStrategyUtils.java
index 142614d7ed32..5c505641190d 100644
---
a/components/camel-file/src/main/java/org/apache/camel/component/file/MoveExistingFileStrategyUtils.java
+++
b/components/camel-file/src/main/java/org/apache/camel/component/file/MoveExistingFileStrategyUtils.java
@@ -44,12 +44,37 @@ public final class MoveExistingFileStrategyUtils {
* @return the full destination path
*/
public static String completePartialRelativePath(String destinationPath,
String fileOnlyName, String directoryName) {
+ return complete(destinationPath, fileOnlyName, directoryName, false);
+ }
+
+ /**
+ * As {@link #completePartialRelativePath(String, String, String)}, but a
relative destination path is only left as
+ * it is when it starts with the directory as whole path segments: for the
directory move, movement.txt.bak and
+ * move-old/whatever.ext are completed (move/movement.txt.bak,
move/move-old/whatever.ext), while move/backup/ and
+ * move are not. Used by the remote file components, where the file name
in the moveExisting expression is the name
+ * of the file in its directory.
+ *
+ * @param destinationPath the destination path
+ * @param fileOnlyName the file name without the path
+ * @param directoryName the path of the file to be moved/renamed
+ * @return the full destination path
+ * @since 4.24
+ */
+ public static String completeRelativePathInDirectory(
+ String destinationPath, String fileOnlyName, String directoryName)
{
+ return complete(destinationPath, fileOnlyName, directoryName, true);
+ }
+
+ private static String complete(
+ String destinationPath, String fileOnlyName, String directoryName,
boolean wholePathSegments) {
if (destinationPath.length() > 1 && destinationPath.endsWith("/")) {
destinationPath = destinationPath + fileOnlyName;
}
- if (ObjectHelper.isNotEmpty(directoryName) &&
!destinationPath.startsWith(directoryName)
+ if (ObjectHelper.isNotEmpty(directoryName)
+ && !(wholePathSegments
+ ? isInDirectory(destinationPath, directoryName) :
destinationPath.startsWith(directoryName))
&& !FileUtil.isAbsolute(new File(destinationPath))) {
destinationPath = directoryName + "/" + destinationPath;
}
@@ -57,4 +82,22 @@ public final class MoveExistingFileStrategyUtils {
return destinationPath;
}
+ /**
+ * Whether the path is the directory itself or a path below it. The
directory must match whole path segments, so
+ * movement.txt.bak is not in the directory move.
+ */
+ private static boolean isInDirectory(String path, String directoryName) {
+ if (!path.startsWith(directoryName)) {
+ return false;
+ }
+ if (path.length() == directoryName.length() ||
isSeparator(directoryName.charAt(directoryName.length() - 1))) {
+ return true;
+ }
+ return isSeparator(path.charAt(directoryName.length()));
+ }
+
+ private static boolean isSeparator(char ch) {
+ return ch == '/' || ch == '\\';
+ }
+
}
diff --git
a/components/camel-file/src/test/java/org/apache/camel/component/file/MoveExistingFileStrategyUtilsTest.java
b/components/camel-file/src/test/java/org/apache/camel/component/file/MoveExistingFileStrategyUtilsTest.java
new file mode 100644
index 000000000000..c80f6a3076ae
--- /dev/null
+++
b/components/camel-file/src/test/java/org/apache/camel/component/file/MoveExistingFileStrategyUtilsTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.camel.component.file;
+
+import org.junit.jupiter.api.Test;
+
+import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completePartialRelativePath;
+import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completeRelativePathInDirectory;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MoveExistingFileStrategyUtilsTest {
+
+ @Test
+ void nameThatStartsWithTheDirectoryNameIsPutInTheDirectory() {
+ assertEquals("move/movement.txt.bak",
completeRelativePathInDirectory("movement.txt.bak", "movement.txt", "move"));
+ assertEquals("move/move-old/a.txt",
completeRelativePathInDirectory("move-old/a.txt", "a.txt", "move"));
+ assertEquals("data/in/data/inbox/a.txt",
completeRelativePathInDirectory("data/inbox/a.txt", "a.txt", "data/in"));
+ }
+
+ @Test
+ void pathInTheDirectoryIsUnchanged() {
+ assertEquals("move/backup/hello.txt",
completeRelativePathInDirectory("move/backup/hello.txt", "hello.txt", "move"));
+ assertEquals("move\\backup\\hello.txt",
+ completeRelativePathInDirectory("move\\backup\\hello.txt",
"hello.txt", "move"));
+ assertEquals("move", completeRelativePathInDirectory("move",
"hello.txt", "move"));
+ assertEquals("data/in/hello.txt.bak",
+ completeRelativePathInDirectory("data/in/hello.txt.bak",
"hello.txt", "data/in"));
+ assertEquals("/archive/hello.txt",
completeRelativePathInDirectory("/archive/hello.txt", "hello.txt", "move"));
+ }
+
+ @Test
+ void otherRelativePathIsPutInTheDirectory() {
+ assertEquals("move/backup/hello.txt",
completeRelativePathInDirectory("backup/hello.txt", "hello.txt", "move"));
+ assertEquals("move/backup/hello.txt",
completeRelativePathInDirectory("backup/", "hello.txt", "move"));
+ assertEquals("backup/hello.txt",
completeRelativePathInDirectory("backup/hello.txt", "hello.txt", null));
+ }
+
+ @Test
+ void completePartialRelativePathKeepsAPathThatStartsWithTheDirectoryName()
{
+ // the file component: ${file:parent}-old/${file:onlyname} is a
directory next to the directory of the file
+ assertEquals("data/in-old/a.txt",
completePartialRelativePath("data/in-old/a.txt", "a.txt", "data/in"));
+ assertEquals("data/in/backup/a.txt",
completePartialRelativePath("backup/a.txt", "a.txt", "data/in"));
+ assertEquals("data/in/backup/a.txt",
completePartialRelativePath("data/in/backup/a.txt", "a.txt", "data/in"));
+ assertEquals("backup/a.txt",
completePartialRelativePath("backup/a.txt", "a.txt", null));
+ }
+}
diff --git
a/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/SftpDefaultMoveExistingFileStrategy.java
b/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/SftpDefaultMoveExistingFileStrategy.java
index 13ef470c5498..b49368d117e3 100644
---
a/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/SftpDefaultMoveExistingFileStrategy.java
+++
b/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/SftpDefaultMoveExistingFileStrategy.java
@@ -26,7 +26,7 @@ import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completePartialRelativePath;
+import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completeRelativePathInDirectory;
public class SftpDefaultMoveExistingFileStrategy implements
FileMoveExistingStrategy {
@@ -48,7 +48,11 @@ public class SftpDefaultMoveExistingFileStrategy implements
FileMoveExistingStra
//any leading separator
String parent =
FileUtil.stripLeadingSeparator(FileUtil.onlyPath(fileName));
String onlyName = FileUtil.stripPath(fileName);
- dummy.getIn().setHeader(FtpConstants.FILE_NAME, fileName);
+ // the file name is the name in the directory of the file, as when the
producer has changed into that
+ // directory (stepwise=true); a relative result is completed with that
directory below. Without stepwise,
+ // with a tempFileName, and in mina-sftp, the given name has the path
from the login directory, and with
+ // it ${file:name} would put that path again below the directory of
the file
+ dummy.getIn().setHeader(FtpConstants.FILE_NAME, onlyName);
dummy.getIn().setHeader(FtpConstants.FILE_NAME_ONLY, onlyName);
dummy.getIn().setHeader(FtpConstants.FILE_PARENT, parent);
@@ -63,7 +67,7 @@ public class SftpDefaultMoveExistingFileStrategy implements
FileMoveExistingStra
"moveExisting evaluated as empty String, cannot move
existing file: " + fileName);
}
- to = completePartialRelativePath(to, onlyName, parent);
+ to = completeRelativePathInDirectory(to, onlyName, parent);
// normalize accordingly to configuration
to = ((RemoteFileEndpoint<?>)
endpoint).getConfiguration().normalizePath(to);
diff --git
a/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpDefaultMoveExistingFileStrategy.java
b/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpDefaultMoveExistingFileStrategy.java
index 6ca60cde8b8c..b7b30b53e3c5 100644
---
a/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpDefaultMoveExistingFileStrategy.java
+++
b/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpDefaultMoveExistingFileStrategy.java
@@ -29,7 +29,7 @@ import org.apache.commons.net.ftp.FTPFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completePartialRelativePath;
+import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completeRelativePathInDirectory;
public class FtpDefaultMoveExistingFileStrategy implements
FileMoveExistingStrategy {
@@ -51,7 +51,11 @@ public class FtpDefaultMoveExistingFileStrategy implements
FileMoveExistingStrat
//any leading separator
String parent =
FileUtil.stripLeadingSeparator(FileUtil.onlyPath(fileName));
String onlyName = FileUtil.stripPath(fileName);
- dummy.getIn().setHeader(FtpConstants.FILE_NAME, fileName);
+ // the file name is the name in the directory of the file, as when the
producer has changed into that
+ // directory (stepwise=true); a relative result is completed with that
directory below. Without stepwise,
+ // with a tempFileName, and in mina-sftp, the given name has the path
from the login directory, and with
+ // it ${file:name} would put that path again below the directory of
the file
+ dummy.getIn().setHeader(FtpConstants.FILE_NAME, onlyName);
dummy.getIn().setHeader(FtpConstants.FILE_NAME_ONLY, onlyName);
dummy.getIn().setHeader(FtpConstants.FILE_PARENT, parent);
@@ -66,7 +70,7 @@ public class FtpDefaultMoveExistingFileStrategy implements
FileMoveExistingStrat
"moveExisting evaluated as empty String, cannot move
existing file: " + fileName);
}
- to = completePartialRelativePath(to, onlyName, parent);
+ to = completeRelativePathInDirectory(to, onlyName, parent);
// normalize accordingly to configuration
to = ((FtpEndpoint<FTPFile>)
endpoint).getConfiguration().normalizePath(to);
diff --git
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/integration/FtpProducerMoveExistingFileNameIT.java
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/integration/FtpProducerMoveExistingFileNameIT.java
new file mode 100644
index 000000000000..10d46a0d7a23
--- /dev/null
+++
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/integration/FtpProducerMoveExistingFileNameIT.java
@@ -0,0 +1,165 @@
+/*
+ * 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.camel.component.file.remote.integration;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.file.GenericFileOperationFailedException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.apache.camel.test.junit6.TestSupport.assertFileExists;
+import static org.apache.camel.test.junit6.TestSupport.assertFileNotExists;
+import static org.apache.camel.test.junit6.TestSupport.assertIsInstanceOf;
+import static org.apache.camel.test.junit6.TestSupport.createCleanDirectory;
+import static org.apache.camel.test.junit6.TestSupport.deleteDirectory;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * fileExist=Move: ${file:name} in moveExisting is the name of the file in its
directory, with and without stepwise
+ * (without it the producer works with the path of the file from the login
directory).
+ */
+class FtpProducerMoveExistingFileNameIT extends FtpServerTestSupport {
+
+ private String getFtpUrl(boolean stepwise) {
+ return
"ftp://admin@localhost:{{ftp.server.port}}/move?password=admin&fileExist=Move&stepwise="
+ stepwise;
+ }
+
+ @BeforeEach
+ void cleanupDir() {
+ createCleanDirectory(service.ftpFile("move"));
+ deleteDirectory(service.ftpFile("move-old"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingIntoBackupDirectoryWithFileName(boolean stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name}", "Hello World",
"hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name}", "Bye World",
"hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingWithFileNamePrefix(boolean stepwise) {
+ send(stepwise, "&moveExisting=old-${file:name}", "Hello World",
"hello.txt");
+ send(stepwise, "&moveExisting=old-${file:name}", "Bye World",
"hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/old-hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingWithFileNameWithoutExtension(boolean stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name.noext}.bak", "Hello
World", "hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name.noext}.bak", "Bye
World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.bak"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingOfFileInSubDirectory(boolean stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name}", "Hello World",
"sub/hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name}", "Bye World",
"sub/hello.txt");
+
+ assertEquals("Bye World", read("move/sub/hello.txt"));
+ assertEquals("Hello World", read("move/sub/backup/hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingWithTempFileName(boolean stepwise) {
+ send(stepwise,
"&tempFileName=${file:onlyname}.tmp&moveExisting=backup/${file:name}", "Hello
World", "hello.txt");
+ send(stepwise,
"&tempFileName=${file:onlyname}.tmp&moveExisting=backup/${file:name}", "Bye
World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingWithFileNameThatStartsWithTheDirectoryName(boolean
stepwise) {
+ send(stepwise, "&moveExisting=${file:name}.bak", "Hello World",
"movement.txt");
+ send(stepwise, "&moveExisting=${file:name}.bak", "Bye World",
"movement.txt");
+
+ assertEquals("Bye World", read("move/movement.txt"));
+ assertEquals("Hello World", read("move/movement.txt.bak"));
+ assertFileNotExists(service.ftpFile("movement.txt.bak"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void
moveExistingWithTempFileNameAndFileNameThatStartsWithTheDirectoryName(boolean
stepwise) {
+ send(stepwise,
"&tempFileName=${file:onlyname}.tmp&moveExisting=${file:name}.bak", "Hello
World", "movement.txt");
+ send(stepwise,
"&tempFileName=${file:onlyname}.tmp&moveExisting=${file:name}.bak", "Bye
World", "movement.txt");
+
+ assertEquals("Bye World", read("move/movement.txt"));
+ assertEquals("Hello World", read("move/movement.txt.bak"));
+ assertFileNotExists(service.ftpFile("movement.txt.bak"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingIntoDirectoryNextToTheDirectoryOfTheFile(boolean
stepwise) {
+ send(stepwise, "&moveExisting=../move-old/${file:onlyname}", "Hello
World", "hello.txt");
+ send(stepwise, "&moveExisting=../move-old/${file:onlyname}", "Bye
World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move-old/hello.txt"));
+ }
+
+ @Test
+ void moveExistingWithFileParentIsUnchanged() {
+ send(false, "&moveExisting=${file:parent}/renamed-${file:onlyname}",
"Hello World", "hello.txt");
+ send(false, "&moveExisting=${file:parent}/renamed-${file:onlyname}",
"Bye World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/renamed-hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void existingBackupIsNotOverwrittenWhenEagerDeleteIsDisabled(boolean
stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name}", "Old backup",
"backup/hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name}", "Hello World",
"hello.txt");
+
+ Exception ex = assertThrows(CamelExecutionException.class,
+ () -> send(stepwise,
"&moveExisting=backup/${file:name}&eagerDeleteTargetFile=false", "Bye World",
+ "hello.txt"));
+ assertIsInstanceOf(GenericFileOperationFailedException.class,
ex.getCause());
+
+ assertEquals("Hello World", read("move/hello.txt"));
+ assertEquals("Old backup", read("move/backup/hello.txt"));
+ }
+
+ private void send(boolean stepwise, String options, String body, String
fileName) {
+ template.sendBodyAndHeader(getFtpUrl(stepwise) + options, body,
Exchange.FILE_NAME, fileName);
+ }
+
+ private String read(String file) {
+ assertFileExists(service.ftpFile(file));
+ return context.getTypeConverter().convertTo(String.class,
service.ftpFile(file).toFile());
+ }
+}
diff --git
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/SftpDefaultMoveExistingFileStrategyTest.java
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/SftpDefaultMoveExistingFileStrategyTest.java
new file mode 100644
index 000000000000..9a2ce1151dcd
--- /dev/null
+++
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/SftpDefaultMoveExistingFileStrategyTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.camel.component.file.remote.sftp;
+
+import org.apache.camel.component.file.GenericFileEndpoint;
+import org.apache.camel.component.file.GenericFileOperations;
+import
org.apache.camel.component.file.remote.SftpDefaultMoveExistingFileStrategy;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * fileExist=Move with the path of the file from the login directory
(stepwise=false, a tempFileName, mina-sftp): a
+ * relative moveExisting is put in the directory of the file, also when it
starts with the name of that directory.
+ */
+class SftpDefaultMoveExistingFileStrategyTest extends CamelTestSupport {
+
+ @ParameterizedTest
+ @CsvSource({
+ "move/movement.txt, ${file:name}.bak, move/movement.txt.bak",
+ "move/hello.txt, ${file:name}.bak, move/hello.txt.bak",
+ "move/hello.txt, backup/${file:name}, move/backup/hello.txt",
+ "move/hello.txt, move-old/${file:name}, move/move-old/hello.txt",
+ "move/hello.txt, ${file:parent}/renamed-${file:onlyname},
move/renamed-hello.txt",
+ "move/hello.txt, ${file:parent}-old/${file:onlyname},
move/move-old/hello.txt",
+ "inbox/orders/hello.txt, backup/${file:name},
inbox/orders/backup/hello.txt" })
+ void moveExistingIsInTheDirectoryOfTheFile(String fileName, String
moveExisting, String expected) {
+ GenericFileEndpoint<?> endpoint = context.getEndpoint(
+ "sftp://localhost/move?fileExist=Move&moveExisting=" +
moveExisting, GenericFileEndpoint.class);
+ GenericFileOperations<?> operations =
mock(GenericFileOperations.class);
+ when(operations.renameFile(anyString(), anyString())).thenReturn(true);
+
+ new SftpDefaultMoveExistingFileStrategy().moveExistingFile(endpoint,
operations, fileName);
+
+ verify(operations).renameFile(fileName, expected);
+ }
+}
diff --git
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpProducerMoveExistingFileNameIT.java
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpProducerMoveExistingFileNameIT.java
new file mode 100644
index 000000000000..1beec3a37b26
--- /dev/null
+++
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpProducerMoveExistingFileNameIT.java
@@ -0,0 +1,145 @@
+/*
+ * 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.camel.component.file.remote.sftp.integration;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.file.GenericFileOperationFailedException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.apache.camel.test.junit6.TestSupport.assertFileExists;
+import static org.apache.camel.test.junit6.TestSupport.assertIsInstanceOf;
+import static org.apache.camel.test.junit6.TestSupport.createCleanDirectory;
+import static org.apache.camel.test.junit6.TestSupport.deleteDirectory;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * fileExist=Move: ${file:name} in moveExisting is the name of the file in its
directory, with and without stepwise
+ * (without it the producer works with the path of the file from the login
directory).
+ */
+@EnabledIf(value =
"org.apache.camel.test.infra.ftp.services.embedded.SftpUtil#hasRequiredAlgorithms('src/test/resources/hostkey.pem')")
+class SftpProducerMoveExistingFileNameIT extends SftpServerTestSupport {
+
+ private String getFtpUrl(boolean stepwise) {
+ return
"sftp://admin@localhost:{{ftp.server.port}}/{{ftp.root.dir}}/move?password=admin&fileExist=Move&knownHostsFile="
+ + service.getKnownHostsFile() + "&stepwise=" + stepwise;
+ }
+
+ @BeforeEach
+ void cleanupDir() {
+ createCleanDirectory(ftpFile("move"));
+ deleteDirectory(ftpFile("move-old"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingIntoBackupDirectoryWithFileName(boolean stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name}", "Hello World",
"hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name}", "Bye World",
"hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingWithFileNamePrefix(boolean stepwise) {
+ send(stepwise, "&moveExisting=old-${file:name}", "Hello World",
"hello.txt");
+ send(stepwise, "&moveExisting=old-${file:name}", "Bye World",
"hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/old-hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingWithFileNameWithoutExtension(boolean stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name.noext}.bak", "Hello
World", "hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name.noext}.bak", "Bye
World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.bak"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingOfFileInSubDirectory(boolean stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name}", "Hello World",
"sub/hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name}", "Bye World",
"sub/hello.txt");
+
+ assertEquals("Bye World", read("move/sub/hello.txt"));
+ assertEquals("Hello World", read("move/sub/backup/hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingWithTempFileName(boolean stepwise) {
+ send(stepwise,
"&tempFileName=${file:onlyname}.tmp&moveExisting=backup/${file:name}", "Hello
World", "hello.txt");
+ send(stepwise,
"&tempFileName=${file:onlyname}.tmp&moveExisting=backup/${file:name}", "Bye
World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void moveExistingIntoDirectoryNextToTheDirectoryOfTheFile(boolean
stepwise) {
+ send(stepwise, "&moveExisting=../move-old/${file:onlyname}", "Hello
World", "hello.txt");
+ send(stepwise, "&moveExisting=../move-old/${file:onlyname}", "Bye
World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move-old/hello.txt"));
+ }
+
+ @Test
+ void moveExistingWithFileParentIsUnchanged() {
+ send(false, "&moveExisting=${file:parent}/renamed-${file:onlyname}",
"Hello World", "hello.txt");
+ send(false, "&moveExisting=${file:parent}/renamed-${file:onlyname}",
"Bye World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/renamed-hello.txt"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void existingBackupIsNotOverwrittenWhenEagerDeleteIsDisabled(boolean
stepwise) {
+ send(stepwise, "&moveExisting=backup/${file:name}", "Old backup",
"backup/hello.txt");
+ send(stepwise, "&moveExisting=backup/${file:name}", "Hello World",
"hello.txt");
+
+ Exception ex = assertThrows(CamelExecutionException.class,
+ () -> send(stepwise,
"&moveExisting=backup/${file:name}&eagerDeleteTargetFile=false", "Bye World",
+ "hello.txt"));
+ assertIsInstanceOf(GenericFileOperationFailedException.class,
ex.getCause());
+
+ assertEquals("Hello World", read("move/hello.txt"));
+ assertEquals("Old backup", read("move/backup/hello.txt"));
+ }
+
+ private void send(boolean stepwise, String options, String body, String
fileName) {
+ template.sendBodyAndHeader(getFtpUrl(stepwise) + options, body,
Exchange.FILE_NAME, fileName);
+ }
+
+ private String read(String file) {
+ assertFileExists(ftpFile(file));
+ return context.getTypeConverter().convertTo(String.class,
ftpFile(file).toFile());
+ }
+}
diff --git
a/components/camel-mina-sftp/src/test/java/org/apache/camel/component/file/remote/mina/sftp/MinaSftpMoveExistingFileStrategyTest.java
b/components/camel-mina-sftp/src/test/java/org/apache/camel/component/file/remote/mina/sftp/MinaSftpMoveExistingFileStrategyTest.java
new file mode 100644
index 000000000000..b0bd86b081b3
--- /dev/null
+++
b/components/camel-mina-sftp/src/test/java/org/apache/camel/component/file/remote/mina/sftp/MinaSftpMoveExistingFileStrategyTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.camel.component.file.remote.mina.sftp;
+
+import org.apache.camel.component.file.GenericFileEndpoint;
+import org.apache.camel.component.file.GenericFileOperations;
+import
org.apache.camel.component.file.remote.SftpDefaultMoveExistingFileStrategy;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * fileExist=Move with the path of the file from the login directory
(stepwise=false, a tempFileName, mina-sftp): a
+ * relative moveExisting is put in the directory of the file, also when it
starts with the name of that directory.
+ */
+class MinaSftpMoveExistingFileStrategyTest extends CamelTestSupport {
+
+ @ParameterizedTest
+ @CsvSource({
+ "move/movement.txt, ${file:name}.bak, move/movement.txt.bak",
+ "move/hello.txt, ${file:name}.bak, move/hello.txt.bak",
+ "move/hello.txt, backup/${file:name}, move/backup/hello.txt",
+ "move/hello.txt, move-old/${file:name}, move/move-old/hello.txt",
+ "move/hello.txt, ${file:parent}/renamed-${file:onlyname},
move/renamed-hello.txt",
+ "move/hello.txt, ${file:parent}-old/${file:onlyname},
move/move-old/hello.txt",
+ "inbox/orders/hello.txt, backup/${file:name},
inbox/orders/backup/hello.txt" })
+ void moveExistingIsInTheDirectoryOfTheFile(String fileName, String
moveExisting, String expected) {
+ GenericFileEndpoint<?> endpoint = context.getEndpoint(
+ "mina-sftp://localhost/move?fileExist=Move&moveExisting=" +
moveExisting, GenericFileEndpoint.class);
+ GenericFileOperations<?> operations =
mock(GenericFileOperations.class);
+ when(operations.renameFile(anyString(), anyString())).thenReturn(true);
+
+ new SftpDefaultMoveExistingFileStrategy().moveExistingFile(endpoint,
operations, fileName);
+
+ verify(operations).renameFile(fileName, expected);
+ }
+}
diff --git
a/components/camel-mina-sftp/src/test/java/org/apache/camel/component/file/remote/mina/sftp/MinaSftpProducerMoveExistingFileNameIT.java
b/components/camel-mina-sftp/src/test/java/org/apache/camel/component/file/remote/mina/sftp/MinaSftpProducerMoveExistingFileNameIT.java
new file mode 100644
index 000000000000..dbada20c710a
--- /dev/null
+++
b/components/camel-mina-sftp/src/test/java/org/apache/camel/component/file/remote/mina/sftp/MinaSftpProducerMoveExistingFileNameIT.java
@@ -0,0 +1,135 @@
+/*
+ * 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.camel.component.file.remote.mina.sftp;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.file.GenericFileOperationFailedException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+
+import static org.apache.camel.test.junit6.TestSupport.assertFileExists;
+import static org.apache.camel.test.junit6.TestSupport.assertIsInstanceOf;
+import static org.apache.camel.test.junit6.TestSupport.createCleanDirectory;
+import static org.apache.camel.test.junit6.TestSupport.deleteDirectory;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * fileExist=Move: ${file:name} in moveExisting is the name of the file in its
directory, as with the sftp (JSch) and
+ * ftp components with their default stepwise=true, and not the path of the
file from the login directory.
+ */
+@EnabledIf(value =
"org.apache.camel.test.infra.ftp.services.embedded.SftpUtil#hasRequiredAlgorithms('src/test/resources/sftp/hostkey.pem')")
+class MinaSftpProducerMoveExistingFileNameIT extends SftpServerTestSupport {
+
+ private String getFtpUrl() {
+ return
"mina-sftp://admin@localhost:{{ftp.server.port}}/{{ftp.root.dir}}/move?password=admin&fileExist=Move&knownHostsFile="
+ + service.getKnownHostsFile();
+ }
+
+ @BeforeEach
+ void cleanupDir() {
+ createCleanDirectory(ftpFile("move"));
+ deleteDirectory(ftpFile("move-old"));
+ }
+
+ @Test
+ void moveExistingIntoBackupDirectoryWithFileName() {
+ send("&moveExisting=backup/${file:name}", "Hello World", "hello.txt");
+ send("&moveExisting=backup/${file:name}", "Bye World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.txt"));
+ }
+
+ @Test
+ void moveExistingWithFileNamePrefix() {
+ send("&moveExisting=old-${file:name}", "Hello World", "hello.txt");
+ send("&moveExisting=old-${file:name}", "Bye World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/old-hello.txt"));
+ }
+
+ @Test
+ void moveExistingWithFileNameWithoutExtension() {
+ send("&moveExisting=backup/${file:name.noext}.bak", "Hello World",
"hello.txt");
+ send("&moveExisting=backup/${file:name.noext}.bak", "Bye World",
"hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.bak"));
+ }
+
+ @Test
+ void moveExistingOfFileInSubDirectory() {
+ send("&moveExisting=backup/${file:name}", "Hello World",
"sub/hello.txt");
+ send("&moveExisting=backup/${file:name}", "Bye World",
"sub/hello.txt");
+
+ assertEquals("Bye World", read("move/sub/hello.txt"));
+ assertEquals("Hello World", read("move/sub/backup/hello.txt"));
+ }
+
+ @Test
+ void moveExistingWithTempFileName() {
+
send("&tempFileName=${file:onlyname}.tmp&moveExisting=backup/${file:name}",
"Hello World", "hello.txt");
+
send("&tempFileName=${file:onlyname}.tmp&moveExisting=backup/${file:name}",
"Bye World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/backup/hello.txt"));
+ }
+
+ @Test
+ void moveExistingIntoDirectoryNextToTheDirectoryOfTheFile() {
+ send("&moveExisting=../move-old/${file:onlyname}", "Hello World",
"hello.txt");
+ send("&moveExisting=../move-old/${file:onlyname}", "Bye World",
"hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move-old/hello.txt"));
+ }
+
+ @Test
+ void moveExistingWithFileParentIsUnchanged() {
+ send("&moveExisting=${file:parent}/renamed-${file:onlyname}", "Hello
World", "hello.txt");
+ send("&moveExisting=${file:parent}/renamed-${file:onlyname}", "Bye
World", "hello.txt");
+
+ assertEquals("Bye World", read("move/hello.txt"));
+ assertEquals("Hello World", read("move/renamed-hello.txt"));
+ }
+
+ @Test
+ void existingBackupIsNotOverwrittenWhenEagerDeleteIsDisabled() {
+ send("&moveExisting=backup/${file:name}", "Old backup",
"backup/hello.txt");
+ send("&moveExisting=backup/${file:name}", "Hello World", "hello.txt");
+
+ Exception ex = assertThrows(CamelExecutionException.class,
+ () ->
send("&moveExisting=backup/${file:name}&eagerDeleteTargetFile=false", "Bye
World", "hello.txt"));
+ assertIsInstanceOf(GenericFileOperationFailedException.class,
ex.getCause());
+
+ assertEquals("Hello World", read("move/hello.txt"));
+ assertEquals("Old backup", read("move/backup/hello.txt"));
+ }
+
+ private void send(String options, String body, String fileName) {
+ template.sendBodyAndHeader(getFtpUrl() + options, body,
Exchange.FILE_NAME, fileName);
+ }
+
+ private String read(String file) {
+ assertFileExists(ftpFile(file));
+ return context.getTypeConverter().convertTo(String.class,
ftpFile(file).toFile());
+ }
+}
diff --git
a/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbDefaultMoveExistingFileStrategy.java
b/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbDefaultMoveExistingFileStrategy.java
index e58e80cd0c08..2c8c73cf0418 100644
---
a/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbDefaultMoveExistingFileStrategy.java
+++
b/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbDefaultMoveExistingFileStrategy.java
@@ -26,7 +26,7 @@ import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completePartialRelativePath;
+import static
org.apache.camel.component.file.MoveExistingFileStrategyUtils.completeRelativePathInDirectory;
public class SmbDefaultMoveExistingFileStrategy implements
FileMoveExistingStrategy {
@@ -48,7 +48,10 @@ public class SmbDefaultMoveExistingFileStrategy implements
FileMoveExistingStrat
//any leading separator
String parent =
FileUtil.stripLeadingSeparator(FileUtil.onlyPath(fileName));
String onlyName = FileUtil.stripPath(fileName);
- dummy.getIn().setHeader(SmbConstants.FILE_NAME, fileName);
+ // the file name is the name in the directory of the file, as with the
ftp and sftp components; a relative
+ // result is completed with that directory below. The given name has
the path from the share, and with it
+ // ${file:name} would put that path again below the directory of the
file
+ dummy.getIn().setHeader(SmbConstants.FILE_NAME, onlyName);
dummy.getIn().setHeader(SmbConstants.FILE_NAME_ONLY, onlyName);
dummy.getIn().setHeader(SmbConstants.FILE_PARENT, parent);
@@ -63,7 +66,10 @@ public class SmbDefaultMoveExistingFileStrategy implements
FileMoveExistingStrat
"moveExisting evaluated as empty String, cannot move
existing file: " + fileName);
}
- to = completePartialRelativePath(to, onlyName, parent);
+ to = completeRelativePathInDirectory(to, onlyName, parent);
+ // the server does not resolve .. in a path, such as
move/../move-old/hello.txt for a directory next to the
+ // directory of the file
+ to = FileUtil.compactPath(to, '/');
// do we have a sub directory
String dir = FileUtil.onlyPath(to);
diff --git
a/components/camel-smb/src/test/java/org/apache/camel/component/smb/SmbDefaultMoveExistingFileStrategyTest.java
b/components/camel-smb/src/test/java/org/apache/camel/component/smb/SmbDefaultMoveExistingFileStrategyTest.java
new file mode 100644
index 000000000000..05b2269176f1
--- /dev/null
+++
b/components/camel-smb/src/test/java/org/apache/camel/component/smb/SmbDefaultMoveExistingFileStrategyTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.camel.component.smb;
+
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.component.file.GenericFileEndpoint;
+import org.apache.camel.component.file.GenericFileOperations;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * fileExist=Move: the producer gives the path of the file from the share, and
a relative moveExisting is put in the
+ * directory of the file, with ${file:name} the name of the file in that
directory.
+ */
+class SmbDefaultMoveExistingFileStrategyTest extends CamelTestSupport {
+
+ @ParameterizedTest
+ @CsvSource({
+ "move/hello.txt, backup/${file:name}, move/backup/hello.txt",
+ "move/hello.txt, old-${file:name}, move/old-hello.txt",
+ "move/movement.txt, ${file:name}.bak, move/movement.txt.bak",
+ "move/hello.txt, ${file:parent}/renamed-${file:onlyname},
move/renamed-hello.txt",
+ "move/hello.txt, ${file:parent}-old/${file:onlyname},
move/move-old/hello.txt",
+ "move/hello.txt, ../move-old/${file:onlyname}, move-old/hello.txt",
+ "move/hello.txt, old-${file:onlyname}, move/old-hello.txt",
+ "inbox/orders/hello.txt, backup/${file:name.noext}.bak,
inbox/orders/backup/hello.bak" })
+ void moveExistingIsInTheDirectoryOfTheFile(String fileName, String
moveExisting, String expected) {
+ GenericFileEndpoint<?> endpoint = context.getEndpoint(
+ "smb:localhost/share/move?fileExist=Move&moveExisting=" +
moveExisting, GenericFileEndpoint.class);
+ List<String> renames = new ArrayList<>();
+ GenericFileOperations<?> operations = (GenericFileOperations<?>)
Proxy.newProxyInstance(
+ getClass().getClassLoader(), new Class<?>[] {
GenericFileOperations.class },
+ (proxy, method, args) -> switch (method.getName()) {
+ case "existsFile" -> false;
+ case "buildDirectory" -> true;
+ case "renameFile" -> renames.add(args[0] + " -> " +
args[1]);
+ default -> throw new
UnsupportedOperationException(method.getName());
+ });
+
+ new SmbDefaultMoveExistingFileStrategy().moveExistingFile(endpoint,
operations, fileName);
+
+ assertEquals(List.of(fileName + " -> " + expected), renames);
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d29de39c38f8..37b78c4c1045 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2125,6 +2125,30 @@ login; an IP address must be listed as a subject
alternative name of the certifi
host of the endpoint, or set the new `hostnameVerification` option to `false`
to keep the previous behaviour. A custom
`ftpClient` set on the endpoint is not changed.
+=== camel-ftp, camel-sftp, camel-ftps, camel-mina-sftp, camel-smb -
`${file:name}` in moveExisting is the name of the file
+
+With `fileExist=Move`, the `${file:name}` token of `moveExisting` is now the
name of the existing file in its directory,
+as the producer of `ftp`, `ftps` and `sftp` already used it with the default
`stepwise=true`. So with `stepwise=false`,
+with a `tempFileName`, in `mina-sftp` and in `smb`, a relative `moveExisting`
built from `${file:name}`,
+`${file:onlyname}` or literal text now gives the same result as with
`stepwise=true`. Before, `${file:name}` was the
+path of the file from the login directory (or the share), and since a relative
`moveExisting` is put below the directory
+of the file, `moveExisting=backup/${file:name}` for `hello.txt` in the
directory `inbox/orders` moved the existing file
+to `inbox/orders/backup/inbox/orders/hello.txt`; it is now moved to
`inbox/orders/backup/hello.txt`. The tokens
+`${file:name.noext}` and `${file:name.noext.single}` read the same name and
change the same way: `inbox/orders/hello` is
+now `hello`. A backup kept in the nested directory is no longer found there,
for example by the check for an existing
+backup when `eagerDeleteTargetFile=false`, or by a job that reads the backups.
`${file:parent}`, `${file:onlyname}` and
+`${file:ext}` are unchanged, and so is a `moveExisting` without `${...}` such
as `moveExisting=backup`.
+
+In the same cases, a relative result is now left as it is only when it starts
with the directory of the file as whole
+path segments (`inbox/orders/backup/hello.txt`); any other relative result is
put below that directory. Before, a result
+that only started with the same characters was left as it was. So for a file
in `inbox/orders`,
+`moveExisting=${file:parent}-old/${file:onlyname}` and
`moveExisting=inbox/orders-old/${file:onlyname}` now move it to
+`inbox/orders/inbox/orders-old/hello.txt` instead of
`inbox/orders-old/hello.txt`; to keep a directory next to the
+directory of the file, use a path relative to it, such as
`moveExisting=../orders-old/${file:onlyname}` (camel-smb
+resolves the `..` itself, as SMB servers do not accept it in a path). Such a
path can lead out of the directory of the
+endpoint: `jailStartingDirectory` does not apply to `moveExisting`. The `file`
component is not changed: there
+`${file:name}` is still the path of the file, and the check is as before.
+
=== camel-aws-secrets-manager - the CloudTrail refresh task reads every event
since the last check
With `camel.vault.aws.refreshEnabled=true` and without SQS notification, each
check now reads every page of the