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

Reply via email to