[prev in list] [next in list] [prev in thread] [next in thread]
List: mina-commits
Subject: [mina-sshd] 04/05: [SSHD-1056] Add SCP remote-to-remote transfer of directories
From: lgoldstein () apache ! org
Date: 2020-08-21 7:15:19
Message-ID: 20200821071516.16B56890BC () gitbox ! apache ! org
[Download RAW message or body]
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 <lgoldstein@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 + "]"; + }
}
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic