You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2020/08/21 07:15:19 UTC
[mina-sshd] 04/05: [SSHD-1056] Add SCP remote-to-remote transfer of
directories
This is an automated email from the ASF dual-hosted git repository.
lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit d1c18fe0f9886441dff32e6c56a48dd176c56d76
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Tue Aug 18 15:11:39 2020 +0300
[SSHD-1056] Add SCP remote-to-remote transfer of directories
---
.../org/apache/sshd/common/util/SelectorUtils.java | 39 ++-
.../sshd/common/util/PathsConcatentionTest.java | 87 ++++++
.../apache/sshd/common/util/SelectorUtilsTest.java | 8 +
.../scp/client/ScpRemote2RemoteTransferHelper.java | 304 ++++++++++++++++-----
.../client/ScpRemote2RemoteTransferListener.java | 37 +++
.../java/org/apache/sshd/scp/common/ScpHelper.java | 65 ++++-
.../common/helpers/ScpDirEndCommandDetails.java | 14 +
.../apache/sshd/scp/common/helpers/ScpIoUtils.java | 71 -----
.../common/helpers/ScpTimestampCommandDetails.java | 2 +-
.../client/ScpRemote2RemoteTransferHelperTest.java | 221 ++++++++++++++-
10 files changed, 692 insertions(+), 156 deletions(-)
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java
index 78e3235..9cb0a71 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java
@@ -477,7 +477,7 @@ public final class SelectorUtils {
/**
* Tests whether two characters are equal.
- *
+ *
* @param c1 1st character
* @param c2 2nd character
* @param isCaseSensitive Whether to compare case sensitive
@@ -520,7 +520,7 @@ public final class SelectorUtils {
* /** Converts a path to one matching the target file system by applying the "slashification" rules,
* converting it to a local path and then translating its separator to the target file system one (if different than
* local one)
- *
+ *
* @param path The input path
* @param pathSeparator The separator used to build the input path
* @param fs The target {@link FileSystem} - may not be {@code null}
@@ -536,7 +536,7 @@ public final class SelectorUtils {
* Converts a path to one matching the target file system by applying the "slashification" rules,
* converting it to a local path and then translating its separator to the target file system one (if different than
* local one)
- *
+ *
* @param path The input path
* @param pathSeparator The separator used to build the input path
* @param fsSeparator The target file system separator
@@ -559,7 +559,7 @@ public final class SelectorUtils {
* Specification version 3, section 3.266</A> and
* <A HREF="http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11">section 4.11 -
* Pathname resolution</A>
- *
+ *
* @param path The original path - ignored if {@code null}/empty or does not contain any slashes
* @param sepChar The "slash" character
* @return The effective path - may be same as input if no changes required
@@ -693,7 +693,7 @@ public final class SelectorUtils {
/**
* Converts a path containing a specific separator to one using the specified file-system one
- *
+ *
* @param path The input path - ignored if {@code null}/empty
* @param pathSeparator The separator used to build the input path - may not be {@code null}/empty
* @param fs The target {@link FileSystem} - may not be {@code null}
@@ -709,7 +709,7 @@ public final class SelectorUtils {
/**
* Converts a path containing a specific separator to one using the specified file-system one
- *
+ *
* @param path The input path - ignored if {@code null}/empty
* @param pathSeparator The separator used to build the input path - may not be {@code null}/empty
* @param fsSeparator The target file system separator - may not be {@code null}/empty
@@ -742,6 +742,33 @@ public final class SelectorUtils {
}
/**
+ * Creates a single path by concatenating 2 parts and taking care not to create FS separator duplication in the
+ * process
+ *
+ * @param p1 prefix part - ignored if {@code null}/empty
+ * @param p2 suffix part - ignored if {@code null}/empty
+ * @param fsSeparator The expected file-system separator
+ * @return Concatenation result
+ */
+ public static String concatPaths(String p1, String p2, char fsSeparator) {
+ if (GenericUtils.isEmpty(p1)) {
+ return p2;
+ } else if (GenericUtils.isEmpty(p2)) {
+ return p1;
+ } else if (p1.charAt(p1.length() - 1) == fsSeparator) {
+ if (p2.charAt(0) == fsSeparator) {
+ return (p2.length() == 1) ? p1 : p1 + p2.substring(1); // a/b/c/ + /d/e/f
+ } else {
+ return p1 + p2; // a/b/c/ + d/e/f
+ }
+ } else if (p2.charAt(0) == fsSeparator) {
+ return (p2.length() == 1) ? p1 : p1 + p2; // /a/b/c + /d/e/f
+ } else {
+ return p1 + Character.toString(fsSeparator) + p2; // /a/b/c + d/e/f
+ }
+ }
+
+ /**
* "Flattens" a string by removing all whitespace (space, tab, line-feed, carriage return, and form-feed). This uses
* StringTokenizer and the default set of tokens as documented in the single argument constructor.
*
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/PathsConcatentionTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/PathsConcatentionTest.java
new file mode 100644
index 0000000..bf8fd58
--- /dev/null
+++ b/sshd-common/src/test/java/org/apache/sshd/common/util/PathsConcatentionTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.sshd.common.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+@Category({ NoIoTestCase.class })
+public class PathsConcatentionTest extends JUnitTestSupport {
+ private final String p1;
+ private final String p2;
+ private final String expected;
+
+ public PathsConcatentionTest(String p1, String p2, String expected) {
+ this.p1 = p1;
+ this.p2 = p2;
+ this.expected = expected;
+ }
+
+ @Parameters(name = "p1={0}, p2={1}, expected={2}")
+ public static List<Object[]> parameters() {
+ return new ArrayList<Object[]>() {
+ // not serializing it
+ private static final long serialVersionUID = 1L;
+
+ {
+ addTestCase("/a/b/c", "d/e/f", "/a/b/c/d/e/f");
+ addTestCase("/a/b/c", "/d/e/f", "/a/b/c/d/e/f");
+ addTestCase("/a/b/c/", "d/e/f", "/a/b/c/d/e/f");
+ addTestCase("/a/b/c/", "/d/e/f", "/a/b/c/d/e/f");
+
+ addTestCase("/", "/d", "/d");
+ addTestCase("/a", "/", "/a");
+ addTestCase("/", "/", "/");
+
+ addTestCase(null, null, null);
+ addTestCase(null, "", "");
+ addTestCase("", null, null);
+ addTestCase("", "", "");
+ }
+
+ private void addTestCase(String p1, String p2, String expected) {
+ add(new Object[] { p1, p2, expected });
+ }
+ };
+ }
+
+ @Test
+ public void testConcatPaths() {
+ assertEquals(expected, SelectorUtils.concatPaths(p1, p2, '/'));
+ }
+}
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java
index 430873e..7fb0575 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java
@@ -144,4 +144,12 @@ public class SelectorUtilsTest extends JUnitTestSupport {
}
}
+ @Test
+ public void testConcatPathsOneEmptyOrNull() {
+ String path = getCurrentTestName();
+ assertSame("Null 1st", path, SelectorUtils.concatPaths(null, path, File.separatorChar));
+ assertSame("Empty 1st", path, SelectorUtils.concatPaths("", path, File.separatorChar));
+ assertSame("Null 2nd", path, SelectorUtils.concatPaths(path, null, File.separatorChar));
+ assertSame("Empty 2nd", path, SelectorUtils.concatPaths(path, "", File.separatorChar));
+ }
}
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java
index 2f88fc3..07a8ae2 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java
@@ -28,15 +28,20 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
+import java.util.Set;
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.util.SelectorUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.io.LimitInputStream;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
import org.apache.sshd.scp.client.ScpClient.Option;
import org.apache.sshd.scp.common.helpers.AbstractScpCommandDetails;
+import org.apache.sshd.scp.common.helpers.ScpDirEndCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpIoUtils;
+import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport;
+import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails;
@@ -88,8 +93,27 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
public void transferFile(String source, String destination, boolean preserveAttributes) throws IOException {
Collection<Option> options = preserveAttributes
? Collections.unmodifiableSet(EnumSet.of(Option.PreserveAttributes))
- : Collections.emptySet()
- ;
+ : Collections.emptySet();
+ executeTransfer(source, options, destination, options);
+ }
+
+ /**
+ * Transfers a directory
+ *
+ * @param source Source path in the source session
+ * @param destination Destination path in the destination session
+ * @param preserveAttributes Whether to preserve the attributes of the transferred file (e.g., permissions, file
+ * associated timestamps, etc.)
+ * @throws IOException If failed to transfer
+ */
+ public void transferDirectory(String source, String destination, boolean preserveAttributes)
+ throws IOException {
+ Set<Option> options = EnumSet.of(Option.TargetIsDirectory, Option.Recursive);
+ if (preserveAttributes) {
+ options.add(Option.PreserveAttributes);
+ }
+
+ options = Collections.unmodifiableSet(options);
executeTransfer(source, options, destination, options);
}
@@ -100,6 +124,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
String srcCmd = ScpClient.createReceiveCommand(source, srcOptions);
ClientSession srcSession = getSourceSession();
ClientSession dstSession = getDestinationSession();
+
boolean debugEnabled = log.isDebugEnabled();
if (debugEnabled) {
log.debug("executeTransfer({})[srcCmd='{}']) {} => {}",
@@ -120,77 +145,254 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
OutputStream dstOut = dstChannel.getInvertedIn()) {
int statusCode = transferStatusCode("XFER-CMD", dstIn, srcOut);
ScpIoUtils.validateCommandStatusCode("XFER-CMD", "executeTransfer", statusCode, false);
- redirectReceivedFile(source, srcIn, srcOut, destination, dstIn, dstOut);
+
+ if (srcOptions.contains(Option.TargetIsDirectory) || dstOptions.contains(Option.TargetIsDirectory)) {
+ redirectDirectoryTransfer(source, srcIn, srcOut, destination, dstIn, dstOut, 0);
+ } else {
+ redirectFileTransfer(source, srcIn, srcOut, destination, dstIn, dstOut);
+ }
} finally {
dstChannel.close(false);
}
} finally {
srcChannel.close(false);
}
-
}
- protected long redirectReceivedFile(
+ protected long redirectFileTransfer(
String source, InputStream srcIn, OutputStream srcOut,
String destination, InputStream dstIn, OutputStream dstOut)
throws IOException {
boolean debugEnabled = log.isDebugEnabled();
String header = ScpIoUtils.readLine(srcIn, false);
if (debugEnabled) {
- log.debug("redirectReceivedFile({}) header={}", this, header);
+ log.debug("redirectFileTransfer({}) {} => {}: header={}", this, source, destination, header);
}
- char cmdName = header.charAt(0);
ScpTimestampCommandDetails time = null;
- if (cmdName == ScpTimestampCommandDetails.COMMAND_NAME) {
+ if (header.charAt(0) == ScpTimestampCommandDetails.COMMAND_NAME) {
// Pass along the "T<mtime> 0 <atime> 0" and wait for response
- time = ScpTimestampCommandDetails.parseTime(header);
- // Read the next command - which must be a 'C' command
- header = transferTimestampCommand(source, srcIn, srcOut, destination, dstIn, dstOut, time);
- cmdName = header.charAt(0);
+ time = new ScpTimestampCommandDetails(header);
+ signalReceivedCommand(time);
+
+ header = transferTimestampCommand(source, srcIn, srcOut, destination, dstIn, dstOut, header);
+ if (debugEnabled) {
+ log.debug("redirectFileTransfer({}) {} => {}: header={}", this, source, destination, header);
+ }
}
- if (cmdName != ScpReceiveFileCommandDetails.COMMAND_NAME) {
- throw new StreamCorruptedException("Unexpected file command: " + header);
+ return handleFileTransferRequest(source, srcIn, srcOut, destination, dstIn, dstOut, time, header);
+ }
+
+ protected long handleFileTransferRequest(
+ String source, InputStream srcIn, OutputStream srcOut,
+ String destination, InputStream dstIn, OutputStream dstOut,
+ ScpTimestampCommandDetails fileTime, String header)
+ throws IOException {
+ if (header.charAt(0) != ScpReceiveFileCommandDetails.COMMAND_NAME) {
+ throw new IllegalArgumentException("Invalid file transfer request: " + header);
}
- ScpReceiveFileCommandDetails details = new ScpReceiveFileCommandDetails(header);
- signalReceivedCommand(details);
+ ScpIoUtils.writeLine(dstOut, header);
+ int statusCode = transferStatusCode(header, dstIn, srcOut);
+ ScpIoUtils.validateCommandStatusCode("[DST] " + header, "handleFileTransferRequest", statusCode, false);
+
+ ScpReceiveFileCommandDetails fileDetails = new ScpReceiveFileCommandDetails(header);
+ signalReceivedCommand(fileDetails);
+
+ ClientSession srcSession = getSourceSession();
+ ClientSession dstSession = getDestinationSession();
+ if (listener != null) {
+ listener.startDirectFileTransfer(srcSession, source, dstSession, destination, fileTime, fileDetails);
+ }
+
+ long xferCount;
+ try {
+ xferCount = transferSimpleFile(source, srcIn, srcOut, destination, dstIn, dstOut, header, fileDetails.getLength());
+ } catch (IOException | RuntimeException | Error e) {
+ if (listener != null) {
+ listener.endDirectFileTransfer(srcSession, source, dstSession, destination, fileTime, fileDetails, 0L, e);
+ }
+ throw e;
+ }
+
+ if (listener != null) {
+ listener.endDirectFileTransfer(srcSession, source, dstSession, destination, fileTime, fileDetails, xferCount, null);
+ }
+
+ return xferCount;
+ }
+
+ protected void redirectDirectoryTransfer(
+ String source, InputStream srcIn, OutputStream srcOut,
+ String destination, InputStream dstIn, OutputStream dstOut,
+ int depth)
+ throws IOException {
+ boolean debugEnabled = log.isDebugEnabled();
+ String header = ScpIoUtils.readLine(srcIn, false);
+ if (debugEnabled) {
+ log.debug("redirectDirectoryTransfer({})[depth={}] {} => {}: header={}",
+ this, depth, source, destination, header);
+ }
+
+ ScpTimestampCommandDetails time = null;
+ if (header.charAt(0) == ScpTimestampCommandDetails.COMMAND_NAME) {
+ // Pass along the "T<mtime> 0 <atime> 0" and wait for response
+ time = new ScpTimestampCommandDetails(header);
+ signalReceivedCommand(time);
+
+ header = transferTimestampCommand(source, srcIn, srcOut, destination, dstIn, dstOut, header);
+ if (debugEnabled) {
+ log.debug("redirectDirectoryTransfer({})[depth={}] {} => {}: header={}",
+ this, depth, source, destination, header);
+ }
+ }
+
+ handleDirectoryTransferRequest(source, srcIn, srcOut, destination, dstIn, dstOut, depth, time, header);
+ }
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ protected void handleDirectoryTransferRequest(
+ String srcPath, InputStream srcIn, OutputStream srcOut,
+ String dstPath, InputStream dstIn, OutputStream dstOut,
+ int depth, ScpTimestampCommandDetails dirTime, String header)
+ throws IOException {
+ if (header.charAt(0) != ScpReceiveDirCommandDetails.COMMAND_NAME) {
+ throw new IllegalArgumentException("Invalid file transfer request: " + header);
+ }
- // Pass along the "Cmmmm <length> <filename" command and wait for ACK
ScpIoUtils.writeLine(dstOut, header);
int statusCode = transferStatusCode(header, dstIn, srcOut);
- ScpIoUtils.validateCommandStatusCode("[DST] " + header, "redirectReceivedFile", statusCode, false);
- // Wait with ACK ready for transfer until ready to transfer data
- long xferCount = transferFileData(source, srcIn, srcOut, destination, dstIn, dstOut, time, details);
+ ScpIoUtils.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest", statusCode,
+ false);
+
+ ScpReceiveDirCommandDetails dirDetails = new ScpReceiveDirCommandDetails(header);
+ signalReceivedCommand(dirDetails);
+
+ String dirName = dirDetails.getName();
+ // 1st command refers to the first path component of the original source/destination
+ String source = (depth == 0) ? srcPath : SelectorUtils.concatPaths(srcPath, dirName, '/');
+ String destination = (depth == 0) ? dstPath : SelectorUtils.concatPaths(dstPath, dirName, '/');
+
+ ClientSession srcSession = getSourceSession();
+ ClientSession dstSession = getDestinationSession();
+ if (listener != null) {
+ listener.startDirectDirectoryTransfer(srcSession, source, dstSession, destination, dirTime, dirDetails);
+ }
+
+ try {
+ for (boolean debugEnabled = log.isDebugEnabled(), dirEndSignal = false;
+ !dirEndSignal;
+ debugEnabled = log.isDebugEnabled()) {
+ header = ScpIoUtils.readLine(srcIn, false);
+ if (debugEnabled) {
+ log.debug("handleDirectoryTransferRequest({})[depth={}] {} => {}: header={}",
+ this, depth, source, destination, header);
+ }
+
+ ScpTimestampCommandDetails time = null;
+ char cmdName = header.charAt(0);
+ if (cmdName == ScpTimestampCommandDetails.COMMAND_NAME) {
+ // Pass along the "T<mtime> 0 <atime> 0" and wait for response
+ time = new ScpTimestampCommandDetails(header);
+ signalReceivedCommand(time);
+
+ header = transferTimestampCommand(source, srcIn, srcOut, destination, dstIn, dstOut, header);
+ if (debugEnabled) {
+ log.debug("handleDirectoryTransferRequest({})[depth={}] {} => {}: header={}",
+ this, depth, source, destination, header);
+ }
+ cmdName = header.charAt(0);
+ }
+
+ switch (cmdName) {
+ case ScpReceiveFileCommandDetails.COMMAND_NAME:
+ case ScpReceiveDirCommandDetails.COMMAND_NAME: {
+ ScpPathCommandDetailsSupport subPathDetails = (cmdName == ScpReceiveFileCommandDetails.COMMAND_NAME)
+ ? new ScpReceiveFileCommandDetails(header)
+ : new ScpReceiveDirCommandDetails(header);
+ String name = subPathDetails.getName();
+ String srcSubPath = SelectorUtils.concatPaths(source, name, '/');
+ String dstSubPath = SelectorUtils.concatPaths(destination, name, '/');
+ if (cmdName == ScpReceiveFileCommandDetails.COMMAND_NAME) {
+ handleFileTransferRequest(srcSubPath, srcIn, srcOut, dstSubPath, dstIn, dstOut, time, header);
+ } else {
+ handleDirectoryTransferRequest(srcSubPath, srcIn, srcOut, dstSubPath, dstIn, dstOut, depth + 1,
+ time, header);
+ }
+ break;
+ }
+
+ case ScpDirEndCommandDetails.COMMAND_NAME: {
+ ScpIoUtils.writeLine(dstOut, header);
+ statusCode = transferStatusCode(header, dstIn, srcOut);
+ ScpIoUtils.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest",
+ statusCode, false);
+
+ ScpDirEndCommandDetails details = ScpDirEndCommandDetails.parse(header);
+ signalReceivedCommand(details);
+ dirEndSignal = true;
+ break;
+ }
+
+ default:
+ throw new StreamCorruptedException("Unexpected file command: " + header);
+ }
+ }
+ } catch (IOException | RuntimeException | Error e) {
+ if (listener != null) {
+ listener.endDirectDirectoryTransfer(srcSession, source, dstSession, destination, dirTime, dirDetails, e);
+ }
+ throw e;
+ }
+
+ if (listener != null) {
+ listener.endDirectDirectoryTransfer(srcSession, source, dstSession, destination, dirTime, dirDetails, null);
+ }
+ }
+
+ protected long transferSimpleFile(
+ String source, InputStream srcIn, OutputStream srcOut,
+ String destination, InputStream dstIn, OutputStream dstOut,
+ String header, long length)
+ throws IOException {
+ if (length < 0L) { // TODO consider throwing an exception...
+ log.warn("transferSimpleFile({})[{} => {}] bad length in header: {}",
+ this, source, destination, header);
+ }
+
+ long xferCount;
+ try (InputStream inputStream = new LimitInputStream(srcIn, length)) {
+ ScpIoUtils.ack(srcOut); // ready to receive the data from source
+ xferCount = IoUtils.copy(inputStream, dstOut);
+ dstOut.flush(); // make sure all data sent to destination
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("transferSimpleFile({})[{} => {}] xfer {}/{}",
+ this, source, destination, xferCount, length);
+ }
// wait for source to signal data finished and pass it along
- statusCode = transferStatusCode("SRC-EOF", srcIn, dstOut);
- ScpIoUtils.validateCommandStatusCode("[SRC-EOF] " + header, "redirectReceivedFile", statusCode, false);
+ int statusCode = transferStatusCode("SRC-EOF", srcIn, dstOut);
+ ScpIoUtils.validateCommandStatusCode("[SRC-EOF] " + header, "transferSimpleFile", statusCode, false);
// wait for destination to signal data received
statusCode = ScpIoUtils.readAck(dstIn, false, log, "DST-EOF");
- ScpIoUtils.validateCommandStatusCode("[DST-EOF] " + header, "redirectReceivedFile", statusCode, false);
+ ScpIoUtils.validateCommandStatusCode("[DST-EOF] " + header, "transferSimpleFile", statusCode, false);
return xferCount;
}
protected String transferTimestampCommand(
String source, InputStream srcIn, OutputStream srcOut,
String destination, InputStream dstIn, OutputStream dstOut,
- ScpTimestampCommandDetails time)
+ String header)
throws IOException {
- signalReceivedCommand(time);
-
- String header = time.toHeader();
ScpIoUtils.writeLine(dstOut, header);
int statusCode = transferStatusCode(header, dstIn, srcOut);
ScpIoUtils.validateCommandStatusCode("[DST] " + header, "transferTimestampCommand", statusCode, false);
header = ScpIoUtils.readLine(srcIn, false);
- if (log.isDebugEnabled()) {
- log.debug("transferTimestampCommand({}) header={}", this, header);
- }
-
return header;
}
@@ -218,46 +420,6 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
return statusCode;
}
- protected long transferFileData(
- String source, InputStream srcIn, OutputStream srcOut,
- String destination, InputStream dstIn, OutputStream dstOut,
- ScpTimestampCommandDetails time, ScpReceiveFileCommandDetails details)
- throws IOException {
- long length = details.getLength();
- if (length < 0L) { // TODO consider throwing an exception...
- log.warn("transferFileData({})[{} => {}] bad length in header: {}",
- this, source, destination, details.toHeader());
- }
-
- ClientSession srcSession = getSourceSession();
- ClientSession dstSession = getDestinationSession();
- if (listener != null) {
- listener.startDirectFileTransfer(srcSession, source, dstSession, destination, time, details);
- }
-
- long xferCount;
- try (InputStream inputStream = new LimitInputStream(srcIn, length)) {
- ScpIoUtils.ack(srcOut); // ready to receive the data from source
- xferCount = IoUtils.copy(inputStream, dstOut);
- dstOut.flush(); // make sure all data sent to destination
- } catch (IOException | RuntimeException | Error e) {
- if (listener != null) {
- listener.endDirectFileTransfer(srcSession, source, dstSession, destination, time, details, 0L, e);
- }
- throw e;
- }
-
- if (log.isDebugEnabled()) {
- log.debug("transferFileData({})[{} => {}] xfer {}/{} for {}",
- this, source, destination, xferCount, length, details.getName());
- }
- if (listener != null) {
- listener.endDirectFileTransfer(srcSession, source, dstSession, destination, time, details, xferCount, null);
- }
-
- return xferCount;
- }
-
// Useful "hook" for implementors
protected void signalReceivedCommand(AbstractScpCommandDetails details) throws IOException {
if (log.isDebugEnabled()) {
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java
index 8ddad59..5aea4a4 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferListener.java
@@ -22,6 +22,7 @@ package org.apache.sshd.scp.client;
import java.io.IOException;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails;
@@ -65,4 +66,40 @@ public interface ScpRemote2RemoteTransferListener {
ScpTimestampCommandDetails timestamp, ScpReceiveFileCommandDetails details,
long xferSize, Throwable thrown)
throws IOException;
+
+ /**
+ * Indicates start of direct directory transfer
+ *
+ * @param srcSession The source {@link ClientSession}
+ * @param source The source path
+ * @param dstSession The destination {@link ClientSession}
+ * @param destination The destination path
+ * @param timestamp The {@link ScpTimestampCommandDetails timestamp} of the directory - may be {@code null}
+ * @param details The {@link ScpReceiveDirCommandDetails details} of the attempted directory transfer
+ * @throws IOException If failed to handle the callback
+ */
+ void startDirectDirectoryTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp, ScpReceiveDirCommandDetails details)
+ throws IOException;
+
+ /**
+ * Indicates end of direct file transfer
+ *
+ * @param srcSession The source {@link ClientSession}
+ * @param source The source path
+ * @param dstSession The destination {@link ClientSession}
+ * @param destination The destination path
+ * @param timestamp The {@link ScpTimestampCommandDetails timestamp} of the directory - may be {@code null}
+ * @param details The {@link ScpReceiveDirCommandDetails details} of the attempted directory transfer
+ * @param thrown Error thrown during transfer attempt - {@code null} if successful
+ * @throws IOException If failed to handle the callback
+ */
+ void endDirectDirectoryTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp, ScpReceiveDirCommandDetails details,
+ Throwable thrown)
+ throws IOException;
}
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java
index 4a6b111..1a68d86 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java
@@ -160,8 +160,67 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
});
}
+ /**
+ * Reads command line(s) and invokes the handler until EOF or and "E" command is received
+ *
+ * @param handler The {@link ScpReceiveLineHandler} to invoke when a command has been read
+ * @throws IOException If failed to read/write
+ */
protected void receive(ScpReceiveLineHandler handler) throws IOException {
- ScpIoUtils.receive(getSession(), in, out, log, this, handler);
+ ack();
+
+ boolean debugEnabled = log.isDebugEnabled();
+ Session session = getSession();
+ for (ScpTimestampCommandDetails time = null;; debugEnabled = log.isDebugEnabled()) {
+ String line;
+ boolean isDir = false;
+ int c = readAck(true);
+ switch (c) {
+ case -1:
+ return;
+ case ScpReceiveDirCommandDetails.COMMAND_NAME:
+ line = ScpIoUtils.readLine(in);
+ line = Character.toString((char) c) + line;
+ isDir = true;
+ if (debugEnabled) {
+ log.debug("receive({}) - Received 'D' header: {}", this, line);
+ }
+ break;
+ case ScpReceiveFileCommandDetails.COMMAND_NAME:
+ line = ScpIoUtils.readLine(in);
+ line = Character.toString((char) c) + line;
+ if (debugEnabled) {
+ log.debug("receive({}) - Received 'C' header: {}", this, line);
+ }
+ break;
+ case ScpTimestampCommandDetails.COMMAND_NAME:
+ line = ScpIoUtils.readLine(in);
+ line = Character.toString((char) c) + line;
+ if (debugEnabled) {
+ log.debug("receive({}) - Received 'T' header: {}", this, line);
+ }
+ time = ScpTimestampCommandDetails.parse(line);
+ ack();
+ continue;
+ case ScpDirEndCommandDetails.COMMAND_NAME:
+ line = ScpIoUtils.readLine(in);
+ line = Character.toString((char) c) + line;
+ if (debugEnabled) {
+ log.debug("receive({}) - Received 'E' header: {}", this, line);
+ }
+ ack();
+ return;
+ default:
+ // a real ack that has been acted upon already
+ continue;
+ }
+
+ try {
+ handler.process(session, line, isDir, time);
+ } finally {
+ time = null;
+ }
+ }
}
public void receiveDir(String header, Path local, ScpTimestampCommandDetails time, boolean preserve, int bufferSize)
@@ -176,7 +235,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
ScpReceiveDirCommandDetails details = new ScpReceiveDirCommandDetails(header);
String name = details.getName();
long length = details.getLength();
- if (length != 0) {
+ if (length != 0L) {
throw new IOException("Expected 0 length for directory=" + name + " but got " + length);
}
@@ -207,7 +266,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
ack();
break;
} else if (cmdChar == ScpTimestampCommandDetails.COMMAND_NAME) {
- time = ScpTimestampCommandDetails.parseTime(header);
+ time = ScpTimestampCommandDetails.parse(header);
ack();
} else {
throw new IOException("Unexpected message: '" + header + "'");
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java
index 3075384..6fd02e5 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java
@@ -19,6 +19,8 @@
package org.apache.sshd.scp.common.helpers;
+import org.apache.sshd.common.util.GenericUtils;
+
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
@@ -64,4 +66,16 @@ public class ScpDirEndCommandDetails extends AbstractScpCommandDetails {
// All ScpDirEndCommandDetails are equal to each other
return true;
}
+
+ public static ScpDirEndCommandDetails parse(String header) {
+ if (GenericUtils.isEmpty(header)) {
+ return null;
+ }
+
+ if (HEADER.equals(header)) {
+ return INSTANCE;
+ }
+
+ throw new IllegalArgumentException("Invalid header: " + header);
+ }
}
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java
index 0f95834..775a502 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java
@@ -35,12 +35,10 @@ import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.core.CoreModuleProperties;
import org.apache.sshd.scp.ScpModuleProperties;
import org.apache.sshd.scp.common.ScpException;
-import org.apache.sshd.scp.common.ScpReceiveLineHandler;
import org.slf4j.Logger;
/**
@@ -183,75 +181,6 @@ public final class ScpIoUtils {
out.flush();
}
- /**
- * Reads command line(s) and invokes the handler until EOF or and "E" command is received
- *
- * @param session The associated {@link Session}
- * @param in The {@link InputStream} to read from
- * @param out The {@link OutputStream} to write ACKs to
- * @param log An optional {@link Logger} to use for issuing log messages - ignored if {@code null}
- * @param logHint An optional hint to be used in the logged messages to identifier the caller's context
- * @param handler The {@link ScpReceiveLineHandler} to invoke when a command has been read
- * @throws IOException If failed to read/write
- */
- public static void receive(
- Session session, InputStream in, OutputStream out, Logger log, Object logHint, ScpReceiveLineHandler handler)
- throws IOException {
- ack(out);
-
- boolean debugEnabled = (log != null) && log.isDebugEnabled();
- for (ScpTimestampCommandDetails time = null;;) {
- String line;
- boolean isDir = false;
- int c = readAck(in, true, log, logHint);
- switch (c) {
- case -1:
- return;
- case ScpReceiveDirCommandDetails.COMMAND_NAME:
- line = readLine(in);
- line = Character.toString((char) c) + line;
- isDir = true;
- if (debugEnabled) {
- log.debug("receive({}) - Received 'D' header: {}", logHint, line);
- }
- break;
- case ScpReceiveFileCommandDetails.COMMAND_NAME:
- line = readLine(in);
- line = Character.toString((char) c) + line;
- if (debugEnabled) {
- log.debug("receive({}) - Received 'C' header: {}", logHint, line);
- }
- break;
- case ScpTimestampCommandDetails.COMMAND_NAME:
- line = readLine(in);
- line = Character.toString((char) c) + line;
- if (debugEnabled) {
- log.debug("receive({}) - Received 'T' header: {}", logHint, line);
- }
- time = ScpTimestampCommandDetails.parseTime(line);
- ack(out);
- continue;
- case ScpDirEndCommandDetails.COMMAND_NAME:
- line = readLine(in);
- line = Character.toString((char) c) + line;
- if (debugEnabled) {
- log.debug("receive({}) - Received 'E' header: {}", logHint, line);
- }
- ack(out);
- return;
- default:
- // a real ack that has been acted upon already
- continue;
- }
-
- try {
- handler.process(session, line, isDir, time);
- } finally {
- time = null;
- }
- }
- }
-
public static <O extends OutputStream> O sendWarning(O out, String message) throws IOException {
return sendResponseMessage(out, WARNING, message);
}
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpTimestampCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpTimestampCommandDetails.java
index e1a085d..a8fa773 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpTimestampCommandDetails.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpTimestampCommandDetails.java
@@ -111,7 +111,7 @@ public class ScpTimestampCommandDetails extends AbstractScpCommandDetails {
* @see <A HREF="https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works">How the
* SCP protocol works</A>
*/
- public static ScpTimestampCommandDetails parseTime(String line) throws NumberFormatException {
+ public static ScpTimestampCommandDetails parse(String line) throws NumberFormatException {
return GenericUtils.isEmpty(line) ? null : new ScpTimestampCommandDetails(line);
}
}
diff --git a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java
index f28739c..043a37d 100644
--- a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java
+++ b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelperTest.java
@@ -20,33 +20,107 @@
package org.apache.sshd.scp.client;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.scp.common.ScpHelper;
+import org.apache.sshd.scp.common.ScpTransferEventListener;
+import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails;
+import org.apache.sshd.scp.server.ScpCommandFactory;
import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
public class ScpRemote2RemoteTransferHelperTest extends AbstractScpTestSupport {
- public ScpRemote2RemoteTransferHelperTest() {
- super();
+ protected final Logger log;
+ private final boolean preserveAttributes;
+
+ public ScpRemote2RemoteTransferHelperTest(boolean preserveAttributes) {
+ this.preserveAttributes = preserveAttributes;
+ this.log = LoggerFactory.getLogger(getClass());
}
@BeforeClass
public static void setupClientAndServer() throws Exception {
setupClientAndServer(ScpRemote2RemoteTransferHelperTest.class);
+
+ ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
+ factory.addEventListener(new ScpTransferEventListener() {
+ private final Logger log = LoggerFactory.getLogger(ScpRemote2RemoteTransferHelperTest.class);
+
+ @Override
+ public void startFileEvent(
+ Session session, FileOperation op, Path file,
+ long length, Set<PosixFilePermission> perms)
+ throws IOException {
+ log.info("startFileEvent({})[{}] {}", session, op, file);
+ }
+
+ @Override
+ public void endFileEvent(
+ Session session, FileOperation op, Path file,
+ long length, Set<PosixFilePermission> perms, Throwable thrown)
+ throws IOException {
+ if (thrown == null) {
+ log.info("endFileEvent({})[{}] {}", session, op, file);
+ } else {
+ log.error("endFileEvent({})[{}] {}: {}", session, op, file, thrown);
+ }
+ }
+
+ @Override
+ public void startFolderEvent(
+ Session session, FileOperation op, Path file,
+ Set<PosixFilePermission> perms)
+ throws IOException {
+ log.info("startFolderEvent({})[{}] {}", session, op, file);
+ }
+
+ @Override
+ public void endFolderEvent(
+ Session session, FileOperation op, Path file,
+ Set<PosixFilePermission> perms,
+ Throwable thrown)
+ throws IOException {
+ if (thrown == null) {
+ log.info("endFolderEvent({})[{}] {}", session, op, file);
+ } else {
+ log.error("endFolderEvent({})[{}] {}: {}", session, op, file, thrown);
+ }
+ }
+ });
+ }
+
+ @Parameters(name = "preserveAttributes={0}")
+ public static List<Object[]> parameters() {
+ return parameterize(Arrays.asList(Boolean.TRUE, Boolean.FALSE));
}
@Test
@@ -54,7 +128,7 @@ public class ScpRemote2RemoteTransferHelperTest extends AbstractScpTestSupport {
Path targetPath = detectTargetFolder();
Path parentPath = targetPath.getParent();
Path scpRoot = CommonTestSupportUtils.resolve(targetPath,
- ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), "testTransferFiles-" + preserveAttributes);
CommonTestSupportUtils.deleteRecursive(scpRoot); // start clean
Path srcDir = assertHierarchyTargetFolderExists(scpRoot.resolve("srcdir"));
@@ -95,8 +169,29 @@ public class ScpRemote2RemoteTransferHelperTest extends AbstractScpTestSupport {
long prev = xferCount.getAndSet(xferSize);
assertEquals("Mismatched 1st end file xfer size", 0L, prev);
}
+
+ @Override
+ public void startDirectDirectoryTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp,
+ ScpReceiveDirCommandDetails details)
+ throws IOException {
+ fail("Unexpected start directory transfer: " + source + " => " + destination);
+ }
+
+ @Override
+ public void endDirectDirectoryTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp,
+ ScpReceiveDirCommandDetails details,
+ Throwable thrown)
+ throws IOException {
+ fail("Unexpected end directory transfer: " + source + " => " + destination);
+ }
});
- helper.transferFile(srcPath, dstPath, true);
+ helper.transferFile(srcPath, dstPath, preserveAttributes);
}
assertEquals("Mismatched transfer size", expectedData.length, xferCount.getAndSet(0L));
@@ -104,6 +199,119 @@ public class ScpRemote2RemoteTransferHelperTest extends AbstractScpTestSupport {
assertArrayEquals("Mismatched transfer contents", expectedData, actualData);
}
+ @Test
+ public void testTransferDirectoriesRecursively() throws Exception {
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = CommonTestSupportUtils.resolve(targetPath,
+ ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(),
+ "testTransferDirectories-" + preserveAttributes);
+ CommonTestSupportUtils.deleteRecursive(scpRoot); // start clean
+
+ Path srcDir = assertHierarchyTargetFolderExists(scpRoot.resolve("srcdir"));
+ Path curDir = assertHierarchyTargetFolderExists(srcDir.resolve("root"));
+ String srcPath = CommonTestSupportUtils.resolveRelativeRemotePath(parentPath, curDir);
+ for (int depth = 0; depth <= 3; depth++) {
+ curDir = assertHierarchyTargetFolderExists(curDir);
+
+ Path curFile = curDir.resolve(depth + ".txt");
+ CommonTestSupportUtils.writeFile(
+ curFile, getClass().getName() + "#" + getCurrentTestName() + "@" + depth + IoUtils.EOL);
+ curDir = curDir.resolve("0" + Integer.toHexString(depth));
+ }
+
+ Path dstDir = assertHierarchyTargetFolderExists(scpRoot.resolve("dstdir"));
+ String dstPath = CommonTestSupportUtils.resolveRelativeRemotePath(parentPath, dstDir);
+ try (ClientSession srcSession = createClientSession(getCurrentTestName() + "-src");
+ ClientSession dstSession = createClientSession(getCurrentTestName() + "-dst")) {
+ ScpRemote2RemoteTransferHelper helper = new ScpRemote2RemoteTransferHelper(
+ srcSession, dstSession,
+ new ScpRemote2RemoteTransferListener() {
+ private final String logHint = getCurrentTestName();
+
+ @Override
+ public void startDirectFileTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp,
+ ScpReceiveFileCommandDetails details)
+ throws IOException {
+ log.info("{}: startDirectFileTransfer - {} => {}",
+ logHint, source, destination);
+ }
+
+ @Override
+ public void startDirectDirectoryTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp,
+ ScpReceiveDirCommandDetails details)
+ throws IOException {
+ log.info("{}: startDirectDirectoryTransfer - {} => {}",
+ logHint, source, destination);
+ }
+
+ @Override
+ public void endDirectFileTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp,
+ ScpReceiveFileCommandDetails details,
+ long xferSize, Throwable thrown)
+ throws IOException {
+ log.info("{}: endDirectFileTransfer - {} => {}: size={}, thrown={}",
+ logHint, source, destination, xferSize,
+ (thrown == null) ? null : thrown.getClass().getSimpleName());
+ }
+
+ @Override
+ public void endDirectDirectoryTransfer(
+ ClientSession srcSession, String source,
+ ClientSession dstSession, String destination,
+ ScpTimestampCommandDetails timestamp,
+ ScpReceiveDirCommandDetails details,
+ Throwable thrown)
+ throws IOException {
+ log.info("{}: endDirectDirectoryTransfer {} => {}: thrown={}",
+ logHint, source, destination, (thrown == null) ? null : thrown.getClass().getSimpleName());
+ }
+ });
+ helper.transferDirectory(srcPath, dstPath, preserveAttributes);
+ }
+
+ validateXferDirContents(srcDir, dstDir);
+ }
+
+ private static void validateXferDirContents(Path srcPath, Path dstPath) throws Exception {
+ try (DirectoryStream<Path> srcDir = Files.newDirectoryStream(srcPath)) {
+ for (Path srcFile : srcDir) {
+ String name = srcFile.getFileName().toString();
+ Path dstFile = dstPath.resolve(name);
+ if (Files.isDirectory(srcFile)) {
+ validateXferDirContents(srcFile, dstFile);
+ } else {
+ byte[] srcData = Files.readAllBytes(srcFile);
+ byte[] dstData = Files.readAllBytes(dstFile);
+ assertEquals(name + "[DATA]",
+ new String(srcData, StandardCharsets.UTF_8),
+ new String(dstData, StandardCharsets.UTF_8));
+ }
+ }
+ }
+
+ try (DirectoryStream<Path> dstDir = Files.newDirectoryStream(dstPath)) {
+ for (Path dstFile : dstDir) {
+ String name = dstFile.getFileName().toString();
+ Path srcFile = srcPath.resolve(name);
+ if (Files.isDirectory(dstFile)) {
+ assertTrue(name + ": unmatched destination folder", Files.isDirectory(srcFile));
+ } else {
+ assertTrue(name + ": unmatched destination file", Files.exists(srcFile));
+ }
+ }
+ }
+ }
+
private ClientSession createClientSession(String username) throws IOException {
ClientSession session = client.connect(username, TEST_LOCALHOST, port)
.verify(CONNECT_TIMEOUT)
@@ -121,4 +329,9 @@ public class ScpRemote2RemoteTransferHelperTest extends AbstractScpTestSupport {
}
}
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[preserveAttributes=" + preserveAttributes + "]";
+ }
}