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 2018/04/25 05:01:25 UTC
[4/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
new file mode 100644
index 0000000..81c20db
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
@@ -0,0 +1,278 @@
+/*
+ * 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.client.scp;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+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.FactoryManager;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractScpClient extends AbstractLoggingBean implements ScpClient {
+ public static final Set<ClientChannelEvent> COMMAND_WAIT_EVENTS =
+ Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.CLOSED));
+
+ protected AbstractScpClient() {
+ super();
+ }
+
+ @Override
+ public final ClientSession getSession() {
+ return getClientSession();
+ }
+
+ @Override
+ public void download(String[] remote, String local, Collection<Option> options) throws IOException {
+ local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", (Object) remote);
+
+ if (remote.length > 1) {
+ options = addTargetIsDirectory(options);
+ }
+
+ for (String r : remote) {
+ download(r, local, options);
+ }
+ }
+
+ @Override
+ public void download(String[] remote, Path local, Collection<Option> options) throws IOException {
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", (Object) remote);
+
+ if (remote.length > 1) {
+ options = addTargetIsDirectory(options);
+ }
+
+ for (String r : remote) {
+ download(r, local, options);
+ }
+ }
+
+ @Override
+ public void download(String remote, Path local, Collection<Option> options) throws IOException {
+ local = ValidateUtils.checkNotNull(local, "Invalid argument local: %s", local);
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", remote);
+
+ LinkOption[] opts = IoUtils.getLinkOptions(true);
+ if (Files.isDirectory(local, opts)) {
+ options = addTargetIsDirectory(options);
+ }
+
+ if (options.contains(Option.TargetIsDirectory)) {
+ Boolean status = IoUtils.checkFileExists(local, opts);
+ if (status == null) {
+ throw new SshException("Target directory " + local.toString() + " is probably inaccesible");
+ }
+
+ if (!status) {
+ throw new SshException("Target directory " + local.toString() + " does not exist");
+ }
+
+ if (!Files.isDirectory(local, opts)) {
+ throw new SshException("Target directory " + local.toString() + " is not a directory");
+ }
+ }
+
+ download(remote, local.getFileSystem(), local, options);
+ }
+
+ @Override
+ public void download(String remote, String local, Collection<Option> options) throws IOException {
+ local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
+
+ ClientSession session = getClientSession();
+ FactoryManager manager = session.getFactoryManager();
+ FileSystemFactory factory = manager.getFileSystemFactory();
+ FileSystem fs = factory.createFileSystem(session);
+ try {
+ download(remote, fs, fs.getPath(local), options);
+ } finally {
+ try {
+ fs.close();
+ } catch (UnsupportedOperationException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("download({}) {} => {} - failed ({}) to close file system={}: {}",
+ session, remote, local, e.getClass().getSimpleName(), fs, e.getMessage());
+ }
+ }
+ }
+ }
+
+ protected abstract void download(String remote, FileSystem fs, Path local, Collection<Option> options) throws IOException;
+
+ @Override
+ public void upload(String[] local, String remote, Collection<Option> options) throws IOException {
+ final Collection<String> paths = Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", (Object) local));
+ runUpload(remote, options, paths, (helper, local1, sendOptions) ->
+ helper.send(local1,
+ sendOptions.contains(Option.Recursive),
+ sendOptions.contains(Option.PreserveAttributes),
+ ScpHelper.DEFAULT_SEND_BUFFER_SIZE));
+ }
+
+ @Override
+ public void upload(Path[] local, String remote, Collection<Option> options) throws IOException {
+ final Collection<Path> paths = Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", (Object) local));
+ runUpload(remote, options, paths, (helper, local1, sendOptions) ->
+ helper.sendPaths(local1,
+ sendOptions.contains(Option.Recursive),
+ sendOptions.contains(Option.PreserveAttributes),
+ ScpHelper.DEFAULT_SEND_BUFFER_SIZE));
+ }
+
+ protected abstract <T> void runUpload(String remote, Collection<Option> options, Collection<T> local, AbstractScpClient.ScpOperationExecutor<T> executor) throws IOException;
+
+ /**
+ * Invoked by the various <code>upload/download</code> methods after having successfully
+ * completed the remote copy command and (optionally) having received an exit status
+ * from the remote server. If no exit status received within {@link FactoryManager#CHANNEL_CLOSE_TIMEOUT}
+ * the no further action is taken. Otherwise, the exit status is examined to ensure it
+ * is either OK or WARNING - if not, an {@link ScpException} is thrown
+ *
+ * @param cmd The attempted remote copy command
+ * @param channel The {@link ClientChannel} through which the command was sent - <B>Note:</B>
+ * then channel may be in the process of being closed
+ * @throws IOException If failed the command
+ * @see #handleCommandExitStatus(String, Integer)
+ */
+ protected void handleCommandExitStatus(String cmd, ClientChannel channel) throws IOException {
+ // give a chance for the exit status to be received
+ long timeout = channel.getLongProperty(SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT, DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT);
+ if (timeout <= 0L) {
+ handleCommandExitStatus(cmd, (Integer) null);
+ return;
+ }
+
+ long waitStart = System.nanoTime();
+ Collection<ClientChannelEvent> events = channel.waitFor(COMMAND_WAIT_EVENTS, timeout);
+ long waitEnd = System.nanoTime();
+ if (log.isDebugEnabled()) {
+ log.debug("handleCommandExitStatus({}) cmd='{}', waited={} nanos, events={}",
+ getClientSession(), cmd, waitEnd - waitStart, events);
+ }
+
+ /*
+ * There are sometimes race conditions in the order in which channels are closed and exit-status
+ * sent by the remote peer (if at all), thus there is no guarantee that we will have an exit
+ * status here
+ */
+ handleCommandExitStatus(cmd, channel.getExitStatus());
+ }
+
+ /**
+ * Invoked by the various <code>upload/download</code> methods after having successfully
+ * completed the remote copy command and (optionally) having received an exit status
+ * from the remote server
+ *
+ * @param cmd The attempted remote copy command
+ * @param exitStatus The exit status - if {@code null} then no status was reported
+ * @throws IOException If failed the command
+ */
+ protected void handleCommandExitStatus(String cmd, Integer exitStatus) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("handleCommandExitStatus({}) cmd='{}', exit-status={}", getClientSession(), cmd, ScpHelper.getExitStatusName(exitStatus));
+ }
+
+ if (exitStatus == null) {
+ return;
+ }
+
+ int statusCode = exitStatus;
+ switch (statusCode) {
+ case ScpHelper.OK: // do nothing
+ break;
+ case ScpHelper.WARNING:
+ log.warn("handleCommandExitStatus({}) cmd='{}' may have terminated with some problems", getClientSession(), cmd);
+ break;
+ default:
+ throw new ScpException("Failed to run command='" + cmd + "': " + ScpHelper.getExitStatusName(exitStatus), exitStatus);
+ }
+ }
+
+ protected Collection<Option> addTargetIsDirectory(Collection<Option> options) {
+ if (GenericUtils.isEmpty(options) || (!options.contains(Option.TargetIsDirectory))) {
+ // create a copy in case the original collection is un-modifiable
+ options = GenericUtils.isEmpty(options) ? EnumSet.noneOf(Option.class) : GenericUtils.of(options);
+ options.add(Option.TargetIsDirectory);
+ }
+
+ return options;
+ }
+
+ protected ChannelExec openCommandChannel(ClientSession session, String cmd) throws IOException {
+ long waitTimeout = session.getLongProperty(SCP_EXEC_CHANNEL_OPEN_TIMEOUT, DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT);
+ ChannelExec channel = session.createExecChannel(cmd);
+
+ long startTime = System.nanoTime();
+ try {
+ channel.open().verify(waitTimeout);
+ long endTime = System.nanoTime();
+ long nanosWait = endTime - startTime;
+ if (log.isTraceEnabled()) {
+ log.trace("openCommandChannel(" + session + ")[" + cmd + "]"
+ + " completed after " + nanosWait
+ + " nanos out of " + TimeUnit.MILLISECONDS.toNanos(waitTimeout));
+ }
+
+ return channel;
+ } catch (IOException | RuntimeException e) {
+ long endTime = System.nanoTime();
+ long nanosWait = endTime - startTime;
+ if (log.isTraceEnabled()) {
+ log.trace("openCommandChannel(" + session + ")[" + cmd + "]"
+ + " failed (" + e.getClass().getSimpleName() + ")"
+ + " to complete after " + nanosWait
+ + " nanos out of " + TimeUnit.MILLISECONDS.toNanos(waitTimeout)
+ + ": " + e.getMessage());
+ }
+
+ channel.close(false);
+ throw e;
+ }
+ }
+
+ @FunctionalInterface
+ public interface ScpOperationExecutor<T> {
+ void execute(ScpHelper helper, Collection<T> local, Collection<Option> options) throws IOException;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java
new file mode 100644
index 0000000..34ef7e5
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.client.scp;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractScpClientCreator extends AbstractLoggingBean implements ScpClientCreator {
+ private ScpFileOpener opener = DefaultScpFileOpener.INSTANCE;
+ private ScpTransferEventListener listener;
+
+ protected AbstractScpClientCreator() {
+ this("");
+ }
+
+ public AbstractScpClientCreator(String discriminator) {
+ super(discriminator);
+ }
+
+ @Override
+ public ScpFileOpener getScpFileOpener() {
+ return opener;
+ }
+
+ @Override
+ public void setScpFileOpener(ScpFileOpener opener) {
+ this.opener = opener;
+ }
+
+ @Override
+ public ScpTransferEventListener getScpTransferEventListener() {
+ return listener;
+ }
+
+ @Override
+ public void setScpTransferEventListener(ScpTransferEventListener listener) {
+ this.listener = listener;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
new file mode 100644
index 0000000..40afaf7
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
@@ -0,0 +1,32 @@
+/*
+ * 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.client.scp;
+
+import java.nio.channels.Channel;
+
+/**
+ * An {@link ScpClient} wrapper that also closes the underlying session
+ * when closed
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface CloseableScpClient extends ScpClient, Channel {
+ // Marker interface
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
new file mode 100644
index 0000000..16d0cb2
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
@@ -0,0 +1,159 @@
+/*
+ * 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.client.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Objects;
+
+import org.apache.sshd.client.channel.ChannelExec;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.util.MockFileSystem;
+import org.apache.sshd.common.file.util.MockPath;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpClient extends AbstractScpClient {
+ protected final ScpFileOpener opener;
+ protected final ScpTransferEventListener listener;
+ private final ClientSession clientSession;
+
+ public DefaultScpClient(
+ ClientSession clientSession, ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
+ this.clientSession = Objects.requireNonNull(clientSession, "No client session");
+ this.opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
+ this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
+ }
+
+ @Override
+ public ClientSession getClientSession() {
+ return clientSession;
+ }
+
+ @Override
+ public void download(String remote, OutputStream local) throws IOException {
+ String cmd = ScpClient.createReceiveCommand(remote, Collections.emptyList());
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ // NOTE: we use a mock file system since we expect no invocations for it
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
+ helper.receiveFileStream(local, ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+
+ @Override
+ protected void download(String remote, FileSystem fs, Path local, Collection<Option> options) throws IOException {
+ String cmd = ScpClient.createReceiveCommand(remote, options);
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
+ helper.receive(local,
+ options.contains(Option.Recursive),
+ options.contains(Option.TargetIsDirectory),
+ options.contains(Option.PreserveAttributes),
+ ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+
+ @Override
+ public void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ int namePos = ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified").lastIndexOf('/');
+ String name = (namePos < 0)
+ ? remote
+ : ValidateUtils.checkNotNullAndNotEmpty(remote.substring(namePos + 1), "No name value in remote=%s", remote);
+ Collection<Option> options = (time != null) ? EnumSet.of(Option.PreserveAttributes) : Collections.emptySet();
+ String cmd = ScpClient.createSendCommand(remote, options);
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ // NOTE: we use a mock file system since we expect no invocations for it
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
+ Path mockPath = new MockPath(remote);
+ helper.sendStream(new DefaultScpStreamResolver(name, mockPath, perms, time, size, local, cmd),
+ options.contains(Option.PreserveAttributes), ScpHelper.DEFAULT_SEND_BUFFER_SIZE);
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+
+ @Override
+ protected <T> void runUpload(String remote, Collection<Option> options, Collection<T> local, AbstractScpClient.ScpOperationExecutor<T> executor) throws IOException {
+ local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", remote);
+ if (local.size() > 1) {
+ options = addTargetIsDirectory(options);
+ }
+
+ String cmd = ScpClient.createSendCommand(remote, options);
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try {
+ FactoryManager manager = session.getFactoryManager();
+ FileSystemFactory factory = manager.getFileSystemFactory();
+ FileSystem fs = factory.createFileSystem(session);
+
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
+ executor.execute(helper, local, options);
+ } finally {
+ try {
+ fs.close();
+ } catch (UnsupportedOperationException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("runUpload({}) {} => {} - failed ({}) to close file system={}: {}",
+ session, remote, local, e.getClass().getSimpleName(), fs, e.getMessage());
+ }
+ }
+ }
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+}
+
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java
new file mode 100644
index 0000000..a23e0ba
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java
@@ -0,0 +1,42 @@
+/*
+ * 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.client.scp;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpClientCreator extends AbstractScpClientCreator {
+ public static final DefaultScpClientCreator INSTANCE = new DefaultScpClientCreator();
+
+ public DefaultScpClientCreator() {
+ super();
+ }
+
+ @Override
+ public ScpClient createScpClient(ClientSession session, ScpFileOpener opener, ScpTransferEventListener listener) {
+ return new DefaultScpClient(session, opener, listener);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
new file mode 100644
index 0000000..e6362b8
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
@@ -0,0 +1,88 @@
+/*
+ * 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.client.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+
+import org.apache.sshd.common.scp.ScpSourceStreamResolver;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.Session;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpStreamResolver implements ScpSourceStreamResolver {
+ private final String name;
+ private final Path mockPath;
+ private final Collection<PosixFilePermission> perms;
+ private final ScpTimestamp time;
+ private final long size;
+ private final java.io.InputStream local;
+ private final String cmd;
+
+ public DefaultScpStreamResolver(String name, Path mockPath, Collection<PosixFilePermission> perms, ScpTimestamp time, long size, InputStream local, String cmd) {
+ this.name = name;
+ this.mockPath = mockPath;
+ this.perms = perms;
+ this.time = time;
+ this.size = size;
+ this.local = local;
+ this.cmd = cmd;
+ }
+
+ @Override
+ public String getFileName() throws java.io.IOException {
+ return name;
+ }
+
+ @Override
+ public Path getEventListenerFilePath() {
+ return mockPath;
+ }
+
+ @Override
+ public Collection<PosixFilePermission> getPermissions() throws IOException {
+ return perms;
+ }
+
+ @Override
+ public ScpTimestamp getTimestamp() throws IOException {
+ return time;
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return size;
+ }
+
+ @Override
+ public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
+ return local;
+ }
+
+ @Override
+ public String toString() {
+ return cmd;
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java
new file mode 100644
index 0000000..b2a6091
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java
@@ -0,0 +1,174 @@
+/*
+ * 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.client.scp;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.session.ClientSessionHolder;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.SessionHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpClient extends SessionHolder<ClientSession>, ClientSessionHolder {
+ enum Option {
+ Recursive,
+ PreserveAttributes,
+ TargetIsDirectory
+ }
+
+ /**
+ * Configurable value of the {@link org.apache.sshd.common.FactoryManager}
+ * for controlling the wait timeout for opening a channel for an SCP command
+ * in milliseconds. If not specified, then {@link #DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT}
+ * value is used
+ */
+ String SCP_EXEC_CHANNEL_OPEN_TIMEOUT = "scp-exec-channel-open-timeout";
+ long DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT = TimeUnit.SECONDS.toMillis(30L);
+
+ /**
+ * Configurable value of the {@link org.apache.sshd.common.FactoryManager}
+ * for controlling the wait timeout for waiting on a channel exit status'
+ * for an SCP command in milliseconds. If not specified, then
+ * {@link #DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT}
+ * value is used. If non-positive, then no wait is performed and the command
+ * is assumed to have completed successfully.
+ */
+ String SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT = "scp-exec-channel-exit-status-timeout";
+ long DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
+
+ default void download(String remote, String local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ void download(String remote, String local, Collection<Option> options) throws IOException;
+
+ default void download(String remote, Path local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ void download(String remote, Path local, Collection<Option> options) throws IOException;
+
+ // NOTE: the remote location MUST be a file or an exception is generated
+ void download(String remote, OutputStream local) throws IOException;
+
+ default byte[] downloadBytes(String remote) throws IOException {
+ try (ByteArrayOutputStream local = new ByteArrayOutputStream()) {
+ download(remote, local);
+ return local.toByteArray();
+ }
+ }
+
+ default void download(String[] remote, String local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ default void download(String[] remote, Path local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ void download(String[] remote, String local, Collection<Option> options) throws IOException;
+
+ void download(String[] remote, Path local, Collection<Option> options) throws IOException;
+
+ default void upload(String local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ default void upload(String local, String remote, Collection<Option> options) throws IOException {
+ upload(new String[]{ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local)}, remote, options);
+ }
+
+ default void upload(Path local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ default void upload(Path local, String remote, Collection<Option> options) throws IOException {
+ upload(new Path[]{ValidateUtils.checkNotNull(local, "Invalid local argument: %s", local)}, remote, GenericUtils.of(options));
+ }
+
+ default void upload(String[] local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ void upload(String[] local, String remote, Collection<Option> options) throws IOException;
+
+ default void upload(Path[] local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ void upload(Path[] local, String remote, Collection<Option> options) throws IOException;
+
+ // NOTE: due to SCP command limitations, the amount of data to be uploaded must be known a-priori
+ // To upload a dynamic amount of data use SFTP
+ default void upload(byte[] data, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ upload(data, 0, data.length, remote, perms, time);
+ }
+
+ default void upload(byte[] data, int offset, int len, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ try (InputStream local = new ByteArrayInputStream(data, offset, len)) {
+ upload(local, remote, len, perms, time);
+ }
+ }
+
+ void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException;
+
+ static String createSendCommand(String remote, Collection<Option> options) {
+ StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX);
+ if (options.contains(Option.Recursive)) {
+ sb.append(" -r");
+ }
+ if (options.contains(Option.TargetIsDirectory)) {
+ sb.append(" -d");
+ }
+ if (options.contains(Option.PreserveAttributes)) {
+ sb.append(" -p");
+ }
+
+ sb.append(" -t").append(" --").append(" ").append(remote);
+ return sb.toString();
+ }
+
+ static String createReceiveCommand(String remote, Collection<Option> options) {
+ ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified");
+ StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX);
+ if (options.contains(Option.Recursive)) {
+ sb.append(" -r");
+ }
+ if (options.contains(Option.PreserveAttributes)) {
+ sb.append(" -p");
+ }
+
+ sb.append(" -f").append(" --").append(' ').append(remote);
+ return sb.toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
new file mode 100644
index 0000000..a7a31cb
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
@@ -0,0 +1,106 @@
+/*
+ * 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.client.scp;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpFileOpenerHolder;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpClientCreator extends ScpFileOpenerHolder {
+ static ScpClientCreator instance() {
+ return DefaultScpClientCreator.INSTANCE;
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @return An {@link ScpClient} instance. <B>Note:</B> uses the currently
+ * registered {@link ScpTransferEventListener} and {@link ScpFileOpener} if any
+ * @see #setScpFileOpener(ScpFileOpener)
+ * @see #setScpTransferEventListener(ScpTransferEventListener)
+ */
+ default ScpClient createScpClient(ClientSession session) {
+ return createScpClient(session, getScpFileOpener(), getScpTransferEventListener());
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @param listener A {@link ScpTransferEventListener} that can be used
+ * to receive information about the SCP operations - may be {@code null}
+ * to indicate no more events are required. <B>Note:</B> this listener
+ * is used <U>instead</U> of any listener set via {@link #setScpTransferEventListener(ScpTransferEventListener)}
+ * @return An {@link ScpClient} instance
+ */
+ default ScpClient createScpClient(ClientSession session, ScpTransferEventListener listener) {
+ return createScpClient(session, getScpFileOpener(), listener);
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @param opener The {@link ScpFileOpener} to use to control how local files
+ * are read/written. If {@code null} then a default opener is used.
+ * <B>Note:</B> this opener is used <U>instead</U> of any instance
+ * set via {@link #setScpFileOpener(ScpFileOpener)}
+ * @return An {@link ScpClient} instance
+ */
+ default ScpClient createScpClient(ClientSession session, ScpFileOpener opener) {
+ return createScpClient(session, opener, getScpTransferEventListener());
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @param opener The {@link ScpFileOpener} to use to control how local files
+ * are read/written. If {@code null} then a default opener is used.
+ * <B>Note:</B> this opener is used <U>instead</U> of any instance
+ * set via {@link #setScpFileOpener(ScpFileOpener)}
+ * @param listener A {@link ScpTransferEventListener} that can be used
+ * to receive information about the SCP operations - may be {@code null}
+ * to indicate no more events are required. <B>Note:</B> this listener
+ * is used <U>instead</U> of any listener set via
+ * {@link #setScpTransferEventListener(ScpTransferEventListener)}
+ * @return An {@link ScpClient} instance
+ */
+ ScpClient createScpClient(ClientSession session, ScpFileOpener opener, ScpTransferEventListener listener);
+
+ /**
+ * @return The last {@link ScpTransferEventListener} set via
+ * {@link #setScpTransferEventListener(ScpTransferEventListener)}
+ */
+ ScpTransferEventListener getScpTransferEventListener();
+
+ /**
+ * @param listener A default {@link ScpTransferEventListener} that can be used
+ * to receive information about the SCP operations - may be {@code null}
+ * to indicate no more events are required
+ * @see #createScpClient(ScpTransferEventListener)
+ */
+ void setScpTransferEventListener(ScpTransferEventListener listener);
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java
new file mode 100644
index 0000000..e1a4c72
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java
@@ -0,0 +1,178 @@
+/*
+ * 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.client.scp;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.channels.Channel;
+import java.security.KeyPair;
+import java.util.Objects;
+
+import org.apache.sshd.client.simple.SimpleClientConfigurator;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * A simplified <U>synchronous</U> API for obtaining SCP sessions.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SimpleScpClient extends Channel {
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, String username, String password) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param port The target port
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, int port, String username, String password) throws IOException {
+ return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
+ }
+
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, String username, KeyPair identity) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param port The target port
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, int port, String username, KeyPair identity) throws IOException {
+ return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
+ }
+
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, String username, String password) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param port The target port
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, int port, String username, String password) throws IOException {
+ return scpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, password);
+ }
+
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param port The target port
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException {
+ return scpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, identity);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param target The target {@link SocketAddress}
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ CloseableScpClient scpLogin(SocketAddress target, String username, String password) throws IOException;
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param target The target {@link SocketAddress}
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ CloseableScpClient scpLogin(SocketAddress target, String username, KeyPair identity) throws IOException;
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java
new file mode 100644
index 0000000..c863c6c
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java
@@ -0,0 +1,153 @@
+/*
+ * 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.client.scp;
+
+import java.io.IOException;
+import java.lang.reflect.Proxy;
+import java.net.SocketAddress;
+import java.security.KeyPair;
+import java.util.Objects;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.simple.SimpleClient;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.functors.IOFunction;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SimpleScpClientImpl extends AbstractLoggingBean implements SimpleScpClient {
+ private SimpleClient clientInstance;
+ private ScpClientCreator scpClientCreator;
+
+ public SimpleScpClientImpl() {
+ this(null);
+ }
+
+ public SimpleScpClientImpl(SimpleClient client) {
+ this(client, null);
+ }
+
+ public SimpleScpClientImpl(SimpleClient client, ScpClientCreator scpClientCreator) {
+ this.clientInstance = client;
+ setScpClientCreator(scpClientCreator);
+ }
+
+ public SimpleClient getClient() {
+ return clientInstance;
+ }
+
+ public void setClient(SimpleClient client) {
+ this.clientInstance = client;
+ }
+
+ public ScpClientCreator getScpClientCreator() {
+ return scpClientCreator;
+ }
+
+ public void setScpClientCreator(ScpClientCreator scpClientCreator) {
+ this.scpClientCreator = (scpClientCreator == null) ? ScpClientCreator.instance() : scpClientCreator;
+ }
+
+ @Override
+ public CloseableScpClient scpLogin(SocketAddress target, String username, String password) throws IOException {
+ return createScpClient(client -> client.sessionLogin(target, username, password));
+ }
+
+ @Override
+ public CloseableScpClient scpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
+ return createScpClient(client -> client.sessionLogin(target, username, identity));
+ }
+
+ protected CloseableScpClient createScpClient(IOFunction<? super SimpleClient, ? extends ClientSession> sessionProvider) throws IOException {
+ SimpleClient client = getClient();
+ ClientSession session = sessionProvider.apply(client);
+ try {
+ CloseableScpClient scp = createScpClient(session);
+ session = null; // disable auto-close at finally block
+ return scp;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
+ protected CloseableScpClient createScpClient(ClientSession session) throws IOException {
+ try {
+ ScpClientCreator creator = getScpClientCreator();
+ ScpClient client = creator.createScpClient(Objects.requireNonNull(session, "No client session"));
+ return createScpClient(session, client);
+ } catch (Exception e) {
+ log.warn("createScpClient({}) failed ({}) to create proxy: {}",
+ session, e.getClass().getSimpleName(), e.getMessage());
+ try {
+ session.close();
+ } catch (Exception t) {
+ if (log.isDebugEnabled()) {
+ log.debug("createScpClient({}) failed ({}) to close session: {}",
+ session, t.getClass().getSimpleName(), t.getMessage());
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("createScpClient(" + session + ") session close failure details", t);
+ }
+ e.addSuppressed(t);
+ }
+
+ throw GenericUtils.toIOException(e);
+ }
+ }
+
+ protected CloseableScpClient createScpClient(ClientSession session, ScpClient client) throws IOException {
+ ClassLoader loader = getClass().getClassLoader();
+ Class<?>[] interfaces = {CloseableScpClient.class};
+ return (CloseableScpClient) Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
+ String name = method.getName();
+ try {
+ // The Channel implementation is provided by the session
+ if (("close".equals(name) || "isOpen".equals(name)) && GenericUtils.isEmpty(args)) {
+ return method.invoke(session, args);
+ } else {
+ return method.invoke(client, args);
+ }
+ } catch (Throwable t) {
+ if (log.isTraceEnabled()) {
+ log.trace("invoke(CloseableScpClient#{}) failed ({}) to execute: {}",
+ name, t.getClass().getSimpleName(), t.getMessage());
+ }
+ throw t;
+ }
+ });
+ }
+
+ @Override
+ public boolean isOpen() {
+ return true;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Do nothing
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
new file mode 100644
index 0000000..d929a07
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
@@ -0,0 +1,74 @@
+/*
+ * 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.scp;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * A no-op implementation of {@link ScpTransferEventListener} for those who wish to
+ * implement only a small number of methods. By default, all non-overridden methods
+ * simply log at TRACE level their invocation parameters
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractScpTransferEventListenerAdapter
+ extends AbstractLoggingBean
+ implements ScpTransferEventListener {
+ protected AbstractScpTransferEventListenerAdapter() {
+ super();
+ }
+
+ @Override
+ public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms)
+ throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("startFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")");
+ }
+ }
+
+ @Override
+ public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
+ throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("endFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")"
+ + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+ }
+ }
+
+ @Override
+ public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("startFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")");
+ }
+ }
+
+ @Override
+ public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
+ throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("endFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")"
+ + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java
new file mode 100644
index 0000000..9ae17c7
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.scp;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpException extends IOException {
+ private static final long serialVersionUID = 7734851624372451732L;
+ private final Integer exitStatus;
+
+ public ScpException(String message) {
+ this(message, null);
+ }
+
+ public ScpException(Integer exitStatus) {
+ this("Exit status=" + ScpHelper.getExitStatusName(Objects.requireNonNull(exitStatus, "No exit status")), exitStatus);
+ }
+
+ public ScpException(String message, Integer exitStatus) {
+ this(message, null, exitStatus);
+ }
+
+ public ScpException(Throwable cause, Integer exitStatus) {
+ this(Objects.requireNonNull(cause, "No cause").getMessage(), cause, exitStatus);
+ }
+
+ public ScpException(String message, Throwable cause, Integer exitStatus) {
+ super(message, cause);
+ this.exitStatus = exitStatus;
+ }
+
+ public Integer getExitStatus() {
+ return exitStatus;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
new file mode 100644
index 0000000..78e033f
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
@@ -0,0 +1,284 @@
+/*
+ * 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.scp;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SelectorUtils;
+import org.apache.sshd.common.util.io.DirectoryScanner;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Plug-in mechanism for users to intervene in the SCP process - e.g.,
+ * apply some kind of traffic shaping mechanism, display upload/download
+ * progress, etc...
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpFileOpener {
+ /**
+ * Invoked when receiving a new file to via a directory command
+ *
+ * @param localPath The target local path
+ * @param name The target file name
+ * @param preserve Whether requested to preserve the permissions and timestamp
+ * @param permissions The requested file permissions
+ * @param time The requested {@link ScpTimestamp} - may be {@code null} if nothing to update
+ * @return The actual target file path for the incoming file/directory
+ * @throws IOException If failed to resolve the file path
+ * @see #updateFileProperties(Path, Set, ScpTimestamp) updateFileProperties
+ */
+ default Path resolveIncomingFilePath(
+ Path localPath, String name, boolean preserve, Set<PosixFilePermission> permissions, ScpTimestamp time)
+ throws IOException {
+ LinkOption[] options = IoUtils.getLinkOptions(true);
+ Boolean status = IoUtils.checkFileExists(localPath, options);
+ if (status == null) {
+ throw new AccessDeniedException("Receive directory existence status cannot be determined: " + localPath);
+ }
+
+ Path file = null;
+ if (status && Files.isDirectory(localPath, options)) {
+ String localName = name.replace('/', File.separatorChar);
+ file = localPath.resolve(localName);
+ } else if (!status) {
+ Path parent = localPath.getParent();
+
+ status = IoUtils.checkFileExists(parent, options);
+ if (status == null) {
+ throw new AccessDeniedException("Receive directory parent (" + parent + ") existence status cannot be determined for " + localPath);
+ }
+
+ if (status && Files.isDirectory(parent, options)) {
+ file = localPath;
+ }
+ }
+
+ if (file == null) {
+ throw new IOException("Cannot write to " + localPath);
+ }
+
+ status = IoUtils.checkFileExists(file, options);
+ if (status == null) {
+ throw new AccessDeniedException("Receive directory file existence status cannot be determined: " + file);
+ }
+
+ if (!(status && Files.isDirectory(file, options))) {
+ Files.createDirectory(file);
+ }
+
+ if (preserve) {
+ updateFileProperties(file, permissions, time);
+ }
+
+ return file;
+ }
+
+ /**
+ * Invoked when required to send a pattern of files
+ *
+ * @param basedir The base directory - may be {@code null}/empty to indicate CWD
+ * @param pattern The required pattern
+ * @return The matching <U>relative paths</U> of the children to send
+ */
+ default Iterable<String> getMatchingFilesToSend(String basedir, String pattern) {
+ String[] matches = new DirectoryScanner(basedir, pattern).scan();
+ if (GenericUtils.isEmpty(matches)) {
+ return Collections.emptyList();
+ }
+
+ return Arrays.asList(matches);
+ }
+
+ /**
+ * Invoked on a local path in order to decide whether it should be sent
+ * as a file or as a directory
+ *
+ * @param path The local {@link Path}
+ * @param options The {@link LinkOption}-s
+ * @return Whether to send the file as a regular one - <B>Note:</B> if {@code false}
+ * then the {@link #sendAsDirectory(Path, LinkOption...)} is consulted.
+ * @throws IOException If failed to decide
+ */
+ default boolean sendAsRegularFile(Path path, LinkOption... options) throws IOException {
+ return Files.isRegularFile(path, options);
+ }
+
+ /**
+ * Invoked on a local path in order to decide whether it should be sent
+ * as a file or as a directory
+ *
+ * @param path The local {@link Path}
+ * @param options The {@link LinkOption}-s
+ * @return Whether to send the file as a directory - <B>Note:</B> if {@code true}
+ * then {@link #getLocalFolderChildren(Path)} is consulted
+ * @throws IOException If failed to decide
+ */
+ default boolean sendAsDirectory(Path path, LinkOption... options) throws IOException {
+ return Files.isDirectory(path, options);
+ }
+
+ /**
+ * Invoked when required to send all children of a local directory
+ *
+ * @param path The local folder {@link Path}{
+ * @return The {@link DirectoryStream} of children to send - <B>Note:</B> for each child
+ * the decision whether to send it as a file or a directory will be reached by consulting
+ * the respective {@link #sendAsRegularFile(Path, LinkOption...) sendAsRegularFile} and
+ * {@link #sendAsDirectory(Path, LinkOption...) sendAsDirectory} methods
+ * @throws IOException If failed to provide the children stream
+ * @see #sendAsDirectory(Path, LinkOption...) sendAsDirectory
+ */
+ default DirectoryStream<Path> getLocalFolderChildren(Path path) throws IOException {
+ return Files.newDirectoryStream(path);
+ }
+
+ default BasicFileAttributes getLocalBasicFileAttributes(Path path, LinkOption... options) throws IOException {
+ return Files.getFileAttributeView(path, BasicFileAttributeView.class, options).readAttributes();
+ }
+
+ default Set<PosixFilePermission> getLocalFilePermissions(Path path, LinkOption... options) throws IOException {
+ return IoUtils.getPermissions(path, options);
+ }
+
+ /**
+ * @param fileSystem The <U>local</U> {@link FileSystem} on which local file should reside
+ * @param commandPath The command path using the <U>local</U> file separator
+ * @return The resolved absolute and normalized local {@link Path}
+ * @throws IOException If failed to resolve the path
+ * @throws InvalidPathException If invalid local path value
+ */
+ default Path resolveLocalPath(FileSystem fileSystem, String commandPath) throws IOException, InvalidPathException {
+ String path = SelectorUtils.translateToLocalFileSystemPath(commandPath, File.separatorChar, fileSystem);
+ Path lcl = fileSystem.getPath(path);
+ Path abs = lcl.isAbsolute() ? lcl : lcl.toAbsolutePath();
+ return abs.normalize();
+ }
+
+ /**
+ * Invoked when a request to receive something is processed
+ *
+ * @param path The local target {@link Path} of the request
+ * @param recursive Whether the request is recursive
+ * @param shouldBeDir Whether target path is expected to be a directory
+ * @param preserve Whether target path is expected to preserve attributes (permissions, times)
+ * @return The effective target path - default=same as input
+ * @throws IOException If failed to resolve target location
+ */
+ default Path resolveIncomingReceiveLocation(
+ Path path, boolean recursive, boolean shouldBeDir, boolean preserve)
+ throws IOException {
+ if (!shouldBeDir) {
+ return path;
+ }
+ LinkOption[] options = IoUtils.getLinkOptions(true);
+ Boolean status = IoUtils.checkFileExists(path, options);
+ if (status == null) {
+ throw new SshException("Target directory " + path + " is most like inaccessible");
+ }
+ if (!status) {
+ throw new SshException("Target directory " + path + " does not exist");
+ }
+ if (!Files.isDirectory(path, options)) {
+ throw new SshException("Target directory " + path + " is not a directory");
+ }
+
+ return path;
+ }
+
+ /**
+ * Called when there is a candidate file/folder for sending
+ *
+ * @param localPath The original file/folder {@link Path} for sending
+ * @param options The {@link LinkOption}-s to use for validation
+ * @return The effective outgoing file path (default=same as input)
+ * @throws IOException If failed to resolve
+ */
+ default Path resolveOutgoingFilePath(Path localPath, LinkOption... options) throws IOException {
+ Boolean status = IoUtils.checkFileExists(localPath, options);
+ if (status == null) {
+ throw new AccessDeniedException("Send file existence status cannot be determined: " + localPath);
+ }
+ if (!status) {
+ throw new IOException(localPath + ": no such file or directory");
+ }
+
+ return localPath;
+ }
+
+ /**
+ * Create an input stream to read from a file
+ *
+ * @param session The {@link Session} requesting the access
+ * @param file The requested local file {@link Path}
+ * @param options The {@link OpenOption}s - may be {@code null}/empty
+ * @return The open {@link InputStream} never {@code null}
+ * @throws IOException If failed to open the file
+ */
+ InputStream openRead(Session session, Path file, OpenOption... options) throws IOException;
+
+ ScpSourceStreamResolver createScpSourceStreamResolver(Path path) throws IOException;
+
+ /**
+ * Create an output stream to write to a file
+ *
+ * @param session The {@link Session} requesting the access
+ * @param file The requested local file {@link Path}
+ * @param options The {@link OpenOption}s - may be {@code null}/empty
+ * @return The open {@link OutputStream} never {@code null}
+ * @throws IOException If failed to open the file
+ */
+ OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException;
+
+ ScpTargetStreamResolver createScpTargetStreamResolver(Path path) throws IOException;
+
+ static void updateFileProperties(Path file, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ IoUtils.setPermissions(file, perms);
+
+ if (time != null) {
+ BasicFileAttributeView view = Files.getFileAttributeView(file, BasicFileAttributeView.class);
+ FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS);
+ FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS);
+ view.setTimes(lastModified, lastAccess, null);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
new file mode 100644
index 0000000..b492129
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
@@ -0,0 +1,37 @@
+/*
+ * 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.scp;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpFileOpenerHolder {
+ /**
+ * @return The last {@link ScpFileOpener} set via call
+ * to {@link #setScpFileOpener(ScpFileOpener)}
+ */
+ ScpFileOpener getScpFileOpener();
+
+ /**
+ * @param opener The default {@link ScpFileOpener} to use - if {@code null}
+ * then a default opener is used
+ */
+ void setScpFileOpener(ScpFileOpener opener);
+}