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);
+}