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 fdc1c0086a54 CAMEL-26672: Fix ScpOperations.storeFileDirectly to set 
command and connect channel (#29317)
fdc1c0086a54 is described below

commit fdc1c0086a54610d16dd0765215ec3729599be56
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Nov 6 21:38:24 2026 +0100

    CAMEL-26672: Fix ScpOperations.storeFileDirectly to set command and connect 
channel (#29317)
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 .../apache/camel/component/scp/ScpOperations.java  |  88 +++++++------
 .../scp/ScpProducerChecksumFileAlgorithmTest.java  | 140 +++++++++++++++++++++
 2 files changed, 183 insertions(+), 45 deletions(-)

diff --git 
a/components/camel-jsch/src/main/java/org/apache/camel/component/scp/ScpOperations.java
 
b/components/camel-jsch/src/main/java/org/apache/camel/component/scp/ScpOperations.java
index 662c6fa5a030..ff4d8aedc552 100644
--- 
a/components/camel-jsch/src/main/java/org/apache/camel/component/scp/ScpOperations.java
+++ 
b/components/camel-jsch/src/main/java/org/apache/camel/component/scp/ScpOperations.java
@@ -107,11 +107,6 @@ public class ScpOperations implements 
RemoteFileOperations<ScpFile> {
         checkFileName(name);
         ScpConfiguration cfg = endpoint.getConfiguration();
 
-        int timeout = cfg.getConnectTimeout();
-        if (LOG.isTraceEnabled()) {
-            LOG.trace("Opening channel to {} with {} timeout...", 
cfg.remoteServerInformation(),
-                    timeout > 0 ? (Integer.toString(timeout) + " ms") : "no");
-        }
         String file = getRemoteFile(name, cfg);
 
         InputStream is = null;
@@ -125,22 +120,55 @@ public class ScpOperations implements 
RemoteFileOperations<ScpFile> {
             }
         }
 
+        try {
+            if (is == null) {
+                is = exchange.getIn().getMandatoryBody(InputStream.class);
+            }
+            executeScpTransfer(file, is, cfg);
+        } catch (InvalidPayloadException e) {
+            throw new GenericFileOperationFailedException("Cannot store file: 
" + name, e);
+        } finally {
+            // must close stream after usage
+            IOHelper.close(is);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean storeFileDirectly(String name, String payload) throws 
GenericFileOperationFailedException {
+        checkFileName(name);
+        ObjectHelper.notNull(session, "session");
+        ScpConfiguration cfg = endpoint.getConfiguration();
+
+        String file = getRemoteFile(name, cfg);
+
+        ByteArrayInputStream bis = new 
ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
+        try {
+            executeScpTransfer(file, bis, cfg);
+        } finally {
+            IOHelper.close(bis);
+        }
+        return true;
+    }
+
+    /**
+     * Opens an exec channel, sets the scp command, writes the stream, and 
disconnects. Both {@link #storeFile} and
+     * {@link #storeFileDirectly} delegate here so the two paths stay in sync.
+     */
+    private void executeScpTransfer(String file, InputStream data, 
ScpConfiguration cfg)
+            throws GenericFileOperationFailedException {
+        int timeout = cfg.getConnectTimeout();
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("Opening channel to {} with {} timeout...", 
cfg.remoteServerInformation(),
+                    timeout > 0 ? (Integer.toString(timeout) + " ms") : "no");
+        }
         try {
             channel = (ChannelExec) session.openChannel("exec");
             channel.setCommand(getScpCommand(cfg, file));
-
             try {
-                if (is == null) {
-                    is = exchange.getIn().getMandatoryBody(InputStream.class);
-                }
-                connectAndWrite(channel, file, is, cfg, timeout);
-            } catch (InvalidPayloadException e) {
-                throw new GenericFileOperationFailedException("Cannot store 
file: " + name, e);
+                connectAndWrite(channel, file, data, cfg, timeout);
             } catch (IOException e) {
                 throw new GenericFileOperationFailedException("Failed to write 
file " + file, e);
-            } finally {
-                // must close stream after usage
-                IOHelper.close(is);
             }
         } catch (JSchException e) {
             throw new GenericFileOperationFailedException("Failed to write 
file " + file, e);
@@ -149,30 +177,11 @@ public class ScpOperations implements 
RemoteFileOperations<ScpFile> {
                 LOG.trace("Disconnecting 'exec' scp channel");
                 channel.disconnect();
                 channel = null;
-
                 if (LOG.isTraceEnabled()) {
                     LOG.trace("Channel disconnected from {}", 
cfg.remoteServerInformation());
                 }
             }
         }
-        return true;
-    }
-
-    @Override
-    public boolean storeFileDirectly(String name, String payload) throws 
GenericFileOperationFailedException {
-        checkFileName(name);
-        ByteArrayInputStream bis = null;
-        try {
-            bis = new ByteArrayInputStream(payload.getBytes());
-            ScpConfiguration cfg = endpoint.getConfiguration();
-            channel = (ChannelExec) session.openChannel("exec");
-            write(channel, name, bis, cfg);
-            return true;
-        } catch (JSchException | IOException e) {
-            throw new GenericFileOperationFailedException("Failed to write 
file " + name, e);
-        } finally {
-            IOHelper.close(bis);
-        }
     }
 
     @Override
@@ -457,17 +466,6 @@ public class ScpOperations implements 
RemoteFileOperations<ScpFile> {
         }
     }
 
-    private void write(ChannelExec c, String name, InputStream data, 
ScpConfiguration cfg) throws IOException {
-        OutputStream os = c.getOutputStream();
-        InputStream is = c.getInputStream();
-
-        try {
-            writeFile(name, data, os, is, cfg);
-        } finally {
-            IOHelper.close(is, os);
-        }
-    }
-
     private void writeFile(String filename, InputStream data, OutputStream os, 
InputStream is, ScpConfiguration cfg)
             throws IOException {
         final int lineFeed = '\n';
diff --git 
a/components/camel-jsch/src/test/java/org/apache/camel/component/scp/ScpProducerChecksumFileAlgorithmTest.java
 
b/components/camel-jsch/src/test/java/org/apache/camel/component/scp/ScpProducerChecksumFileAlgorithmTest.java
new file mode 100644
index 000000000000..4e287dbeada2
--- /dev/null
+++ 
b/components/camel-jsch/src/test/java/org/apache/camel/component/scp/ScpProducerChecksumFileAlgorithmTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.scp;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.FileConstants;
+import org.apache.camel.util.StringHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * Tests that the SCP producer correctly writes the checksum file when {@code 
checksumFileAlgorithm} is set
+ * (CAMEL-26672). {@link ScpOperations#storeFileDirectly} was opening the 
channel but never setting the scp command or
+ * connecting it, so every exchange with the option set failed.
+ */
+class ScpProducerChecksumFileAlgorithmTest extends ScpServerTestSupport {
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // no consumer route needed: we verify the files on the server 
filesystem directly
+            }
+        };
+    }
+
+    private String getScpUriWithChecksum() {
+        return getScpUri() + "?username=admin&password=admin&knownHostsFile=" 
+ getKnownHostsFile()
+               + "&checksumFileAlgorithm=md5";
+    }
+
+    private static String md5Hex(byte[] data) throws Exception {
+        return 
StringHelper.bytesToHex(MessageDigest.getInstance("MD5").digest(data));
+    }
+
+    /**
+     * Sending a file with {@code checksumFileAlgorithm=md5} must succeed: the 
main file is written, the {@code .md5}
+     * checksum file is created with the hex MD5 of the uploaded bytes, and 
the {@code CamelFileChecksum} header is set.
+     */
+    @Test
+    void testChecksumFileIsWritten() throws Exception {
+        assumeTrue(isSetupComplete());
+
+        String body = "Hello World";
+        Exchange out = template.request(getScpUriWithChecksum(), exchange -> {
+            exchange.getIn().setHeader(Exchange.FILE_NAME, "hello.txt");
+            exchange.getIn().setBody(body);
+        });
+
+        assertNull(out.getException(), "Exchange must not fail: " + 
out.getException());
+
+        // the main file must exist and contain the uploaded body
+        Path mainFile = Path.of(getScpPath(), "hello.txt");
+        assertTrue(mainFile.toFile().exists(), "hello.txt must exist on the 
server");
+        assertEquals(body, Files.readString(mainFile));
+
+        // the checksum file must exist and contain the hex MD5 of the 
uploaded bytes
+        Path checksumFile = Path.of(getScpPath(), "hello.txt.md5");
+        assertTrue(checksumFile.toFile().exists(), "hello.txt.md5 must exist 
on the server");
+        byte[] uploadedBytes = body.getBytes(StandardCharsets.UTF_8);
+        String expectedMd5 = md5Hex(uploadedBytes);
+        assertEquals(expectedMd5, Files.readString(checksumFile));
+
+        // the CamelFileChecksum header must carry the same value
+        String checksumHeader = 
out.getMessage().getHeader(FileConstants.FILE_CHECKSUM, String.class);
+        assertNotNull(checksumHeader, "CamelFileChecksum header must be set");
+        assertEquals(expectedMd5, checksumHeader);
+    }
+
+    /**
+     * Same scenario with the file name containing a sub-directory path, 
verifying that {@code storeFileDirectly}
+     * correctly resolves the remote name when the name includes the base 
directory prefix.
+     */
+    @Test
+    void testChecksumFileIsWrittenInSubDirectory() throws Exception {
+        assumeTrue(isSetupComplete());
+
+        String body = "Bye World";
+        Exchange out = template.request(getScpUriWithChecksum(), exchange -> {
+            exchange.getIn().setHeader(Exchange.FILE_NAME, "subdir/bye.txt");
+            exchange.getIn().setBody(body);
+        });
+
+        assertNull(out.getException(), "Exchange must not fail: " + 
out.getException());
+
+        Path mainFile = Path.of(getScpPath(), "subdir", "bye.txt");
+        assertTrue(mainFile.toFile().exists(), "subdir/bye.txt must exist on 
the server");
+        assertEquals(body, Files.readString(mainFile));
+
+        Path checksumFile = Path.of(getScpPath(), "subdir", "bye.txt.md5");
+        assertTrue(checksumFile.toFile().exists(), "subdir/bye.txt.md5 must 
exist on the server");
+        assertEquals(md5Hex(body.getBytes(StandardCharsets.UTF_8)), 
Files.readString(checksumFile));
+    }
+
+    /**
+     * Without {@code checksumFileAlgorithm}, no {@code .md5} file is written 
and no checksum header is set (control).
+     */
+    @Test
+    void testNoChecksumFileWithoutOption() throws Exception {
+        assumeTrue(isSetupComplete());
+
+        String uri = getScpUri() + 
"?username=admin&password=admin&knownHostsFile=" + getKnownHostsFile();
+        Exchange out = template.request(uri, exchange -> {
+            exchange.getIn().setHeader(Exchange.FILE_NAME, "control.txt");
+            exchange.getIn().setBody("Control");
+        });
+
+        assertNull(out.getException(), "Exchange must not fail");
+        assertTrue(Path.of(getScpPath(), "control.txt").toFile().exists(), 
"control.txt must exist");
+        assertFalse(Path.of(getScpPath(), 
"control.txt.md5").toFile().exists(), "control.txt.md5 must not exist");
+        assertNull(out.getMessage().getHeader(FileConstants.FILE_CHECKSUM));
+    }
+}

Reply via email to