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:22 UTC

[1/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Repository: mina-sshd
Updated Branches:
  refs/heads/master cb8982abb -> af415e5fe


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java b/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java
deleted file mode 100644
index 3b651b3..0000000
--- a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.simple;
-
-import java.io.IOException;
-import java.lang.reflect.Proxy;
-import java.net.SocketAddress;
-import java.security.KeyPair;
-
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-public class SimpleSftpClientImpl extends AbstractLoggingBean implements SimpleSftpClient {
-
-    private SimpleClient client;
-    private SftpClientFactory sftpClientFactory;
-
-    public SimpleSftpClientImpl(SimpleClient client) {
-        this(client, null);
-    }
-
-    public SimpleSftpClientImpl(SimpleClient client, SftpClientFactory sftpClientFactory) {
-        this.client = client;
-        setSftpClientFactory(sftpClientFactory);
-    }
-
-    public SimpleClient getClient() {
-        return client;
-    }
-
-    public void setClient(SimpleClient client) {
-        this.client = client;
-    }
-
-    public SftpClientFactory getSftpClientFactory() {
-        return sftpClientFactory;
-    }
-
-    public void setSftpClientFactory(SftpClientFactory sftpClientFactory) {
-        this.sftpClientFactory = (sftpClientFactory != null) ? sftpClientFactory : SftpClientFactory.instance();
-    }
-
-    @Override
-    public SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException {
-        return createSftpClient(client.sessionLogin(target, username, password));
-    }
-
-    @Override
-    public SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
-        return createSftpClient(client.sessionLogin(target, username, identity));
-    }
-
-    protected SftpClient createSftpClient(final ClientSession session) throws IOException {
-        Exception err = null;
-        try {
-            SftpClient client = sftpClientFactory.createSftpClient(session);
-            try {
-                return createSftpClient(session, client);
-            } catch (Exception e) {
-                err = GenericUtils.accumulateException(err, e);
-                try {
-                    client.close();
-                } catch (Exception t) {
-                    if (log.isDebugEnabled()) {
-                        log.debug("createSftpClient({}) failed ({}) to close client: {}",
-                                session, t.getClass().getSimpleName(), t.getMessage());
-                    }
-
-                    if (log.isTraceEnabled()) {
-                        log.trace("createSftpClient(" + session + ") client close failure details", t);
-                    }
-                    err = GenericUtils.accumulateException(err, t);
-                }
-            }
-        } catch (Exception e) {
-            err = GenericUtils.accumulateException(err, e);
-        }
-
-        // This point is reached if error occurred
-        log.warn("createSftpClient({}) failed ({}) to create session: {}",
-                session, err.getClass().getSimpleName(), err.getMessage());
-
-        try {
-            session.close();
-        } catch (Exception e) {
-            if (log.isDebugEnabled()) {
-                log.debug("createSftpClient({}) failed ({}) to close session: {}",
-                        session, e.getClass().getSimpleName(), e.getMessage());
-            }
-
-            if (log.isTraceEnabled()) {
-                log.trace("createSftpClient(" + session + ") session close failure details", e);
-            }
-            err = GenericUtils.accumulateException(err, e);
-        }
-
-        if (err instanceof IOException) {
-            throw (IOException) err;
-        } else {
-            throw new IOException(err);
-        }
-    }
-
-    protected SftpClient createSftpClient(final ClientSession session, final SftpClient client) throws IOException {
-        ClassLoader loader = getClass().getClassLoader();
-        Class<?>[] interfaces = {SftpClient.class};
-        return (SftpClient) Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
-            Throwable err = null;
-            Object result = null;
-            String name = method.getName();
-            try {
-                result = method.invoke(client, args);
-            } catch (Throwable t) {
-                if (log.isTraceEnabled()) {
-                    log.trace("invoke(SftpClient#{}) failed ({}) to execute: {}",
-                            name, t.getClass().getSimpleName(), t.getMessage());
-                }
-                err = GenericUtils.accumulateException(err, t);
-            }
-
-            // propagate the "close" call to the session as well
-            if ("close".equals(name) && GenericUtils.isEmpty(args)) {
-                try {
-                    session.close();
-                } catch (Throwable t) {
-                    if (log.isDebugEnabled()) {
-                        log.debug("invoke(ClientSession#{}) failed ({}) to execute: {}",
-                                name, t.getClass().getSimpleName(), t.getMessage());
-                    }
-                    err = GenericUtils.accumulateException(err, t);
-                }
-            }
-
-            if (err != null) {
-                throw err;
-            }
-
-            return result;
-        });
-    }
-
-    @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-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java
new file mode 100644
index 0000000..c1dd6d6
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java
@@ -0,0 +1,179 @@
+/*
+ * 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.subsystem.sftp;
+
+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 SFTP sessions.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SimpleSftpClient extends Channel {
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(String host, String username, String password) throws IOException {
+        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+    }
+
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(String host, int port, String username, String password) throws IOException {
+        return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
+    }
+
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(String host, String username, KeyPair identity) throws IOException {
+        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+    }
+
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(String host, int port, String username, KeyPair identity) throws IOException {
+        return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
+    }
+
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(InetAddress host, String username, String password) throws IOException {
+        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+    }
+
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(InetAddress host, int port, String username, String password) throws IOException {
+        return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, password);
+    }
+
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
+        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+    }
+
+    /**
+     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    default SftpClient sftpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException {
+        return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, identity);
+    }
+
+    /**
+     * Creates an SFTP session using the provided credentials
+     *
+     * @param target The target {@link SocketAddress}
+     * @param username Username
+     * @param password Password
+     * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException;
+
+    /**
+     * Creates an SFTP session using the provided credentials
+     *
+     * @param target The target {@link SocketAddress}
+     * @param username Username
+     * @param identity The {@link KeyPair} identity
+     * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+     * underlying session
+     * @throws IOException If failed to login or authenticate
+     */
+    SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java
new file mode 100644
index 0000000..30b4a8b
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java
@@ -0,0 +1,192 @@
+/*
+ * 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.subsystem.sftp.impl;
+
+import java.io.IOException;
+import java.lang.reflect.Proxy;
+import java.net.SocketAddress;
+import java.security.KeyPair;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.simple.SimpleClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
+import org.apache.sshd.client.subsystem.sftp.SimpleSftpClient;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.functors.IOFunction;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+public class SimpleSftpClientImpl extends AbstractLoggingBean implements SimpleSftpClient {
+
+    private SimpleClient clientInstance;
+    private SftpClientFactory sftpClientFactory;
+
+    public SimpleSftpClientImpl() {
+        this(null);
+    }
+
+    public SimpleSftpClientImpl(SimpleClient client) {
+        this(client, null);
+    }
+
+    public SimpleSftpClientImpl(SimpleClient client, SftpClientFactory sftpClientFactory) {
+        this.clientInstance = client;
+        setSftpClientFactory(sftpClientFactory);
+    }
+
+    public SimpleClient getClient() {
+        return clientInstance;
+    }
+
+    public void setClient(SimpleClient client) {
+        this.clientInstance = client;
+    }
+
+    public SftpClientFactory getSftpClientFactory() {
+        return sftpClientFactory;
+    }
+
+    public void setSftpClientFactory(SftpClientFactory sftpClientFactory) {
+        this.sftpClientFactory = (sftpClientFactory != null) ? sftpClientFactory : SftpClientFactory.instance();
+    }
+
+    @Override
+    public SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException {
+        return createSftpClient(client -> client.sessionLogin(target, username, password));
+    }
+
+    @Override
+    public SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
+        return createSftpClient(client -> client.sessionLogin(target, username, identity));
+    }
+
+    protected SftpClient createSftpClient(IOFunction<? super SimpleClient, ? extends ClientSession> sessionProvider) throws IOException {
+        SimpleClient client = getClient();
+        ClientSession session = sessionProvider.apply(client);
+        try {
+            SftpClient sftp = createSftpClient(session);
+            session = null; // disable auto-close at finally block
+            return sftp;
+        } finally {
+            if (session != null) {
+                session.close();
+            }
+        }
+    }
+
+    protected SftpClient createSftpClient(ClientSession session) throws IOException {
+        Exception err = null;
+        try {
+            SftpClient client = sftpClientFactory.createSftpClient(session);
+            try {
+                return createSftpClient(session, client);
+            } catch (Exception e) {
+                err = GenericUtils.accumulateException(err, e);
+                try {
+                    client.close();
+                } catch (Exception t) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("createSftpClient({}) failed ({}) to close client: {}",
+                                session, t.getClass().getSimpleName(), t.getMessage());
+                    }
+
+                    if (log.isTraceEnabled()) {
+                        log.trace("createSftpClient(" + session + ") client close failure details", t);
+                    }
+                    err = GenericUtils.accumulateException(err, t);
+                }
+            }
+        } catch (Exception e) {
+            err = GenericUtils.accumulateException(err, e);
+        }
+
+        // This point is reached if error occurred
+        log.warn("createSftpClient({}) failed ({}) to create session: {}",
+                session, err.getClass().getSimpleName(), err.getMessage());
+
+        try {
+            session.close();
+        } catch (Exception e) {
+            if (log.isDebugEnabled()) {
+                log.debug("createSftpClient({}) failed ({}) to close session: {}",
+                        session, e.getClass().getSimpleName(), e.getMessage());
+            }
+
+            if (log.isTraceEnabled()) {
+                log.trace("createSftpClient(" + session + ") session close failure details", e);
+            }
+            err = GenericUtils.accumulateException(err, e);
+        }
+
+        if (err instanceof IOException) {
+            throw (IOException) err;
+        } else {
+            throw new IOException(err);
+        }
+    }
+
+    protected SftpClient createSftpClient(ClientSession session, SftpClient client) throws IOException {
+        ClassLoader loader = getClass().getClassLoader();
+        Class<?>[] interfaces = {SftpClient.class};
+        return (SftpClient) Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
+            Throwable err = null;
+            Object result = null;
+            String name = method.getName();
+            try {
+                result = method.invoke(client, args);
+            } catch (Throwable t) {
+                if (log.isTraceEnabled()) {
+                    log.trace("invoke(SftpClient#{}) failed ({}) to execute: {}",
+                            name, t.getClass().getSimpleName(), t.getMessage());
+                }
+                err = GenericUtils.accumulateException(err, t);
+            }
+
+            // propagate the "close" call to the session as well
+            if ("close".equals(name) && GenericUtils.isEmpty(args)) {
+                try {
+                    session.close();
+                } catch (Throwable t) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("invoke(ClientSession#{}) failed ({}) to execute: {}",
+                                name, t.getClass().getSimpleName(), t.getMessage());
+                    }
+                    err = GenericUtils.accumulateException(err, t);
+                }
+            }
+
+            if (err != null) {
+                throw err;
+            }
+
+            return result;
+        });
+    }
+
+    @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-sftp/src/test/java/org/apache/sshd/client/ClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/ClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/ClientTest.java
deleted file mode 100644
index 594c756..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/ClientTest.java
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * 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;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.apache.sshd.client.channel.ChannelExec;
-import org.apache.sshd.client.channel.ChannelShell;
-import org.apache.sshd.client.channel.ChannelSubsystem;
-import org.apache.sshd.client.channel.ClientChannel;
-import org.apache.sshd.client.future.OpenFuture;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.subsystem.SubsystemClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
-import org.apache.sshd.common.Factory;
-import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.RuntimeSshException;
-import org.apache.sshd.common.Service;
-import org.apache.sshd.common.channel.Channel;
-import org.apache.sshd.common.channel.ChannelListener;
-import org.apache.sshd.common.channel.ChannelListenerManager;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionListener;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.channel.ChannelSession;
-import org.apache.sshd.server.channel.ChannelSessionFactory;
-import org.apache.sshd.server.forward.DirectTcpipFactory;
-import org.apache.sshd.server.session.ServerConnectionServiceFactory;
-import org.apache.sshd.server.session.ServerUserAuthService;
-import org.apache.sshd.server.session.ServerUserAuthServiceFactory;
-import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.apache.sshd.util.test.EchoShell;
-import org.apache.sshd.util.test.EchoShellFactory;
-import org.apache.sshd.util.test.TestChannelListener;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * TODO Add javadoc
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class ClientTest extends BaseTestSupport {
-    private SshServer sshd;
-    private SshClient client;
-    private int port;
-    private CountDownLatch authLatch;
-    private CountDownLatch channelLatch;
-
-    private final AtomicReference<ClientSession> clientSessionHolder = new AtomicReference<>(null);
-    @SuppressWarnings("synthetic-access")
-    private final SessionListener clientSessionListener = new SessionListener() {
-        @Override
-        public void sessionCreated(Session session) {
-            assertObjectInstanceOf("Non client session creation notification", ClientSession.class, session);
-            assertNull("Multiple creation notifications", clientSessionHolder.getAndSet((ClientSession) session));
-        }
-
-        @Override
-        public void sessionEvent(Session session, Event event) {
-            assertObjectInstanceOf("Non client session event notification: " + event, ClientSession.class, session);
-            assertSame("Mismatched client session event instance: " + event, clientSessionHolder.get(), session);
-        }
-
-        @Override
-        public void sessionException(Session session, Throwable t) {
-            assertObjectInstanceOf("Non client session exception notification", ClientSession.class, session);
-            assertNotNull("No session exception data", t);
-        }
-
-        @Override
-        public void sessionClosed(Session session) {
-            assertObjectInstanceOf("Non client session closure notification", ClientSession.class, session);
-            assertSame("Mismatched client session closure instance", clientSessionHolder.getAndSet(null), session);
-        }
-    };
-
-    public ClientTest() {
-        super();
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        authLatch = new CountDownLatch(0);
-        channelLatch = new CountDownLatch(0);
-
-        sshd = setupTestServer();
-        sshd.setShellFactory(new TestEchoShellFactory());
-        sshd.setServiceFactories(Arrays.asList(
-                new ServerUserAuthServiceFactory() {
-                    @Override
-                    public Service create(Session session) throws IOException {
-                        return new ServerUserAuthService(session) {
-                            @SuppressWarnings("synthetic-access")
-                            @Override
-                            public void process(int cmd, Buffer buffer) throws Exception {
-                                authLatch.await();
-                                super.process(cmd, buffer);
-                            }
-                        };
-                    }
-                },
-                ServerConnectionServiceFactory.INSTANCE
-        ));
-        sshd.setChannelFactories(Arrays.asList(
-                new ChannelSessionFactory() {
-                    @Override
-                    public Channel create() {
-                        return new ChannelSession() {
-                            @SuppressWarnings("synthetic-access")
-                            @Override
-                            public OpenFuture open(int recipient, long rwsize, long rmpsize, Buffer buffer) {
-                                try {
-                                    channelLatch.await();
-                                } catch (InterruptedException e) {
-                                    throw new RuntimeSshException(e);
-                                }
-                                return super.open(recipient, rwsize, rmpsize, buffer);
-                            }
-
-                            @Override
-                            public String toString() {
-                                return "ChannelSession" + "[id=" + getId() + ", recipient=" + getRecipient() + "]";
-                            }
-                        };
-                    }
-                },
-                DirectTcpipFactory.INSTANCE));
-        sshd.start();
-        port = sshd.getPort();
-
-        client = setupTestClient();
-        clientSessionHolder.set(null);  // just making sure
-        client.addSessionListener(clientSessionListener);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (sshd != null) {
-            sshd.stop(true);
-        }
-        if (client != null) {
-            client.stop();
-        }
-        clientSessionHolder.set(null);  // just making sure
-    }
-
-    @Test
-    public void testSimpleClientListener() throws Exception {
-        AtomicReference<Channel> channelHolder = new AtomicReference<>(null);
-        client.addChannelListener(new ChannelListener() {
-            @Override
-            public void channelOpenSuccess(Channel channel) {
-                assertSame("Mismatched opened channel instances", channel, channelHolder.get());
-            }
-
-            @Override
-            public void channelOpenFailure(Channel channel, Throwable reason) {
-                assertSame("Mismatched failed open channel instances", channel, channelHolder.get());
-            }
-
-            @Override
-            public void channelInitialized(Channel channel) {
-                assertNull("Multiple channel initialization notifications", channelHolder.getAndSet(channel));
-            }
-
-            @Override
-            public void channelStateChanged(Channel channel, String hint) {
-                outputDebugMessage("channelStateChanged(%s): %s", channel, hint);
-            }
-
-            @Override
-            public void channelClosed(Channel channel, Throwable reason) {
-                assertSame("Mismatched closed channel instances", channel, channelHolder.getAndSet(null));
-            }
-        });
-        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-
-        client.start();
-
-        try (ClientSession session = createTestClientSession()) {
-            testClientListener(channelHolder, ChannelShell.class, () -> {
-                try {
-                    return session.createShellChannel();
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-            });
-            testClientListener(channelHolder, ChannelExec.class, () -> {
-                try {
-                    return session.createExecChannel(getCurrentTestName());
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-            });
-            testClientListener(channelHolder, SftpClient.class, () -> {
-                try {
-                    return SftpClientFactory.instance().createSftpClient(session);
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-            });
-        } finally {
-            client.stop();
-        }
-    }
-
-    private <C extends Closeable> void testClientListener(AtomicReference<Channel> channelHolder, Class<C> channelType, Factory<? extends C> factory) throws Exception {
-        assertNull(channelType.getSimpleName() + ": Unexpected currently active channel", channelHolder.get());
-
-        try (C instance = factory.create()) {
-            Channel expectedChannel;
-            if (instance instanceof Channel) {
-                expectedChannel = (Channel) instance;
-            } else if (instance instanceof SubsystemClient) {
-                expectedChannel = ((SubsystemClient) instance).getClientChannel();
-            } else {
-                throw new UnsupportedOperationException("Unknown test instance type" + instance.getClass().getSimpleName());
-            }
-
-            Channel actualChannel = channelHolder.get();
-            assertSame("Mismatched listener " + channelType.getSimpleName() + " instances", expectedChannel, actualChannel);
-        }
-
-        assertNull(channelType.getSimpleName() + ": Active channel closure not signalled", channelHolder.get());
-    }
-
-    @Test
-    public void testCreateChannelByType() throws Exception {
-        client.start();
-
-        Collection<ClientChannel> channels = new LinkedList<>();
-        try (ClientSession session = createTestClientSession()) {
-            // required since we do not use an SFTP subsystem
-            PropertyResolverUtils.updateProperty(session, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
-            channels.add(session.createChannel(Channel.CHANNEL_SUBSYSTEM, SftpConstants.SFTP_SUBSYSTEM_NAME));
-            channels.add(session.createChannel(Channel.CHANNEL_EXEC, getCurrentTestName()));
-            channels.add(session.createChannel(Channel.CHANNEL_SHELL, getClass().getSimpleName()));
-
-            Set<Integer> ids = new HashSet<>(channels.size());
-            for (ClientChannel c : channels) {
-                int id = c.getId();
-                assertTrue("Channel ID repeated: " + id, ids.add(id));
-            }
-        } finally {
-            for (Closeable c : channels) {
-                try {
-                    c.close();
-                } catch (IOException e) {
-                    // ignored
-                }
-            }
-            client.stop();
-        }
-
-        assertNull("Session closure not signalled", clientSessionHolder.get());
-    }
-
-    /**
-     * Makes sure that the {@link ChannelListener}s added to the client, session
-     * and channel are <U>cumulative</U> - i.e., all of them invoked
-     * @throws Exception If failed
-     */
-    @Test
-    public void testChannelListenersPropagation() throws Exception {
-        Map<String, TestChannelListener> clientListeners = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        addChannelListener(clientListeners, client, new TestChannelListener(client.getClass().getSimpleName()));
-
-        // required since we do not use an SFTP subsystem
-        PropertyResolverUtils.updateProperty(client, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
-        client.start();
-        try (ClientSession session = createTestClientSession()) {
-            addChannelListener(clientListeners, session, new TestChannelListener(session.getClass().getSimpleName()));
-            assertListenerSizes("ClientSessionOpen", clientListeners, 0, 0);
-
-            try (ClientChannel channel = session.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME)) {
-                channel.open().verify(5L, TimeUnit.SECONDS);
-
-                TestChannelListener channelListener = new TestChannelListener(channel.getClass().getSimpleName());
-                // need to emulate them since we are adding the listener AFTER the channel is open
-                channelListener.channelInitialized(channel);
-                channelListener.channelOpenSuccess(channel);
-                channel.addChannelListener(channelListener);
-                assertListenerSizes("ClientChannelOpen", clientListeners, 1, 1);
-            }
-
-            assertListenerSizes("ClientChannelClose", clientListeners, 0, 1);
-        } finally {
-            client.stop();
-        }
-
-        assertListenerSizes("ClientStop", clientListeners, 0, 1);
-    }
-
-    private static void assertListenerSizes(String phase, Map<String, ? extends TestChannelListener> listeners, int activeSize, int openSize) {
-        assertListenerSizes(phase, listeners.values(), activeSize, openSize);
-    }
-
-    private static void assertListenerSizes(String phase, Collection<? extends TestChannelListener> listeners, int activeSize, int openSize) {
-        if (GenericUtils.isEmpty(listeners)) {
-            return;
-        }
-
-        for (TestChannelListener l : listeners) {
-            if (activeSize >= 0) {
-                assertEquals(phase + ": mismatched active channels size for " + l.getName() + " listener", activeSize, GenericUtils.size(l.getActiveChannels()));
-            }
-
-            if (openSize >= 0) {
-                assertEquals(phase + ": mismatched open channels size for " + l.getName() + " listener", openSize, GenericUtils.size(l.getOpenChannels()));
-            }
-
-            assertEquals(phase + ": unexpected failed channels size for " + l.getName() + " listener", 0, GenericUtils.size(l.getFailedChannels()));
-        }
-    }
-
-    private static <L extends ChannelListener & NamedResource> void addChannelListener(Map<String, L> listeners, ChannelListenerManager manager, L listener) {
-        String name = listener.getName();
-        assertNull("Duplicate listener named " + name, listeners.put(name, listener));
-        manager.addChannelListener(listener);
-    }
-
-    private ClientSession createTestClientSession() throws IOException {
-        ClientSession session = createTestClientSession(TEST_LOCALHOST);
-        try {
-            InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
-            assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
-
-            ClientSession returnValue = session;
-            session = null; // avoid 'finally' close
-            return returnValue;
-        } finally {
-            if (session != null) {
-                session.close();
-            }
-        }
-    }
-
-    private ClientSession createTestClientSession(String host) throws IOException {
-        ClientSession session = client.connect(getCurrentTestName(), host, port).verify(7L, TimeUnit.SECONDS).getSession();
-        try {
-            assertNotNull("Client session creation not signalled", clientSessionHolder.get());
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(5L, TimeUnit.SECONDS);
-
-            InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
-            assertNotNull("No reported connect address", addr);
-            assertEquals("Mismatched connect port", port, addr.getPort());
-
-            ClientSession returnValue = session;
-            session = null; // avoid 'finally' close
-            return returnValue;
-        } finally {
-            if (session != null) {
-                session.close();
-            }
-        }
-    }
-
-    public static class TestEchoShellFactory extends EchoShellFactory {
-        @Override
-        public Command create() {
-            return new TestEchoShell();
-        }
-    }
-
-    public static class TestEchoShell extends EchoShell {
-        // CHECKSTYLE:OFF
-        public static CountDownLatch latch;
-        // CHECKSTYLE:ON
-
-        public TestEchoShell() {
-            super();
-        }
-
-        @Override
-        public void destroy() {
-            if (latch != null) {
-                latch.countDown();
-            }
-            super.destroy();
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java b/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
deleted file mode 100644
index 60b9403..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.simple;
-
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.junit.After;
-import org.junit.Before;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class BaseSimpleClientTestSupport extends BaseTestSupport {
-    public static final long CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
-    public static final long AUTH_TIMEOUT = TimeUnit.SECONDS.toMillis(7L);
-
-    protected SshServer sshd;
-    protected SshClient client;
-    protected int port;
-    protected SimpleClient simple;
-
-    protected BaseSimpleClientTestSupport() {
-        super();
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        sshd = setupTestServer();
-        sshd.start();
-        port = sshd.getPort();
-        client = setupTestClient();
-
-        simple = SshClient.wrapAsSimpleClient(client);
-        simple.setConnectTimeout(CONNECT_TIMEOUT);
-        simple.setAuthenticationTimeout(AUTH_TIMEOUT);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (sshd != null) {
-            sshd.stop(true);
-        }
-        if (simple != null) {
-            simple.close();
-        }
-        if (client != null) {
-            client.stop();
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java
deleted file mode 100644
index d48a19d..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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.simple;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collections;
-import java.util.EnumSet;
-
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
-import org.apache.sshd.util.test.Utils;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class SimpleSftpClientTest extends BaseSimpleClientTestSupport {
-    private final Path targetPath;
-    private final Path parentPath;
-    private final FileSystemFactory fileSystemFactory;
-    private SimpleSftpClient sftpClient;
-
-    public SimpleSftpClientTest() throws Exception {
-        targetPath = detectTargetFolder();
-        parentPath = targetPath.getParent();
-        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
-    }
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-        sshd.setCommandFactory(new ScpCommandFactory());
-        sshd.setFileSystemFactory(fileSystemFactory);
-        client.start();
-        sftpClient = new SimpleSftpClientImpl(simple);
-    }
-
-    @Test
-    public void testSessionClosedWhenClientClosed() throws Exception {
-        try (SftpClient sftp = login()) {
-            assertTrue("SFTP not open", sftp.isOpen());
-
-            Session session = sftp.getClientSession();
-            assertTrue("Session not open", session.isOpen());
-
-            sftp.close();
-            assertFalse("Session not closed", session.isOpen());
-            assertFalse("SFTP not closed", sftp.isOpen());
-        }
-    }
-
-    @Test
-    public void testSftpProxyCalls() throws Exception {
-        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
-        Utils.deleteRecursive(lclSftp);
-        Path clientFolder = assertHierarchyTargetFolderExists(lclSftp).resolve("client");
-        Path clientFile = clientFolder.resolve("file.txt");
-        String remoteFileDir = Utils.resolveRelativeRemotePath(parentPath, clientFolder);
-        String clientFileName = clientFile.getFileName().toString();
-        String remoteFilePath = remoteFileDir + "/" + clientFileName;
-
-        try (SftpClient sftp = login()) {
-            sftp.mkdir(remoteFileDir);
-
-            byte[] written = (getClass().getSimpleName() + "#" + getCurrentTestName() + IoUtils.EOL).getBytes(StandardCharsets.UTF_8);
-            try (SftpClient.CloseableHandle h = sftp.open(remoteFilePath, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) {
-                sftp.write(h, 0L, written);
-
-                SftpClient.Attributes attrs = sftp.stat(h);
-                assertNotNull("No handle attributes", attrs);
-                assertEquals("Mismatched remote file size", written.length, attrs.getSize());
-            }
-
-            assertTrue("Remote file not created: " + clientFile, Files.exists(clientFile, IoUtils.EMPTY_LINK_OPTIONS));
-            byte[] local = Files.readAllBytes(clientFile);
-            assertArrayEquals("Mismatched remote written data", written, local);
-
-            try (SftpClient.CloseableHandle h = sftp.openDir(remoteFileDir)) {
-                boolean matchFound = false;
-                for (SftpClient.DirEntry entry : sftp.listDir(h)) {
-                    String name = entry.getFilename();
-                    if (clientFileName.equals(name)) {
-                        matchFound = true;
-                        break;
-                    }
-                }
-
-                assertTrue("No directory entry found for " + clientFileName, matchFound);
-            }
-
-            sftp.remove(remoteFilePath);
-            assertFalse("Remote file not removed: " + clientFile, Files.exists(clientFile, IoUtils.EMPTY_LINK_OPTIONS));
-        }
-    }
-
-    private SftpClient login() throws IOException {
-        return sftpClient.sftpLogin(TEST_LOCALHOST, port, getCurrentTestName(), getCurrentTestName());
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
index 629bbc9..49e6c28 100644
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
@@ -29,7 +29,6 @@ import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
 import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.scp.ScpCommandFactory;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.apache.sshd.util.test.JSchLogger;
@@ -58,7 +57,6 @@ public abstract class AbstractSftpClientTestSupport extends BaseTestSupport {
         JSchLogger.init();
         sshd = Utils.setupTestServer(AbstractSftpClientTestSupport.class);
         sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-        sshd.setCommandFactory(new ScpCommandFactory());
         sshd.start();
         port = sshd.getPort();
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java
new file mode 100644
index 0000000..87cafdb
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java
@@ -0,0 +1,424 @@
+/*
+ * 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.subsystem.sftp;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.channel.ChannelExec;
+import org.apache.sshd.client.channel.ChannelShell;
+import org.apache.sshd.client.channel.ChannelSubsystem;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.future.OpenFuture;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.SubsystemClient;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.RuntimeSshException;
+import org.apache.sshd.common.Service;
+import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.channel.ChannelListener;
+import org.apache.sshd.common.channel.ChannelListenerManager;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionListener;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.channel.ChannelSessionFactory;
+import org.apache.sshd.server.forward.DirectTcpipFactory;
+import org.apache.sshd.server.session.ServerConnectionServiceFactory;
+import org.apache.sshd.server.session.ServerUserAuthService;
+import org.apache.sshd.server.session.ServerUserAuthServiceFactory;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.EchoShell;
+import org.apache.sshd.util.test.EchoShellFactory;
+import org.apache.sshd.util.test.TestChannelListener;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ClientTest extends BaseTestSupport {
+    private SshServer sshd;
+    private SshClient client;
+    private int port;
+    private CountDownLatch authLatch;
+    private CountDownLatch channelLatch;
+
+    private final AtomicReference<ClientSession> clientSessionHolder = new AtomicReference<>(null);
+    @SuppressWarnings("synthetic-access")
+    private final SessionListener clientSessionListener = new SessionListener() {
+        @Override
+        public void sessionCreated(Session session) {
+            assertObjectInstanceOf("Non client session creation notification", ClientSession.class, session);
+            assertNull("Multiple creation notifications", clientSessionHolder.getAndSet((ClientSession) session));
+        }
+
+        @Override
+        public void sessionEvent(Session session, Event event) {
+            assertObjectInstanceOf("Non client session event notification: " + event, ClientSession.class, session);
+            assertSame("Mismatched client session event instance: " + event, clientSessionHolder.get(), session);
+        }
+
+        @Override
+        public void sessionException(Session session, Throwable t) {
+            assertObjectInstanceOf("Non client session exception notification", ClientSession.class, session);
+            assertNotNull("No session exception data", t);
+        }
+
+        @Override
+        public void sessionClosed(Session session) {
+            assertObjectInstanceOf("Non client session closure notification", ClientSession.class, session);
+            assertSame("Mismatched client session closure instance", clientSessionHolder.getAndSet(null), session);
+        }
+    };
+
+    public ClientTest() {
+        super();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        authLatch = new CountDownLatch(0);
+        channelLatch = new CountDownLatch(0);
+
+        sshd = setupTestServer();
+        sshd.setShellFactory(new TestEchoShellFactory());
+        sshd.setServiceFactories(Arrays.asList(
+                new ServerUserAuthServiceFactory() {
+                    @Override
+                    public Service create(Session session) throws IOException {
+                        return new ServerUserAuthService(session) {
+                            @SuppressWarnings("synthetic-access")
+                            @Override
+                            public void process(int cmd, Buffer buffer) throws Exception {
+                                authLatch.await();
+                                super.process(cmd, buffer);
+                            }
+                        };
+                    }
+                },
+                ServerConnectionServiceFactory.INSTANCE
+        ));
+        sshd.setChannelFactories(Arrays.asList(
+                new ChannelSessionFactory() {
+                    @Override
+                    public Channel create() {
+                        return new ChannelSession() {
+                            @SuppressWarnings("synthetic-access")
+                            @Override
+                            public OpenFuture open(int recipient, long rwsize, long rmpsize, Buffer buffer) {
+                                try {
+                                    channelLatch.await();
+                                } catch (InterruptedException e) {
+                                    throw new RuntimeSshException(e);
+                                }
+                                return super.open(recipient, rwsize, rmpsize, buffer);
+                            }
+
+                            @Override
+                            public String toString() {
+                                return "ChannelSession" + "[id=" + getId() + ", recipient=" + getRecipient() + "]";
+                            }
+                        };
+                    }
+                },
+                DirectTcpipFactory.INSTANCE));
+        sshd.start();
+        port = sshd.getPort();
+
+        client = setupTestClient();
+        clientSessionHolder.set(null);  // just making sure
+        client.addSessionListener(clientSessionListener);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (sshd != null) {
+            sshd.stop(true);
+        }
+        if (client != null) {
+            client.stop();
+        }
+        clientSessionHolder.set(null);  // just making sure
+    }
+
+    @Test
+    public void testSimpleClientListener() throws Exception {
+        AtomicReference<Channel> channelHolder = new AtomicReference<>(null);
+        client.addChannelListener(new ChannelListener() {
+            @Override
+            public void channelOpenSuccess(Channel channel) {
+                assertSame("Mismatched opened channel instances", channel, channelHolder.get());
+            }
+
+            @Override
+            public void channelOpenFailure(Channel channel, Throwable reason) {
+                assertSame("Mismatched failed open channel instances", channel, channelHolder.get());
+            }
+
+            @Override
+            public void channelInitialized(Channel channel) {
+                assertNull("Multiple channel initialization notifications", channelHolder.getAndSet(channel));
+            }
+
+            @Override
+            public void channelStateChanged(Channel channel, String hint) {
+                outputDebugMessage("channelStateChanged(%s): %s", channel, hint);
+            }
+
+            @Override
+            public void channelClosed(Channel channel, Throwable reason) {
+                assertSame("Mismatched closed channel instances", channel, channelHolder.getAndSet(null));
+            }
+        });
+        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
+
+        client.start();
+
+        try (ClientSession session = createTestClientSession()) {
+            testClientListener(channelHolder, ChannelShell.class, () -> {
+                try {
+                    return session.createShellChannel();
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            testClientListener(channelHolder, ChannelExec.class, () -> {
+                try {
+                    return session.createExecChannel(getCurrentTestName());
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            testClientListener(channelHolder, SftpClient.class, () -> {
+                try {
+                    return SftpClientFactory.instance().createSftpClient(session);
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+        } finally {
+            client.stop();
+        }
+    }
+
+    private <C extends Closeable> void testClientListener(AtomicReference<Channel> channelHolder, Class<C> channelType, Factory<? extends C> factory) throws Exception {
+        assertNull(channelType.getSimpleName() + ": Unexpected currently active channel", channelHolder.get());
+
+        try (C instance = factory.create()) {
+            Channel expectedChannel;
+            if (instance instanceof Channel) {
+                expectedChannel = (Channel) instance;
+            } else if (instance instanceof SubsystemClient) {
+                expectedChannel = ((SubsystemClient) instance).getClientChannel();
+            } else {
+                throw new UnsupportedOperationException("Unknown test instance type" + instance.getClass().getSimpleName());
+            }
+
+            Channel actualChannel = channelHolder.get();
+            assertSame("Mismatched listener " + channelType.getSimpleName() + " instances", expectedChannel, actualChannel);
+        }
+
+        assertNull(channelType.getSimpleName() + ": Active channel closure not signalled", channelHolder.get());
+    }
+
+    @Test
+    public void testCreateChannelByType() throws Exception {
+        client.start();
+
+        Collection<ClientChannel> channels = new LinkedList<>();
+        try (ClientSession session = createTestClientSession()) {
+            // required since we do not use an SFTP subsystem
+            PropertyResolverUtils.updateProperty(session, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
+            channels.add(session.createChannel(Channel.CHANNEL_SUBSYSTEM, SftpConstants.SFTP_SUBSYSTEM_NAME));
+            channels.add(session.createChannel(Channel.CHANNEL_EXEC, getCurrentTestName()));
+            channels.add(session.createChannel(Channel.CHANNEL_SHELL, getClass().getSimpleName()));
+
+            Set<Integer> ids = new HashSet<>(channels.size());
+            for (ClientChannel c : channels) {
+                int id = c.getId();
+                assertTrue("Channel ID repeated: " + id, ids.add(id));
+            }
+        } finally {
+            for (Closeable c : channels) {
+                try {
+                    c.close();
+                } catch (IOException e) {
+                    // ignored
+                }
+            }
+            client.stop();
+        }
+
+        assertNull("Session closure not signalled", clientSessionHolder.get());
+    }
+
+    /**
+     * Makes sure that the {@link ChannelListener}s added to the client, session
+     * and channel are <U>cumulative</U> - i.e., all of them invoked
+     * @throws Exception If failed
+     */
+    @Test
+    public void testChannelListenersPropagation() throws Exception {
+        Map<String, TestChannelListener> clientListeners = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        addChannelListener(clientListeners, client, new TestChannelListener(client.getClass().getSimpleName()));
+
+        // required since we do not use an SFTP subsystem
+        PropertyResolverUtils.updateProperty(client, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
+        client.start();
+        try (ClientSession session = createTestClientSession()) {
+            addChannelListener(clientListeners, session, new TestChannelListener(session.getClass().getSimpleName()));
+            assertListenerSizes("ClientSessionOpen", clientListeners, 0, 0);
+
+            try (ClientChannel channel = session.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME)) {
+                channel.open().verify(5L, TimeUnit.SECONDS);
+
+                TestChannelListener channelListener = new TestChannelListener(channel.getClass().getSimpleName());
+                // need to emulate them since we are adding the listener AFTER the channel is open
+                channelListener.channelInitialized(channel);
+                channelListener.channelOpenSuccess(channel);
+                channel.addChannelListener(channelListener);
+                assertListenerSizes("ClientChannelOpen", clientListeners, 1, 1);
+            }
+
+            assertListenerSizes("ClientChannelClose", clientListeners, 0, 1);
+        } finally {
+            client.stop();
+        }
+
+        assertListenerSizes("ClientStop", clientListeners, 0, 1);
+    }
+
+    private static void assertListenerSizes(String phase, Map<String, ? extends TestChannelListener> listeners, int activeSize, int openSize) {
+        assertListenerSizes(phase, listeners.values(), activeSize, openSize);
+    }
+
+    private static void assertListenerSizes(String phase, Collection<? extends TestChannelListener> listeners, int activeSize, int openSize) {
+        if (GenericUtils.isEmpty(listeners)) {
+            return;
+        }
+
+        for (TestChannelListener l : listeners) {
+            if (activeSize >= 0) {
+                assertEquals(phase + ": mismatched active channels size for " + l.getName() + " listener", activeSize, GenericUtils.size(l.getActiveChannels()));
+            }
+
+            if (openSize >= 0) {
+                assertEquals(phase + ": mismatched open channels size for " + l.getName() + " listener", openSize, GenericUtils.size(l.getOpenChannels()));
+            }
+
+            assertEquals(phase + ": unexpected failed channels size for " + l.getName() + " listener", 0, GenericUtils.size(l.getFailedChannels()));
+        }
+    }
+
+    private static <L extends ChannelListener & NamedResource> void addChannelListener(Map<String, L> listeners, ChannelListenerManager manager, L listener) {
+        String name = listener.getName();
+        assertNull("Duplicate listener named " + name, listeners.put(name, listener));
+        manager.addChannelListener(listener);
+    }
+
+    private ClientSession createTestClientSession() throws IOException {
+        ClientSession session = createTestClientSession(TEST_LOCALHOST);
+        try {
+            InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+            assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
+
+            ClientSession returnValue = session;
+            session = null; // avoid 'finally' close
+            return returnValue;
+        } finally {
+            if (session != null) {
+                session.close();
+            }
+        }
+    }
+
+    private ClientSession createTestClientSession(String host) throws IOException {
+        ClientSession session = client.connect(getCurrentTestName(), host, port).verify(7L, TimeUnit.SECONDS).getSession();
+        try {
+            assertNotNull("Client session creation not signalled", clientSessionHolder.get());
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+            assertNotNull("No reported connect address", addr);
+            assertEquals("Mismatched connect port", port, addr.getPort());
+
+            ClientSession returnValue = session;
+            session = null; // avoid 'finally' close
+            return returnValue;
+        } finally {
+            if (session != null) {
+                session.close();
+            }
+        }
+    }
+
+    public static class TestEchoShellFactory extends EchoShellFactory {
+        @Override
+        public Command create() {
+            return new TestEchoShell();
+        }
+    }
+
+    public static class TestEchoShell extends EchoShell {
+        // CHECKSTYLE:OFF
+        public static CountDownLatch latch;
+        // CHECKSTYLE:ON
+
+        public TestEchoShell() {
+            super();
+        }
+
+        @Override
+        public void destroy() {
+            if (latch != null) {
+                latch.countDown();
+            }
+            super.destroy();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
index 5dc9bcf..95f9b66 100644
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
@@ -67,7 +67,6 @@ import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.scp.ScpCommandFactory;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemEnvironment;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 import org.apache.sshd.util.test.BaseTestSupport;
@@ -96,7 +95,6 @@ public class SftpFileSystemTest extends BaseTestSupport {
     public static void setupServerInstance() throws Exception {
         sshd = Utils.setupTestServer(SftpFileSystemTest.class);
         sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-        sshd.setCommandFactory(new ScpCommandFactory());
         sshd.start();
         port = sshd.getPort();
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java
new file mode 100644
index 0000000..2f41c0f
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.subsystem.sftp;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.EnumSet;
+
+import org.apache.sshd.client.subsystem.sftp.impl.SimpleSftpClientImpl;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.Utils;
+import org.apache.sshd.util.test.client.simple.BaseSimpleClientTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SimpleSftpClientTest extends BaseSimpleClientTestSupport {
+    private final Path targetPath;
+    private final Path parentPath;
+    private final FileSystemFactory fileSystemFactory;
+    private SimpleSftpClient sftpClient;
+
+    public SimpleSftpClientTest() throws Exception {
+        targetPath = detectTargetFolder();
+        parentPath = targetPath.getParent();
+        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
+        sshd.setFileSystemFactory(fileSystemFactory);
+        client.start();
+        sftpClient = new SimpleSftpClientImpl(simple);
+    }
+
+    @Test
+    public void testSessionClosedWhenClientClosed() throws Exception {
+        try (SftpClient sftp = login()) {
+            assertTrue("SFTP not open", sftp.isOpen());
+
+            Session session = sftp.getClientSession();
+            assertTrue("Session not open", session.isOpen());
+
+            sftp.close();
+            assertFalse("Session not closed", session.isOpen());
+            assertFalse("SFTP not closed", sftp.isOpen());
+        }
+    }
+
+    @Test
+    public void testSftpProxyCalls() throws Exception {
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(lclSftp);
+        Path clientFolder = assertHierarchyTargetFolderExists(lclSftp).resolve("client");
+        Path clientFile = clientFolder.resolve("file.txt");
+        String remoteFileDir = Utils.resolveRelativeRemotePath(parentPath, clientFolder);
+        String clientFileName = clientFile.getFileName().toString();
+        String remoteFilePath = remoteFileDir + "/" + clientFileName;
+
+        try (SftpClient sftp = login()) {
+            sftp.mkdir(remoteFileDir);
+
+            byte[] written = (getClass().getSimpleName() + "#" + getCurrentTestName() + IoUtils.EOL).getBytes(StandardCharsets.UTF_8);
+            try (SftpClient.CloseableHandle h = sftp.open(remoteFilePath, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) {
+                sftp.write(h, 0L, written);
+
+                SftpClient.Attributes attrs = sftp.stat(h);
+                assertNotNull("No handle attributes", attrs);
+                assertEquals("Mismatched remote file size", written.length, attrs.getSize());
+            }
+
+            assertTrue("Remote file not created: " + clientFile, Files.exists(clientFile, IoUtils.EMPTY_LINK_OPTIONS));
+            byte[] local = Files.readAllBytes(clientFile);
+            assertArrayEquals("Mismatched remote written data", written, local);
+
+            try (SftpClient.CloseableHandle h = sftp.openDir(remoteFileDir)) {
+                boolean matchFound = false;
+                for (SftpClient.DirEntry entry : sftp.listDir(h)) {
+                    String name = entry.getFilename();
+                    if (clientFileName.equals(name)) {
+                        matchFound = true;
+                        break;
+                    }
+                }
+
+                assertTrue("No directory entry found for " + clientFileName, matchFound);
+            }
+
+            sftp.remove(remoteFilePath);
+            assertFalse("Remote file not removed: " + clientFile, Files.exists(clientFile, IoUtils.EMPTY_LINK_OPTIONS));
+        }
+    }
+
+    private SftpClient login() throws IOException {
+        return sftpClient.sftpLogin(TEST_LOCALHOST, port, getCurrentTestName(), getCurrentTestName());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
----------------------------------------------------------------------
diff --git a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
index d6dafee..6b343b0 100644
--- a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
+++ b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
@@ -48,7 +48,6 @@ import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.scp.ScpCommandFactory;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.apache.sshd.util.test.JSchLogger;
@@ -113,7 +112,6 @@ public class ApacheSshdSftpSessionFactoryTest extends BaseTestSupport {
         JSchLogger.init();
         sshd = Utils.setupTestServer(ApacheSshdSftpSessionFactoryTest.class);
         sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-        sshd.setCommandFactory(new ScpCommandFactory());
         sshd.start();
         port = sshd.getPort();
 


[2/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
new file mode 100644
index 0000000..99e3e34
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
@@ -0,0 +1,272 @@
+/*
+ * 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.server.scp;
+
+import java.util.Collection;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpFileOpenerHolder;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.util.EventListenerUtils;
+import org.apache.sshd.common.util.ObjectBuilder;
+import org.apache.sshd.common.util.threads.ExecutorServiceConfigurer;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+
+/**
+ * This <code>CommandFactory</code> can be used as a standalone command factory
+ * or can be used to augment another <code>CommandFactory</code> and provides
+ * <code>SCP</code> support.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see ScpCommand
+ */
+public class ScpCommandFactory
+        implements ScpFileOpenerHolder,
+        CommandFactory,
+        Cloneable,
+        ExecutorServiceConfigurer {
+    /**
+     * A useful {@link ObjectBuilder} for {@link ScpCommandFactory}
+     */
+    public static class Builder implements ObjectBuilder<ScpCommandFactory> {
+        private final ScpCommandFactory factory = new ScpCommandFactory();
+
+        public Builder() {
+            super();
+        }
+
+        public Builder withFileOpener(ScpFileOpener opener) {
+            factory.setScpFileOpener(opener);
+            return this;
+        }
+
+        public Builder withDelegate(CommandFactory delegate) {
+            factory.setDelegateCommandFactory(delegate);
+            return this;
+        }
+
+        public Builder withExecutorService(ExecutorService service) {
+            factory.setExecutorService(service);
+            return this;
+        }
+
+        public Builder withShutdownOnExit(boolean shutdown) {
+            factory.setShutdownOnExit(shutdown);
+            return this;
+        }
+
+        public Builder withSendBufferSize(int sendSize) {
+            factory.setSendBufferSize(sendSize);
+            return this;
+        }
+
+        public Builder withReceiveBufferSize(int receiveSize) {
+            factory.setReceiveBufferSize(receiveSize);
+            return this;
+        }
+
+        public Builder addEventListener(ScpTransferEventListener listener) {
+            factory.addEventListener(listener);
+            return this;
+        }
+
+        public Builder removeEventListener(ScpTransferEventListener listener) {
+            factory.removeEventListener(listener);
+            return this;
+        }
+
+        @Override
+        public ScpCommandFactory build() {
+            return factory.clone();
+        }
+    }
+
+    /*
+     * NOTE: we expose setters since there is no problem to change these settings between
+     * successive invocations of the 'createCommand' method
+     */
+    private CommandFactory delegate;
+    private ExecutorService executors;
+    private boolean shutdownExecutor;
+    private ScpFileOpener fileOpener;
+    private int sendBufferSize = ScpHelper.MIN_SEND_BUFFER_SIZE;
+    private int receiveBufferSize = ScpHelper.MIN_RECEIVE_BUFFER_SIZE;
+    private Collection<ScpTransferEventListener> listeners = new CopyOnWriteArraySet<>();
+    private ScpTransferEventListener listenerProxy;
+
+    public ScpCommandFactory() {
+        listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), listeners);
+    }
+
+    @Override
+    public ScpFileOpener getScpFileOpener() {
+        return fileOpener;
+    }
+
+    @Override
+    public void setScpFileOpener(ScpFileOpener fileOpener) {
+        this.fileOpener = fileOpener;
+    }
+
+    public CommandFactory getDelegateCommandFactory() {
+        return delegate;
+    }
+
+    /**
+     * @param factory A {@link CommandFactory} to be used if the
+     * command is not an SCP one. If {@code null} then an {@link IllegalArgumentException}
+     * will be thrown when attempting to invoke {@link #createCommand(String)}
+     * with a non-SCP command
+     */
+    public void setDelegateCommandFactory(CommandFactory factory) {
+        delegate = factory;
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        return executors;
+    }
+
+    /**
+     * @param service An {@link ExecutorService} to be used when
+     * starting {@link ScpCommand} execution. If {@code null} then a single-threaded
+     * ad-hoc service is used. <B>Note:</B> the service will <U>not</U> be shutdown
+     * when the command is terminated - unless it is the ad-hoc service, which will be
+     * shutdown regardless
+     */
+    @Override
+    public void setExecutorService(ExecutorService service) {
+        executors = service;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownExecutor;
+    }
+
+    @Override
+    public void setShutdownOnExit(boolean shutdown) {
+        shutdownExecutor = shutdown;
+    }
+
+    public int getSendBufferSize() {
+        return sendBufferSize;
+    }
+
+    /**
+     * @param sendSize Size (in bytes) of buffer to use when sending files
+     * @see ScpHelper#MIN_SEND_BUFFER_SIZE
+     */
+    public void setSendBufferSize(int sendSize) {
+        if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
+            throw new IllegalArgumentException("<ScpCommandFactory>() send buffer size "
+                    + "(" + sendSize + ") below minimum required (" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
+        }
+        sendBufferSize = sendSize;
+    }
+
+    public int getReceiveBufferSize() {
+        return receiveBufferSize;
+    }
+
+    /**
+     * @param receiveSize Size (in bytes) of buffer to use when receiving files
+     * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
+     */
+    public void setReceiveBufferSize(int receiveSize) {
+        if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
+            throw new IllegalArgumentException("<ScpCommandFactory>() receive buffer size "
+                    + "(" + receiveSize + ") below minimum required (" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
+        }
+        receiveBufferSize = receiveSize;
+    }
+
+    /**
+     * @param listener The {@link ScpTransferEventListener} to add
+     * @return {@code true} if this is a <U>new</U> listener instance,
+     * {@code false} if the listener is already registered
+     * @throws IllegalArgumentException if {@code null} listener
+     */
+    public boolean addEventListener(ScpTransferEventListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("No listener instance");
+        }
+
+        return listeners.add(listener);
+    }
+
+    /**
+     * @param listener The {@link ScpTransferEventListener} to remove
+     * @return {@code true} if the listener was registered and removed,
+     * {@code false} if the listener was not registered to begin with
+     * @throws IllegalArgumentException if {@code null} listener
+     */
+    public boolean removeEventListener(ScpTransferEventListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("No listener instance");
+        }
+
+        return listeners.remove(listener);
+    }
+
+    /**
+     * Parses a command string and verifies that the basic syntax is
+     * correct. If parsing fails the responsibility is delegated to
+     * the configured {@link CommandFactory} instance; if one exist.
+     *
+     * @param command command to parse
+     * @return configured {@link Command} instance
+     * @throws IllegalArgumentException if not an SCP command and no
+     *                                  delegate command factory is available
+     * @see ScpHelper#SCP_COMMAND_PREFIX
+     */
+    @Override
+    public Command createCommand(String command) {
+        if (command.startsWith(ScpHelper.SCP_COMMAND_PREFIX)) {
+            return new ScpCommand(command,
+                    getExecutorService(), isShutdownOnExit(),
+                    getSendBufferSize(), getReceiveBufferSize(),
+                    getScpFileOpener(), listenerProxy);
+        }
+
+        CommandFactory factory = getDelegateCommandFactory();
+        if (factory != null) {
+            return factory.createCommand(command);
+        }
+
+        throw new IllegalArgumentException("Unknown command, does not begin with '" + ScpHelper.SCP_COMMAND_PREFIX + "': " + command);
+    }
+
+    @Override
+    public ScpCommandFactory clone() {
+        try {
+            ScpCommandFactory other = getClass().cast(super.clone());
+            // clone the listeners set as well
+            other.listeners = new CopyOnWriteArraySet<>(this.listeners);
+            other.listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), other.listeners);
+            return other;
+        } catch (CloneNotSupportedException e) {
+            throw new RuntimeException(e);    // un-expected...
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java b/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
new file mode 100644
index 0000000..7b7d2e8
--- /dev/null
+++ b/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
@@ -0,0 +1,1203 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.scp.ScpCommand;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.JSchLogger;
+import org.apache.sshd.util.test.SimpleUserInfo;
+import org.apache.sshd.util.test.Utils;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+import ch.ethz.ssh2.Connection;
+import ch.ethz.ssh2.ConnectionInfo;
+import ch.ethz.ssh2.SCPClient;
+
+/**
+ * Test for SCP support.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ScpTest extends BaseTestSupport {
+    private static final ScpTransferEventListener DEBUG_LISTENER = new ScpTransferEventListener() {
+        @Override
+        public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
+            logEvent("starFolderEvent", op, file, false, -1L, perms, null);
+        }
+
+        @Override
+        public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
+            logEvent("startFileEvent", op, file, true, length, perms, null);
+
+        }
+
+        @Override
+        public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
+            logEvent("endFolderEvent", op, file, false, -1L, perms, thrown);
+        }
+
+        @Override
+        public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
+            logEvent("endFileEvent", op, file, true, length, perms, thrown);
+        }
+
+        private void logEvent(String type, FileOperation op, Path path, boolean isFile, long length, Collection<PosixFilePermission> perms, Throwable t) {
+            if (!OUTPUT_DEBUG_MESSAGES) {
+                return; // just in case
+            }
+            StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
+            sb.append('\t').append(type)
+                    .append('[').append(op).append(']')
+                    .append(' ').append(isFile ? "File" : "Directory").append('=').append(path)
+                    .append(' ').append("length=").append(length)
+                    .append(' ').append("perms=").append(perms);
+            if (t != null) {
+                sb.append(' ').append("ERROR=").append(t.getClass().getSimpleName()).append(": ").append(t.getMessage());
+            }
+            outputDebugMessage(sb.toString());
+        }
+    };
+
+    private static SshServer sshd;
+    private static int port;
+    private static SshClient client;
+    private final FileSystemFactory fileSystemFactory;
+
+    public ScpTest() throws IOException {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
+    }
+
+    @BeforeClass
+    public static void setupClientAndServer() throws Exception {
+        JSchLogger.init();
+        sshd = Utils.setupTestServer(ScpTest.class);
+        sshd.setCommandFactory(new ScpCommandFactory());
+        sshd.start();
+        port = sshd.getPort();
+
+        client = Utils.setupTestClient(ScpTest.class);
+        client.start();
+    }
+
+    @AfterClass
+    public static void tearDownClientAndServer() throws Exception {
+        if (sshd != null) {
+            try {
+                sshd.stop(true);
+            } finally {
+                sshd = null;
+            }
+        }
+
+        if (client != null) {
+            try {
+                client.stop();
+            } finally {
+                client = null;
+            }
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        sshd.setFileSystemFactory(fileSystemFactory);
+    }
+
+    @Test
+    public void testNormalizedScpRemotePaths() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(scpRoot);
+
+        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+        Path localFile = localDir.resolve("file.txt");
+        byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+        Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
+        String localPath = localFile.toString();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+        String[] remoteComps = GenericUtils.split(remotePath, '/');
+        Factory<? extends Random> factory = client.getRandomFactory();
+        Random rnd = factory.create();
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            StringBuilder sb = new StringBuilder(remotePath.length() + Long.SIZE);
+            for (int i = 0; i < Math.max(Long.SIZE, remoteComps.length); i++) {
+                if (sb.length() > 0) {
+                    sb.setLength(0);    // start again
+                }
+
+                sb.append(remoteComps[0]);
+                for (int j = 1; j < remoteComps.length; j++) {
+                    String name = remoteComps[j];
+                    slashify(sb, rnd);
+                    sb.append(name);
+                }
+                slashify(sb, rnd);
+
+                String path = sb.toString();
+                scp.upload(localPath, path);
+                assertTrue("Remote file not ready for " + path, waitForFile(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L)));
+
+                byte[] actual = Files.readAllBytes(remoteFile);
+                assertArrayEquals("Mismatched uploaded data for " + path, data, actual);
+                Files.delete(remoteFile);
+                assertFalse("Remote file (" + remoteFile + ") not deleted for " + path, Files.exists(remoteFile));
+            }
+        }
+    }
+
+    private static int slashify(StringBuilder sb, Random rnd) {
+        int slashes = 1 /* at least one slash */ + rnd.random(Byte.SIZE);
+        for (int k = 0; k < slashes; k++) {
+            sb.append('/');
+        }
+
+        return slashes;
+    }
+
+    @Test
+    public void testUploadAbsoluteDriveLetter() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(scpRoot);
+
+        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+        Path localFile = localDir.resolve("file-1.txt");
+        byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+        Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
+        String localPath = localFile.toString();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            scp.upload(localPath, remotePath);
+            assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L));
+
+            Path secondRemote = remoteDir.resolve("file-2.txt");
+            String secondPath = Utils.resolveRelativeRemotePath(parentPath, secondRemote);
+            scp.upload(localPath, secondPath);
+            assertFileLength(secondRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
+
+            Path pathRemote = remoteDir.resolve("file-path.txt");
+            String pathPath = Utils.resolveRelativeRemotePath(parentPath, pathRemote);
+            scp.upload(localFile, pathPath);
+            assertFileLength(pathRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
+        }
+    }
+
+    @Test
+    public void testScpUploadOverwrite() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+            Path localFile = localDir.resolve("file.txt");
+            Utils.writeFile(localFile, data);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            Path remoteFile = remoteDir.resolve(localFile.getFileName());
+            Utils.writeFile(remoteFile, data + data);
+
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+            scp.upload(localFile.toString(), remotePath);
+            assertFileLength(remoteFile, data.length(), TimeUnit.SECONDS.toMillis(5L));
+        }
+    }
+
+    @Test
+    public void testScpUploadZeroLengthFile() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+        Path zeroLocal = localDir.resolve("zero.txt");
+
+        try (FileChannel fch = FileChannel.open(zeroLocal, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+            if (fch.size() > 0L) {
+                fch.truncate(0L);
+            }
+        }
+        assertEquals("Non-zero size for local file=" + zeroLocal, 0L, Files.size(zeroLocal));
+
+        Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
+        if (Files.exists(zeroRemote)) {
+            Files.delete(zeroRemote);
+        }
+
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
+            scp.upload(zeroLocal.toString(), remotePath);
+            assertFileLength(zeroRemote, 0L, TimeUnit.SECONDS.toMillis(5L));
+        }
+    }
+
+    @Test
+    public void testScpDownloadZeroLengthFile() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+        Path zeroLocal = localDir.resolve(getCurrentTestName());
+        if (Files.exists(zeroLocal)) {
+            Files.delete(zeroLocal);
+        }
+
+        Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
+        try (FileChannel fch = FileChannel.open(zeroRemote, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+            if (fch.size() > 0L) {
+                fch.truncate(0L);
+            }
+        }
+        assertEquals("Non-zero size for remote file=" + zeroRemote, 0L, Files.size(zeroRemote));
+
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
+            scp.download(remotePath, zeroLocal.toString());
+            assertFileLength(zeroLocal, 0L, TimeUnit.SECONDS.toMillis(5L));
+        }
+    }
+
+    @Test
+    @Ignore("TODO investigate why this fails often")
+    public void testScpNativeOnSingleFile() throws Exception {
+        String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(scpRoot);
+
+        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+        Path localOutFile = localDir.resolve("file-1.txt");
+        Path remoteDir = scpRoot.resolve("remote");
+        Path remoteOutFile = remoteDir.resolve(localOutFile.getFileName());
+
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            Utils.writeFile(localOutFile, data);
+
+            assertFalse("Remote folder already exists: " + remoteDir, Files.exists(remoteDir));
+
+            String localOutPath = localOutFile.toString();
+            String remoteOutPath = Utils.resolveRelativeRemotePath(parentPath, remoteOutFile);
+            outputDebugMessage("Expect upload failure %s => %s", localOutPath, remoteOutPath);
+            try {
+                scp.upload(localOutPath, remoteOutPath);
+                fail("Expected IOException for 1st time " + remoteOutPath);
+            } catch (IOException e) {
+                // ok
+            }
+
+            assertHierarchyTargetFolderExists(remoteDir);
+            outputDebugMessage("Expect upload success %s => %s", localOutPath, remoteOutPath);
+            scp.upload(localOutPath, remoteOutPath);
+            assertFileLength(remoteOutFile, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+            Path secondLocal = localDir.resolve(localOutFile.getFileName());
+            String downloadTarget = Utils.resolveRelativeRemotePath(parentPath, secondLocal);
+            outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
+            scp.download(remoteOutPath, downloadTarget);
+            assertFileLength(secondLocal, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+            Path localPath = localDir.resolve("file-path.txt");
+            downloadTarget = Utils.resolveRelativeRemotePath(parentPath, localPath);
+            outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
+            scp.download(remoteOutPath, downloadTarget);
+            assertFileLength(localPath, data.length(), TimeUnit.SECONDS.toMillis(11L));
+        }
+    }
+
+    @Test
+    public void testScpNativeOnMultipleFiles() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+            Path local1 = localDir.resolve("file-1.txt");
+            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+            Path local2 = localDir.resolve("file-2.txt");
+            Files.write(local2, data);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            Path remote1 = remoteDir.resolve(local1.getFileName());
+            String remote1Path = Utils.resolveRelativeRemotePath(parentPath, remote1);
+            String[] locals = {local1.toString(), local2.toString()};
+            try {
+                scp.upload(locals, remote1Path);
+                fail("Unexpected upload success to missing remote file: " + remote1Path);
+            } catch (IOException e) {
+                // Ok
+            }
+
+            Files.write(remote1, data);
+            try {
+                scp.upload(locals, remote1Path);
+                fail("Unexpected upload success to existing remote file: " + remote1Path);
+            } catch (IOException e) {
+                // Ok
+            }
+
+            Path remoteSubDir = assertHierarchyTargetFolderExists(remoteDir.resolve("dir"));
+            scp.upload(locals, Utils.resolveRelativeRemotePath(parentPath, remoteSubDir));
+
+            Path remoteSub1 = remoteSubDir.resolve(local1.getFileName());
+            assertFileLength(remoteSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            Path remoteSub2 = remoteSubDir.resolve(local2.getFileName());
+            assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            String[] remotes = {
+                Utils.resolveRelativeRemotePath(parentPath, remoteSub1),
+                Utils.resolveRelativeRemotePath(parentPath, remoteSub2),
+            };
+
+            try {
+                scp.download(remotes, Utils.resolveRelativeRemotePath(parentPath, local1));
+                fail("Unexpected download success to existing local file: " + local1);
+            } catch (IOException e) {
+                // Ok
+            }
+
+            Path localSubDir = localDir.resolve("dir");
+            try {
+                scp.download(remotes, localSubDir);
+                fail("Unexpected download success to non-existing folder: " + localSubDir);
+            } catch (IOException e) {
+                // Ok
+            }
+
+            assertHierarchyTargetFolderExists(localSubDir);
+            scp.download(remotes, localSubDir);
+
+            assertFileLength(localSubDir.resolve(remoteSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertFileLength(localSubDir.resolve(remoteSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+        }
+    }
+
+    @Test
+    public void testScpNativeOnRecursiveDirs() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path localDir = scpRoot.resolve("local");
+            Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
+            Path localSub1 = localSubDir.resolve("file-1.txt");
+            byte[] data = Utils.writeFile(localSub1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+            Path localSub2 = localSubDir.resolve("file-2.txt");
+            Files.write(localSub2, data);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            scp.upload(localSubDir, Utils.resolveRelativeRemotePath(parentPath, remoteDir), ScpClient.Option.Recursive);
+
+            Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
+            assertFileLength(remoteSubDir.resolve(localSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            Utils.deleteRecursive(localSubDir);
+
+            scp.download(Utils.resolveRelativeRemotePath(parentPath, remoteSubDir), localDir, ScpClient.Option.Recursive);
+            assertFileLength(localSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+        }
+    }
+
+    @Test
+    public void testScpNativeOnDirWithPattern() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+            Path local1 = localDir.resolve("file-1.txt");
+            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+            Path local2 = localDir.resolve("file-2.txt");
+            Files.write(local2, data);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+            scp.upload(localDir.toString() + File.separator + "*", remotePath);
+            assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertFileLength(remoteDir.resolve(local2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            Files.delete(local1);
+            Files.delete(local2);
+            scp.download(remotePath + "/*", localDir);
+            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertFileLength(local2, data.length, TimeUnit.SECONDS.toMillis(11L));
+        }
+    }
+
+    @Test
+    public void testScpNativeOnMixedDirAndFiles() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path localDir = scpRoot.resolve("local");
+            Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
+            Path local1 = localDir.resolve("file-1.txt");
+            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+            Path localSub2 = localSubDir.resolve("file-2.txt");
+            Files.write(localSub2, data);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+            scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive);
+            assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
+            assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            Files.delete(local1);
+            Utils.deleteRecursive(localSubDir);
+
+            scp.download(remotePath + "/*", localDir);
+            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertFalse("Unexpected recursive local file: " + localSub2, Files.exists(localSub2));
+
+            Files.delete(local1);
+            scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive);
+            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L));
+            assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(5L));
+        }
+    }
+
+    @Test
+    public void testScpNativePreserveAttributes() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path localDir = scpRoot.resolve("local");
+            Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
+            // convert everything to seconds since this is the SCP timestamps granularity
+            final long lastModMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+            final long lastModSecs = TimeUnit.MILLISECONDS.toSeconds(lastModMillis);
+            Path local1 = localDir.resolve("file-1.txt");
+            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+            File lclFile1 = local1.toFile();
+            boolean lcl1ModSet = lclFile1.setLastModified(lastModMillis);
+            lclFile1.setExecutable(true, true);
+            lclFile1.setWritable(false, false);
+
+            Path localSub2 = localSubDir.resolve("file-2.txt");
+            Files.write(localSub2, data);
+            File lclSubFile2 = localSub2.toFile();
+            boolean lclSub2ModSet = lclSubFile2.setLastModified(lastModMillis);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+            scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+
+            Path remote1 = remoteDir.resolve(local1.getFileName());
+            assertFileLength(remote1, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            File remFile1 = remote1.toFile();
+            assertLastModifiedTimeEquals(remFile1, lcl1ModSet, lastModSecs);
+
+            Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
+            Path remoteSub2 = remoteSubDir.resolve(localSub2.getFileName());
+            assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            File remSubFile2 = remoteSub2.toFile();
+            assertLastModifiedTimeEquals(remSubFile2, lclSub2ModSet, lastModSecs);
+
+            Utils.deleteRecursive(localDir);
+            assertHierarchyTargetFolderExists(localDir);
+
+            scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertLastModifiedTimeEquals(lclFile1, lcl1ModSet, lastModSecs);
+            assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+            assertLastModifiedTimeEquals(lclSubFile2, lclSub2ModSet, lastModSecs);
+        }
+    }
+
+    @Test
+    public void testStreamsUploadAndDownload() throws Exception {
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClient scp = createScpClient(session);
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            Path remoteFile = remoteDir.resolve("file.txt");
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+            byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+            outputDebugMessage("Upload data to %s", remotePath);
+            scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
+            assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            byte[] uploaded = Files.readAllBytes(remoteFile);
+            assertArrayEquals("Mismatched uploaded data", data, uploaded);
+
+            outputDebugMessage("Download data from %s", remotePath);
+            byte[] downloaded = scp.downloadBytes(remotePath);
+            assertArrayEquals("Mismatched downloaded data", uploaded, downloaded);
+        }
+    }
+
+    @Test   // see SSHD-649
+    public void testScpFileOpener() throws Exception {
+        class TrackingFileOpener extends DefaultScpFileOpener {
+            private final AtomicInteger readCount = new AtomicInteger(0);
+            private final AtomicInteger writeCount = new AtomicInteger(0);
+
+            TrackingFileOpener() {
+                super();
+            }
+
+            public AtomicInteger getReadCount() {
+                return readCount;
+            }
+
+            public AtomicInteger getWriteCount() {
+                return writeCount;
+            }
+
+            @Override
+            public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
+                int count = readCount.incrementAndGet();
+                outputDebugMessage("openRead(%s)[%s] count=%d", session, file, count);
+                return super.openRead(session, file, options);
+            }
+
+            @Override
+            public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
+                int count = writeCount.incrementAndGet();
+                outputDebugMessage("openWrite(%s)[%s] count=%d", session, file, count);
+                return super.openWrite(session, file, options);
+            }
+        }
+
+        ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
+        ScpFileOpener opener = factory.getScpFileOpener();
+        TrackingFileOpener serverOpener = new TrackingFileOpener();
+        factory.setScpFileOpener(serverOpener);
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            TrackingFileOpener clientOpener = new TrackingFileOpener();
+            ScpClientCreator creator = ScpClientCreator.instance();
+            ScpClient scp = creator.createScpClient(session, clientOpener);
+
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot);
+            Path localFile = remoteDir.resolve("data.txt");
+            byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+            Files.write(localFile, data);
+
+            Path remoteFile = remoteDir.resolve("upload.txt");
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+            outputDebugMessage("Upload data to %s", remotePath);
+            scp.upload(localFile, remotePath);
+            assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            AtomicInteger serverRead = serverOpener.getReadCount();
+            assertEquals("Mismatched server upload open read count", 0, serverRead.get());
+
+            AtomicInteger serverWrite = serverOpener.getWriteCount();
+            assertEquals("Mismatched server upload write count", 1, serverWrite.getAndSet(0));
+
+            AtomicInteger clientRead = clientOpener.getReadCount();
+            assertEquals("Mismatched client upload read count", 1, clientRead.getAndSet(0));
+
+            AtomicInteger clientWrite = clientOpener.getWriteCount();
+            assertEquals("Mismatched client upload write count", 0, clientWrite.get());
+
+            Files.delete(localFile);
+            scp.download(remotePath, localFile);
+            assertFileLength(localFile, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+            assertEquals("Mismatched server download open read count", 1, serverRead.getAndSet(0));
+            assertEquals("Mismatched server download write count", 0, serverWrite.get());
+            assertEquals("Mismatched client download read count", 0, clientRead.get());
+            assertEquals("Mismatched client download write count", 1, clientWrite.getAndSet(0));
+        } finally {
+            factory.setScpFileOpener(opener);
+        }
+    }
+
+    @Test   // see SSHD-628
+    public void testScpExitStatusPropagation() throws Exception {
+        final int testExitValue = 7365;
+        class InternalScpCommand extends ScpCommand implements ExitCallback {
+            private ExitCallback delegate;
+
+            InternalScpCommand(String command, ExecutorService executorService, boolean shutdownOnExit,
+                    int sendSize, int receiveSize, ScpFileOpener opener, ScpTransferEventListener eventListener) {
+                super(command, executorService, shutdownOnExit, sendSize, receiveSize, opener, eventListener);
+            }
+
+            @Override
+            protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
+                outputDebugMessage("writeCommandResponseMessage(%s) status=%d", command, exitValue);
+                super.writeCommandResponseMessage(command, testExitValue, exitMessage);
+            }
+
+            @Override
+            public void setExitCallback(ExitCallback callback) {
+                delegate = callback;
+                super.setExitCallback(this);
+            }
+
+            @Override
+            public void onExit(int exitValue) {
+                onExit(exitValue, Integer.toString(exitValue));
+            }
+
+            @Override
+            public void onExit(int exitValue, String exitMessage) {
+                outputDebugMessage("onExit(%s) status=%d", this, exitValue);
+                if (exitValue == ScpHelper.OK) {
+                    delegate.onExit(testExitValue, exitMessage);
+                } else {
+                    delegate.onExit(exitValue, exitMessage);
+                }
+            }
+        }
+
+        ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
+        sshd.setCommandFactory(new ScpCommandFactory() {
+            @Override
+            public Command createCommand(String command) {
+                ValidateUtils.checkTrue(command.startsWith(ScpHelper.SCP_COMMAND_PREFIX), "Bad SCP command: %s", command);
+                return new InternalScpCommand(command,
+                        getExecutorService(), isShutdownOnExit(),
+                        getSendBufferSize(), getReceiveBufferSize(),
+                        DefaultScpFileOpener.INSTANCE,
+                        ScpTransferEventListener.EMPTY);
+            }
+        });
+
+        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(11L, TimeUnit.SECONDS);
+
+            ScpClientCreator creator = ScpClientCreator.instance();
+            ScpClient scp = creator.createScpClient(session);
+            Path targetPath = detectTargetFolder();
+            Path parentPath = targetPath.getParent();
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            Path remoteFile = remoteDir.resolve("file.txt");
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+            byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+            outputDebugMessage("Upload data to %s", remotePath);
+            try {
+                scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
+                outputDebugMessage("Upload success to %s", remotePath);
+            } catch (ScpException e) {
+                Integer exitCode = e.getExitStatus();
+                assertNotNull("No upload exit status", exitCode);
+                assertEquals("Mismatched upload exit status", testExitValue, exitCode.intValue());
+            }
+
+            if (Files.deleteIfExists(remoteFile)) {
+                outputDebugMessage("Deleted remote file %s", remoteFile);
+            }
+
+            try (OutputStream out = Files.newOutputStream(remoteFile)) {
+                out.write(data);
+            }
+
+            try {
+                byte[] downloaded = scp.downloadBytes(remotePath);
+                outputDebugMessage("Download success to %s: %s", remotePath, new String(downloaded, StandardCharsets.UTF_8));
+            } catch (ScpException e) {
+                Integer exitCode = e.getExitStatus();
+                assertNotNull("No download exit status", exitCode);
+                assertEquals("Mismatched download exit status", testExitValue, exitCode.intValue());
+            }
+        } finally {
+            sshd.setCommandFactory(factory);
+        }
+    }
+
+    // see http://stackoverflow.com/questions/2717936/file-createnewfile-creates-files-with-last-modified-time-before-actual-creatio
+    // See https://msdn.microsoft.com/en-us/library/ms724290(VS.85).aspx
+    private static void assertLastModifiedTimeEquals(File file, boolean modSuccess, long expectedSeconds) {
+        long expectedMillis = TimeUnit.SECONDS.toMillis(expectedSeconds);
+        long actualMillis = file.lastModified();
+        long actualSeconds = TimeUnit.MILLISECONDS.toSeconds(actualMillis);
+        // if failed to set the local file time, don't expect it to be the same
+        if (!modSuccess) {
+            System.err.append("Failed to set last modified time of ").append(file.getAbsolutePath())
+                      .append(" to ").append(String.valueOf(expectedMillis))
+                      .append(" - ").println(new Date(expectedMillis));
+            System.err.append("\t\t").append("Current value: ").append(String.valueOf(actualMillis))
+                      .append(" - ").println(new Date(actualMillis));
+            return;
+        }
+
+        if (OsUtils.isWin32()) {
+            // The NTFS file system delays updates to the last access time for a file by up to 1 hour after the last access
+            if (expectedSeconds != actualSeconds) {
+                System.err.append("Mismatched last modified time for ").append(file.getAbsolutePath())
+                          .append(" - expected=").append(String.valueOf(expectedSeconds))
+                          .append('[').append(new Date(expectedMillis).toString()).append(']')
+                          .append(", actual=").append(String.valueOf(actualSeconds))
+                          .append('[').append(new Date(actualMillis).toString()).append(']')
+                          .println();
+            }
+        } else {
+            assertEquals("Mismatched last modified time for " + file.getAbsolutePath(), expectedSeconds, actualSeconds);
+        }
+    }
+
+    @Test
+    public void testJschScp() throws Exception {
+        com.jcraft.jsch.Session session = getJschSession();
+        try {
+            String data = getCurrentTestName() + "\n";
+
+            String unixDir = "target/scp";
+            String fileName = getCurrentTestName() + ".txt";
+            String unixPath = unixDir + "/" + fileName;
+            File root = new File(unixDir);
+            File target = new File(unixPath);
+            Utils.deleteRecursive(root);
+            root.mkdirs();
+            assertTrue("Failed to ensure existence of " + root, root.exists());
+
+            target.delete();
+            assertFalse("Failed to delete 1st time: " + target, target.exists());
+            sendFile(session, unixPath, target, data);
+            assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+            target.delete();
+            assertFalse("Failed to delete 2nd time: " + target, target.exists());
+            sendFile(session, unixDir, target, data);
+            assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+            sendFileError(session, "target", ScpHelper.SCP_COMMAND_PREFIX, data);
+
+            readFileError(session, unixDir);
+
+            assertEquals("Mismatched file data", data, readFile(session, unixPath, target));
+            assertEquals("Mismatched dir data", data, readDir(session, unixDir, target));
+
+            target.delete();
+            root.delete();
+
+            sendDir(session, "target", ScpHelper.SCP_COMMAND_PREFIX, fileName, data);
+            assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
+        } finally {
+            session.disconnect();
+        }
+    }
+
+    protected com.jcraft.jsch.Session getJschSession() throws JSchException {
+        JSch sch = new JSch();
+        com.jcraft.jsch.Session session = sch.getSession(getCurrentTestName(), TEST_LOCALHOST, port);
+        session.setUserInfo(new SimpleUserInfo(getCurrentTestName()));
+        session.connect();
+        return session;
+    }
+
+    @Test
+    public void testWithGanymede() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(scpRoot);
+
+        byte[] expected = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+        String fileName = "file.txt";
+        Path remoteFile = remoteDir.resolve(fileName);
+        String mode = ScpHelper.getOctalPermissions(EnumSet.of(
+                PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE,
+                PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
+                PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE
+        ));
+
+        ch.ethz.ssh2.log.Logger.enabled = true;
+        Connection conn = new Connection(TEST_LOCALHOST, port);
+        try {
+            ConnectionInfo info = conn.connect(null, (int) TimeUnit.SECONDS.toMillis(5L), (int) TimeUnit.SECONDS.toMillis(13L));
+            outputDebugMessage("Connected: kex=%s, key-type=%s, c2senc=%s, s2cenc=%s, c2mac=%s, s2cmac=%s",
+                    info.keyExchangeAlgorithm, info.serverHostKeyAlgorithm,
+                    info.clientToServerCryptoAlgorithm, info.serverToClientCryptoAlgorithm,
+                    info.clientToServerMACAlgorithm, info.serverToClientMACAlgorithm);
+            assertTrue("Failed to authenticate", conn.authenticateWithPassword(getCurrentTestName(), getCurrentTestName()));
+
+            SCPClient scpClient = new SCPClient(conn);
+            try (OutputStream output = scpClient.put(fileName, expected.length, remotePath, mode)) {
+                output.write(expected);
+            }
+
+            assertTrue("Remote file not created: " + remoteFile, Files.exists(remoteFile));
+            byte[] remoteData = Files.readAllBytes(remoteFile);
+            assertArrayEquals("Mismatched remote put data", expected, remoteData);
+
+            Arrays.fill(remoteData, (byte) 0);  // make sure we start with a clean slate
+            try (InputStream input = scpClient.get(remotePath + "/" + fileName)) {
+                int readLen = input.read(remoteData);
+                assertEquals("Mismatched remote get data size", expected.length, readLen);
+                // make sure we reached EOF
+                assertEquals("Unexpected extra data after read expected size", -1, input.read());
+            }
+
+            assertArrayEquals("Mismatched remote get data", expected, remoteData);
+        } finally {
+            conn.close();
+        }
+    }
+
+    protected String readFile(com.jcraft.jsch.Session session, String path, File target) throws Exception {
+        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+        c.setCommand("scp -f " + path);
+        c.connect();
+
+        String fileName = target.getName();
+        try (OutputStream os = c.getOutputStream();
+             InputStream is = c.getInputStream()) {
+
+            os.write(0);
+            os.flush();
+
+            String header = readLine(is);
+            String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + fileName;
+            assertEquals("Mismatched header for " + path, expHeader, header);
+
+            String lenValue = header.substring(6, header.indexOf(' ', 6));
+            int length = Integer.parseInt(lenValue);
+            os.write(0);
+            os.flush();
+
+            byte[] buffer = new byte[length];
+            length = is.read(buffer, 0, buffer.length);
+            assertEquals("Mismatched read data length for " + path, length, buffer.length);
+            assertAckReceived(is, "Read data of " + path);
+
+            os.write(0);
+            os.flush();
+
+            return new String(buffer, StandardCharsets.UTF_8);
+        } finally {
+            c.disconnect();
+        }
+    }
+
+    protected String readDir(com.jcraft.jsch.Session session, String path, File target) throws Exception {
+        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+        c.setCommand("scp -r -f " + path);
+        c.connect();
+
+        try (OutputStream os = c.getOutputStream();
+             InputStream is = c.getInputStream()) {
+            os.write(0);
+            os.flush();
+
+            String header = readLine(is);
+            String expPrefix = "D" + ScpHelper.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 ";
+            assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith(expPrefix));
+            os.write(0);
+            os.flush();
+
+            header = readLine(is);
+            String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + target.getName();
+            assertEquals("Mismatched dir header for " + path, expHeader, header);
+            int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
+            os.write(0);
+            os.flush();
+
+            byte[] buffer = new byte[length];
+            length = is.read(buffer, 0, buffer.length);
+            assertEquals("Mismatched read buffer size for " + path, length, buffer.length);
+            assertAckReceived(is, "Read date of " + path);
+
+            os.write(0);
+            os.flush();
+
+            header = readLine(is);
+            assertEquals("Mismatched end value for " + path, "E", header);
+            os.write(0);
+            os.flush();
+
+            return new String(buffer, StandardCharsets.UTF_8);
+        } finally {
+            c.disconnect();
+        }
+    }
+
+    protected void readFileError(com.jcraft.jsch.Session session, String path) throws Exception {
+        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+        String command = "scp -f " + path;
+        c.setCommand(command);
+        c.connect();
+
+        try (OutputStream os = c.getOutputStream();
+             InputStream is = c.getInputStream()) {
+
+            os.write(0);
+            os.flush();
+            assertEquals("Mismatched response for command: " + command, 2, is.read());
+        } finally {
+            c.disconnect();
+        }
+    }
+
+    protected void sendFile(com.jcraft.jsch.Session session, String path, File target, String data) throws Exception {
+        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+        String command = "scp -t " + path;
+        c.setCommand(command);
+        c.connect();
+
+        try (OutputStream os = c.getOutputStream();
+             InputStream is = c.getInputStream()) {
+
+            assertAckReceived(is, command);
+
+            File parent = target.getParentFile();
+            Collection<PosixFilePermission> perms = IoUtils.getPermissions(parent.toPath());
+            String octalPerms = ScpHelper.getOctalPermissions(perms);
+            String name = target.getName();
+            assertAckReceived(os, is, "C" + octalPerms + " " + data.length() + " " + name);
+
+            os.write(data.getBytes(StandardCharsets.UTF_8));
+            os.flush();
+            assertAckReceived(is, "Sent data (length=" + data.length() + ") for " + path + "[" + name + "]");
+
+            os.write(0);
+            os.flush();
+
+            Thread.sleep(100);
+        } finally {
+            c.disconnect();
+        }
+    }
+
+    protected void assertAckReceived(OutputStream os, InputStream is, String command) throws IOException {
+        os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
+        os.flush();
+        assertAckReceived(is, command);
+    }
+
+    protected void assertAckReceived(InputStream is, String command) throws IOException {
+        assertEquals("No ACK for command=" + command, 0, is.read());
+    }
+
+    protected void sendFileError(com.jcraft.jsch.Session session, String path, String name, String data) throws Exception {
+        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+        String command = "scp -t " + path;
+        c.setCommand(command);
+        c.connect();
+
+        try (OutputStream os = c.getOutputStream();
+             InputStream is = c.getInputStream()) {
+
+            assertAckReceived(is, command);
+
+            command = "C7777 " + data.length() + " " + name;
+            os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
+            os.flush();
+            assertEquals("Mismatched response for command=" + command, 2, is.read());
+        } finally {
+            c.disconnect();
+        }
+    }
+
+    protected void sendDir(com.jcraft.jsch.Session session, String path, String dirName, String fileName, String data) throws Exception {
+        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+        String command = "scp -t -r " + path;
+        c.setCommand(command);
+        c.connect();
+
+        try (OutputStream os = c.getOutputStream();
+             InputStream is = c.getInputStream()) {
+
+            assertAckReceived(is, command);
+            assertAckReceived(os, is, "D0755 0 " + dirName);
+            assertAckReceived(os, is, "C7777 " + data.length() + " " + fileName);
+
+            os.write(data.getBytes(StandardCharsets.UTF_8));
+            os.flush();
+            assertAckReceived(is, "Send data of " + path);
+
+            os.write(0);
+            os.flush();
+
+            os.write("E\n".getBytes(StandardCharsets.UTF_8));
+            os.flush();
+            assertAckReceived(is, "Signal end of " + path);
+        } finally {
+            c.disconnect();
+        }
+    }
+
+    private static String readLine(InputStream in) throws IOException {
+        try (OutputStream baos = new ByteArrayOutputStream()) {
+            for (;;) {
+                int c = in.read();
+                if (c == '\n') {
+                    return baos.toString();
+                } else if (c == -1) {
+                    throw new IOException("End of stream");
+                } else {
+                    baos.write(c);
+                }
+            }
+        }
+    }
+
+    private static ScpClient createScpClient(ClientSession session) {
+        ScpClientCreator creator = ScpClientCreator.instance();
+        ScpTransferEventListener listener = getScpTransferEventListener(session);
+        return creator.createScpClient(session, listener);
+    }
+
+    private static ScpTransferEventListener getScpTransferEventListener(ClientSession session) {
+        return OUTPUT_DEBUG_MESSAGES ? DEBUG_LISTENER : ScpTransferEventListener.EMPTY;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java b/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java
new file mode 100644
index 0000000..d9b9b08
--- /dev/null
+++ b/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.Files;
+import java.nio.file.Path;
+
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.util.test.Utils;
+import org.apache.sshd.util.test.client.simple.BaseSimpleClientTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SimpleScpClientTest extends BaseSimpleClientTestSupport {
+    private final Path targetPath;
+    private final Path parentPath;
+    private final FileSystemFactory fileSystemFactory;
+    private SimpleScpClient scpClient;
+
+    public SimpleScpClientTest() throws Exception {
+        targetPath = detectTargetFolder();
+        parentPath = targetPath.getParent();
+        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        sshd.setCommandFactory(new ScpCommandFactory());
+        sshd.setFileSystemFactory(fileSystemFactory);
+        client.start();
+        scpClient = new SimpleScpClientImpl(simple);
+    }
+
+    @Test
+    public void testSessionClosedWhenClientClosed() throws Exception {
+        try (CloseableScpClient scp = login()) {
+            assertTrue("SCP not open", scp.isOpen());
+
+            Session session = scp.getClientSession();
+            assertTrue("Session not open", session.isOpen());
+
+            scp.close();
+            assertFalse("Session not closed", session.isOpen());
+            assertFalse("SCP not closed", scp.isOpen());
+        }
+    }
+
+    @Test
+    public void testScpUploadProxy() throws Exception {
+        try (CloseableScpClient scp = login()) {
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+            Path localFile = localDir.resolve("file.txt");
+            String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+            byte[] written = Utils.writeFile(localFile, data);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            Path remoteFile = remoteDir.resolve(localFile.getFileName());
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+            scp.upload(localFile, remotePath);
+
+            byte[] uploaded = Files.readAllBytes(remoteFile);
+            assertArrayEquals("Mismatched uploaded data", written, uploaded);
+        }
+    }
+
+    @Test
+    public void testScpDownloadProxy() throws Exception {
+        try (CloseableScpClient scp = login()) {
+            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+            Utils.deleteRecursive(scpRoot);
+
+            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+            Path remoteFile = remoteDir.resolve("file.txt");
+            String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+            byte[] written = Utils.writeFile(remoteFile, data);
+            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+            Path localFile = localDir.resolve(remoteFile.getFileName());
+            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+            scp.download(remotePath, localFile);
+
+            byte[] downloaded = Files.readAllBytes(localFile);
+            assertArrayEquals("Mismatched downloaded data", written, downloaded);
+        }
+    }
+
+    private CloseableScpClient login() throws IOException {
+        return scpClient.scpLogin(TEST_LOCALHOST, port, getCurrentTestName(), getCurrentTestName());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java b/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java
new file mode 100644
index 0000000..0cd1e91
--- /dev/null
+++ b/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.server.scp;
+
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.server.CommandFactory;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+public class ScpCommandFactoryTest extends BaseTestSupport {
+    public ScpCommandFactoryTest() {
+        super();
+    }
+
+    /**
+     * Make sure that the builder returns a factory with the default values
+     * if no {@code withXXX} method is invoked
+     */
+    @Test
+    public void testBuilderDefaultFactoryValues() {
+        ScpCommandFactory factory = new ScpCommandFactory.Builder().build();
+        assertNull("Mismatched delegate", factory.getDelegateCommandFactory());
+        assertNull("Mismatched executor", factory.getExecutorService());
+        assertEquals("Mismatched send size", ScpHelper.MIN_SEND_BUFFER_SIZE, factory.getSendBufferSize());
+        assertEquals("Mismatched receive size", ScpHelper.MIN_RECEIVE_BUFFER_SIZE, factory.getReceiveBufferSize());
+        assertFalse("Mismatched shutdown state", factory.isShutdownOnExit());
+    }
+
+    /**
+     * Make sure that the builder initializes correctly the built factory
+     */
+    @Test
+    public void testBuilderCorrectlyInitializesFactory() {
+        CommandFactory delegate = dummyFactory();
+        ExecutorService service = dummyExecutor();
+        int receiveSize = Short.MAX_VALUE;
+        int sendSize = receiveSize + Long.SIZE;
+        ScpCommandFactory factory = new ScpCommandFactory.Builder()
+                .withDelegate(delegate)
+                .withExecutorService(service)
+                .withSendBufferSize(sendSize)
+                .withReceiveBufferSize(receiveSize)
+                .withShutdownOnExit(true)
+                .build();
+        assertSame("Mismatched delegate", delegate, factory.getDelegateCommandFactory());
+        assertSame("Mismatched executor", service, factory.getExecutorService());
+        assertEquals("Mismatched send size", sendSize, factory.getSendBufferSize());
+        assertEquals("Mismatched receive size", receiveSize, factory.getReceiveBufferSize());
+        assertTrue("Mismatched shutdown state", factory.isShutdownOnExit());
+    }
+
+    /**
+     * <UL>
+     * <LI>
+     * Make sure the builder returns new instances on every call to
+     * {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
+     * </LI>
+     *
+     * <LI>
+     * Make sure values are preserved between successive invocations
+     * of the {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
+     * </LI>
+     * </UL
+     */
+    @Test
+    public void testBuilderUniqueInstance() {
+        ScpCommandFactory.Builder builder = new ScpCommandFactory.Builder();
+        ScpCommandFactory f1 = builder.withDelegate(dummyFactory()).build();
+        ScpCommandFactory f2 = builder.build();
+        assertNotSame("No new instance built", f1, f2);
+        assertSame("Mismatched delegate", f1.getDelegateCommandFactory(), f2.getDelegateCommandFactory());
+
+        ScpCommandFactory f3 = builder.withDelegate(dummyFactory()).build();
+        assertNotSame("Delegate not changed", f1.getDelegateCommandFactory(), f3.getDelegateCommandFactory());
+    }
+
+    private static ExecutorService dummyExecutor() {
+        return Mockito.mock(ExecutorService.class);
+    }
+
+    private static CommandFactory dummyFactory() {
+        return Mockito.mock(CommandFactory.class);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java b/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java
deleted file mode 100644
index 1034046..0000000
--- a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.simple;
-
-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.subsystem.sftp.SftpClient;
-import org.apache.sshd.common.util.ValidateUtils;
-
-/**
- * A simplified <U>synchronous</U> API for obtaining SFTP sessions.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface SimpleSftpClient extends Channel {
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(String host, String username, String password) throws IOException {
-        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
-    }
-
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(String host, int port, String username, String password) throws IOException {
-        return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
-    }
-
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(String host, String username, KeyPair identity) throws IOException {
-        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
-    }
-
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(String host, int port, String username, KeyPair identity) throws IOException {
-        return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
-    }
-
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(InetAddress host, String username, String password) throws IOException {
-        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
-    }
-
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(InetAddress host, int port, String username, String password) throws IOException {
-        return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, password);
-    }
-
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
-        return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
-    }
-
-    /**
-     * Creates an SFTP 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 SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    default SftpClient sftpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException {
-        return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, identity);
-    }
-
-    /**
-     * Creates an SFTP session using the provided credentials
-     *
-     * @param target The target {@link SocketAddress}
-     * @param username Username
-     * @param password Password
-     * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException;
-
-    /**
-     * Creates an SFTP session using the provided credentials
-     *
-     * @param target The target {@link SocketAddress}
-     * @param username Username
-     * @param identity The {@link KeyPair} identity
-     * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
-     * underlying session
-     * @throws IOException If failed to login or authenticate
-     */
-    SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException;
-
-}


[6/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
deleted file mode 100644
index 6b57443..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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.helpers;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.StreamCorruptedException;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.Files;
-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.FileTime;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpTargetStreamResolver;
-import org.apache.sshd.common.scp.ScpTimestamp;
-import org.apache.sshd.common.session.Session;
-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 class LocalFileScpTargetStreamResolver extends AbstractLoggingBean implements ScpTargetStreamResolver {
-    protected final Path path;
-    protected final ScpFileOpener opener;
-    protected final Boolean status;
-    private Path file;
-
-    public LocalFileScpTargetStreamResolver(Path path, ScpFileOpener opener) throws IOException {
-        LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
-        this.status = IoUtils.checkFileExists(path, linkOptions);
-        if (status == null) {
-            throw new AccessDeniedException("Receive target file path existence status cannot be determined: " + path);
-        }
-
-        this.path = path;
-        this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
-    }
-
-    @Override
-    public OutputStream resolveTargetStream(Session session, String name, long length,
-            Set<PosixFilePermission> perms, OpenOption... options) throws IOException {
-        if (file != null) {
-            throw new StreamCorruptedException("resolveTargetStream(" + name + ")[" + perms + "] already resolved: " + file);
-        }
-
-        LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
-        if (status && Files.isDirectory(path, linkOptions)) {
-            String localName = name.replace('/', File.separatorChar);   // in case we are running on Windows
-            file = path.resolve(localName);
-        } else if (status && Files.isRegularFile(path, linkOptions)) {
-            file = path;
-        } else if (!status) {
-            Path parent = path.getParent();
-
-            Boolean parentStatus = IoUtils.checkFileExists(parent, linkOptions);
-            if (parentStatus == null) {
-                throw new AccessDeniedException("Receive file parent (" + parent + ") existence status cannot be determined for " + path);
-            }
-
-            if (parentStatus && Files.isDirectory(parent, linkOptions)) {
-                file = path;
-            }
-        }
-
-        if (file == null) {
-            throw new IOException("Can not write to " + path);
-        }
-
-        Boolean fileStatus = IoUtils.checkFileExists(file, linkOptions);
-        if (fileStatus == null) {
-            throw new AccessDeniedException("Receive file existence status cannot be determined: " + file);
-        }
-
-        if (fileStatus) {
-            if (Files.isDirectory(file, linkOptions)) {
-                throw new IOException("File is a directory: " + file);
-            }
-
-            if (!Files.isWritable(file)) {
-                throw new IOException("Can not write to file: " + file);
-            }
-        }
-
-        if (log.isTraceEnabled()) {
-            log.trace("resolveTargetStream(" + name + "): " + file);
-        }
-
-        return opener.openWrite(session, file, options);
-    }
-
-    @Override
-    public Path getEventListenerFilePath() {
-        if (file == null) {
-            return path;
-        } else {
-            return file;
-        }
-    }
-
-    @Override
-    public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
-        if (file == null) {
-            throw new StreamCorruptedException("postProcessReceivedData(" + name + ")[" + perms + "] No currently resolved data");
-        }
-
-        if (preserve) {
-            updateFileProperties(name, file, perms, time);
-        }
-    }
-
-    protected void updateFileProperties(String name, Path path, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
-        boolean traceEnabled = log.isTraceEnabled();
-        if (traceEnabled) {
-            log.trace("updateFileProperties(" + name + ")[" + path + "] permissions: " + perms);
-        }
-        IoUtils.setPermissions(path, perms);
-
-        if (time != null) {
-            BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class);
-            FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS);
-            FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS);
-            if (traceEnabled) {
-                log.trace("updateFileProperties(" + name + ")[" + path + "] last-modified=" + lastModified + ", last-access=" + lastAccess);
-            }
-
-            view.setTimes(lastModified, lastAccess, null);
-        }
-    }
-
-    @Override
-    public String toString() {
-        return String.valueOf(getEventListenerFilePath());
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java
new file mode 100644
index 0000000..a55ac2e
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.util.io.functors;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Invokes some I/O function on the input returning some output
+ * and potentially throwing an {@link IOException} in the process
+ *
+ * @param <T> Type of input
+ * @param <R> Type of output
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface IOFunction<T, R> {
+    R apply(T t) throws IOException;
+
+    /**
+     * Returns a composed function that first applies the {@code before}
+     * function to its input, and then applies this function to the result.
+     * If evaluation of either function throws an exception, it is relayed to
+     * the caller of the composed function.
+     *
+     * @param <V> the type of input to the {@code before} function, and to the
+     *           composed function
+     * @param before the function to apply before this function is applied
+     * @return a composed function that first applies the {@code before}
+     * function and then applies this function
+     * @throws NullPointerException if before is null
+     *
+     * @see #andThen(IOFunction)
+     */
+    default <V> IOFunction<V, R> compose(IOFunction<? super V, ? extends T> before) {
+        Objects.requireNonNull(before, "No composing function provided");
+        return (V v) -> apply(before.apply(v));
+    }
+
+    /**
+     * Returns a composed function that first applies this function to
+     * its input, and then applies the {@code after} function to the result.
+     * If evaluation of either function throws an exception, it is relayed to
+     * the caller of the composed function.
+     *
+     * @param <V> the type of output of the {@code after} function, and of the
+     *           composed function
+     * @param after the function to apply after this function is applied
+     * @return a composed function that first applies this function and then
+     * applies the {@code after} function
+     * @throws NullPointerException if after is null
+     *
+     * @see #compose(IOFunction)
+     */
+    default <V> IOFunction<T, V> andThen(IOFunction<? super R, ? extends V> after) {
+        Objects.requireNonNull(after, "No composing function provided");
+        return (T t) -> after.apply(apply(t));
+    }
+
+    /**
+     * Returns a function that always returns its input argument.
+     *
+     * @param <T> the type of the input and output objects to the function
+     * @return a function that always returns its input argument
+     */
+    static <T> IOFunction<T, T> identity() {
+        return t -> t;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
deleted file mode 100644
index e80f791..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * 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.server.scp;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.FileSystem;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-import org.apache.sshd.common.file.FileSystemAware;
-import org.apache.sshd.common.scp.ScpException;
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionHolder;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
-import org.apache.sshd.common.util.threads.ThreadUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
-import org.apache.sshd.server.session.ServerSession;
-import org.apache.sshd.server.session.ServerSessionHolder;
-
-/**
- * This commands provide SCP support on both server and client side.
- * Permissions and preservation of access / modification times on files
- * are not supported.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ScpCommand
-        extends AbstractLoggingBean
-        implements Command, Runnable, FileSystemAware, SessionAware,
-                   SessionHolder<Session>, ServerSessionHolder, ExecutorServiceCarrier {
-
-    protected final String name;
-    protected final int sendBufferSize;
-    protected final int receiveBufferSize;
-    protected final ScpFileOpener opener;
-    protected boolean optR;
-    protected boolean optT;
-    protected boolean optF;
-    protected boolean optD;
-    protected boolean optP; // TODO: handle modification times
-    protected FileSystem fileSystem;
-    protected String path;
-    protected InputStream in;
-    protected OutputStream out;
-    protected OutputStream err;
-    protected ExitCallback callback;
-    protected IOException error;
-    protected Future<?> pendingFuture;
-    protected ScpTransferEventListener listener;
-    protected ServerSession serverSession;
-
-    private ExecutorService executorService;
-    private boolean shutdownOnExit;
-
-    /**
-     * @param command         The command to be executed
-     * @param executorService An {@link ExecutorService} to be used when
-     *                        {@link #start(Environment)}-ing execution. If {@code null} an ad-hoc
-     *                        single-threaded service is created and used.
-     * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()}
-     *                        will be called when command terminates - unless it is the ad-hoc
-     *                        service, which will be shutdown regardless
-     * @param sendSize        Size (in bytes) of buffer to use when sending files
-     * @param receiveSize     Size (in bytes) of buffer to use when receiving files
-     * @param fileOpener      The {@link ScpFileOpener} - if {@code null} then {@link DefaultScpFileOpener} is used
-     * @param eventListener   An {@link ScpTransferEventListener} - may be {@code null}
-     * @see ThreadUtils#newSingleThreadExecutor(String)
-     * @see ScpHelper#MIN_SEND_BUFFER_SIZE
-     * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
-     */
-    public ScpCommand(String command,
-            ExecutorService executorService, boolean shutdownOnExit,
-            int sendSize, int receiveSize,
-            ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
-        name = command;
-
-        if (executorService == null) {
-            String poolName = command.replace(' ', '_').replace('/', ':');
-            this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
-            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
-        } else {
-            this.executorService = executorService;
-            this.shutdownOnExit = shutdownOnExit;
-        }
-
-        if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
-            throw new IllegalArgumentException("<ScpCommmand>(" + command + ") send buffer size "
-                    + "(" + sendSize + ") below minimum required "
-                    + "(" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
-        }
-        sendBufferSize = sendSize;
-
-        if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
-            throw new IllegalArgumentException("<ScpCommmand>(" + command + ") receive buffer size "
-                    + "(" + sendSize + ") below minimum required "
-                    + "(" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
-        }
-        receiveBufferSize = receiveSize;
-
-        opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
-        listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
-
-        boolean debugEnabled = log.isDebugEnabled();
-        if (debugEnabled) {
-            log.debug("Executing command {}", command);
-        }
-
-        String[] args = GenericUtils.split(command, ' ');
-        int numArgs = GenericUtils.length(args);
-        for (int i = 1; i < numArgs; i++) {
-            String argVal = args[i];
-            if (argVal.charAt(0) == '-') {
-                for (int j = 1; j < argVal.length(); j++) {
-                    char option = argVal.charAt(j);
-                    switch (option) {
-                        case 'f':
-                            optF = true;
-                            break;
-                        case 'p':
-                            optP = true;
-                            break;
-                        case 'r':
-                            optR = true;
-                            break;
-                        case 't':
-                            optT = true;
-                            break;
-                        case 'd':
-                            optD = true;
-                            break;
-                        default:  // ignored
-                            if (debugEnabled) {
-                                log.debug("Unknown flag ('{}') in command={}", option, command);
-                            }
-                    }
-                }
-            } else {
-                String prevArg = args[i - 1];
-                path = command.substring(command.indexOf(prevArg) + prevArg.length() + 1);
-
-                int pathLen = path.length();
-                char startDelim = path.charAt(0);
-                char endDelim = (pathLen > 2) ? path.charAt(pathLen - 1) : '\0';
-                // remove quotes
-                if ((pathLen > 2) && (startDelim == endDelim) && ((startDelim == '\'') || (startDelim == '"'))) {
-                    path = path.substring(1, pathLen - 1);
-                }
-                break;
-            }
-        }
-
-        if ((!optF) && (!optT)) {
-            error = new IOException("Either -f or -t option should be set for " + command);
-        }
-    }
-
-    @Override
-    public ExecutorService getExecutorService() {
-        return executorService;
-    }
-
-    @Override
-    public boolean isShutdownOnExit() {
-        return shutdownOnExit;
-    }
-
-    @Override
-    public Session getSession() {
-        return getServerSession();
-    }
-
-    @Override
-    public ServerSession getServerSession() {
-        return serverSession;
-    }
-
-    @Override
-    public void setSession(ServerSession session) {
-        serverSession = session;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void setFileSystem(FileSystem fs) {
-        this.fileSystem = fs;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        if (error != null) {
-            throw error;
-        }
-
-        try {
-            ExecutorService executors = getExecutorService();
-            pendingFuture = executors.submit(this);
-        } catch (RuntimeException e) {    // e.g., RejectedExecutionException
-            log.error("Failed (" + e.getClass().getSimpleName() + ") to start command=" + name + ": " + e.getMessage(), e);
-            throw new IOException(e);
-        }
-    }
-
-    @Override
-    public void destroy() {
-        // if thread has not completed, cancel it
-        boolean debugEnabled = log.isDebugEnabled();
-        if ((pendingFuture != null) && (!pendingFuture.isDone())) {
-            boolean result = pendingFuture.cancel(true);
-            // TODO consider waiting some reasonable (?) amount of time for cancellation
-            if (debugEnabled) {
-                log.debug("destroy() - cancel pending future=" + result);
-            }
-        }
-
-        pendingFuture = null;
-
-        ExecutorService executors = getExecutorService();
-        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
-            Collection<Runnable> runners = executors.shutdownNow();
-            if (debugEnabled) {
-                log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
-            }
-        }
-        this.executorService = null;
-
-        try {
-            fileSystem.close();
-        } catch (UnsupportedOperationException e) {
-            // Ignore
-        } catch (IOException e) {
-            log.debug("Error closing FileSystem", e);
-        }
-    }
-
-    @Override
-    public void run() {
-        int exitValue = ScpHelper.OK;
-        String exitMessage = null;
-        ScpHelper helper = new ScpHelper(getServerSession(), in, out, fileSystem, opener, listener);
-        try {
-            if (optT) {
-                helper.receive(helper.resolveLocalPath(path), optR, optD, optP, receiveBufferSize);
-            } else if (optF) {
-                helper.send(Collections.singletonList(path), optR, optP, sendBufferSize);
-            } else {
-                throw new IOException("Unsupported mode");
-            }
-        } catch (IOException e) {
-            ServerSession session = getServerSession();
-            boolean debugEnabled = log.isDebugEnabled();
-            try {
-                Integer statusCode = null;
-                if (e instanceof ScpException) {
-                    statusCode = ((ScpException) e).getExitStatus();
-                }
-                exitValue = (statusCode == null) ? ScpHelper.ERROR : statusCode;
-                // this is an exception so status cannot be OK/WARNING
-                if ((exitValue == ScpHelper.OK) || (exitValue == ScpHelper.WARNING)) {
-                    if (debugEnabled) {
-                        log.debug("run({})[{}] normalize status code={}", session, name, exitValue);
-                    }
-                    exitValue = ScpHelper.ERROR;
-                }
-                exitMessage = GenericUtils.trimToEmpty(e.getMessage());
-                writeCommandResponseMessage(name, exitValue, exitMessage);
-            } catch (IOException e2) {
-                if (debugEnabled) {
-                    log.debug("run({})[{}] Failed ({}) to send error response: {}",
-                              session, name, e.getClass().getSimpleName(), e.getMessage());
-                }
-                if (log.isTraceEnabled()) {
-                    log.trace("run(" + session + ")[" + name + "] error response failure details", e2);
-                }
-            }
-
-            if (debugEnabled) {
-                log.debug("run({})[{}] Failed ({}) to run command: {}",
-                          session, name, e.getClass().getSimpleName(), e.getMessage());
-            }
-            if (log.isTraceEnabled()) {
-                log.trace("run(" + session + ")[" + name + "] command execution failure details", e);
-            }
-        } finally {
-            if (callback != null) {
-                callback.onExit(exitValue, GenericUtils.trimToEmpty(exitMessage));
-            }
-        }
-    }
-
-    protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("writeCommandResponseMessage({}) command='{}', exit-status={}: {}",
-                      getServerSession(), command, exitValue, exitMessage);
-        }
-        ScpHelper.sendResponseMessage(out, exitValue, exitMessage);
-    }
-
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + "(" + getSession() + ") " + name;
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
deleted file mode 100644
index 99e3e34..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
+++ /dev/null
@@ -1,272 +0,0 @@
-/*
- * 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.server.scp;
-
-import java.util.Collection;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.ExecutorService;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpFileOpenerHolder;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-import org.apache.sshd.common.util.EventListenerUtils;
-import org.apache.sshd.common.util.ObjectBuilder;
-import org.apache.sshd.common.util.threads.ExecutorServiceConfigurer;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.CommandFactory;
-
-/**
- * This <code>CommandFactory</code> can be used as a standalone command factory
- * or can be used to augment another <code>CommandFactory</code> and provides
- * <code>SCP</code> support.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- * @see ScpCommand
- */
-public class ScpCommandFactory
-        implements ScpFileOpenerHolder,
-        CommandFactory,
-        Cloneable,
-        ExecutorServiceConfigurer {
-    /**
-     * A useful {@link ObjectBuilder} for {@link ScpCommandFactory}
-     */
-    public static class Builder implements ObjectBuilder<ScpCommandFactory> {
-        private final ScpCommandFactory factory = new ScpCommandFactory();
-
-        public Builder() {
-            super();
-        }
-
-        public Builder withFileOpener(ScpFileOpener opener) {
-            factory.setScpFileOpener(opener);
-            return this;
-        }
-
-        public Builder withDelegate(CommandFactory delegate) {
-            factory.setDelegateCommandFactory(delegate);
-            return this;
-        }
-
-        public Builder withExecutorService(ExecutorService service) {
-            factory.setExecutorService(service);
-            return this;
-        }
-
-        public Builder withShutdownOnExit(boolean shutdown) {
-            factory.setShutdownOnExit(shutdown);
-            return this;
-        }
-
-        public Builder withSendBufferSize(int sendSize) {
-            factory.setSendBufferSize(sendSize);
-            return this;
-        }
-
-        public Builder withReceiveBufferSize(int receiveSize) {
-            factory.setReceiveBufferSize(receiveSize);
-            return this;
-        }
-
-        public Builder addEventListener(ScpTransferEventListener listener) {
-            factory.addEventListener(listener);
-            return this;
-        }
-
-        public Builder removeEventListener(ScpTransferEventListener listener) {
-            factory.removeEventListener(listener);
-            return this;
-        }
-
-        @Override
-        public ScpCommandFactory build() {
-            return factory.clone();
-        }
-    }
-
-    /*
-     * NOTE: we expose setters since there is no problem to change these settings between
-     * successive invocations of the 'createCommand' method
-     */
-    private CommandFactory delegate;
-    private ExecutorService executors;
-    private boolean shutdownExecutor;
-    private ScpFileOpener fileOpener;
-    private int sendBufferSize = ScpHelper.MIN_SEND_BUFFER_SIZE;
-    private int receiveBufferSize = ScpHelper.MIN_RECEIVE_BUFFER_SIZE;
-    private Collection<ScpTransferEventListener> listeners = new CopyOnWriteArraySet<>();
-    private ScpTransferEventListener listenerProxy;
-
-    public ScpCommandFactory() {
-        listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), listeners);
-    }
-
-    @Override
-    public ScpFileOpener getScpFileOpener() {
-        return fileOpener;
-    }
-
-    @Override
-    public void setScpFileOpener(ScpFileOpener fileOpener) {
-        this.fileOpener = fileOpener;
-    }
-
-    public CommandFactory getDelegateCommandFactory() {
-        return delegate;
-    }
-
-    /**
-     * @param factory A {@link CommandFactory} to be used if the
-     * command is not an SCP one. If {@code null} then an {@link IllegalArgumentException}
-     * will be thrown when attempting to invoke {@link #createCommand(String)}
-     * with a non-SCP command
-     */
-    public void setDelegateCommandFactory(CommandFactory factory) {
-        delegate = factory;
-    }
-
-    @Override
-    public ExecutorService getExecutorService() {
-        return executors;
-    }
-
-    /**
-     * @param service An {@link ExecutorService} to be used when
-     * starting {@link ScpCommand} execution. If {@code null} then a single-threaded
-     * ad-hoc service is used. <B>Note:</B> the service will <U>not</U> be shutdown
-     * when the command is terminated - unless it is the ad-hoc service, which will be
-     * shutdown regardless
-     */
-    @Override
-    public void setExecutorService(ExecutorService service) {
-        executors = service;
-    }
-
-    @Override
-    public boolean isShutdownOnExit() {
-        return shutdownExecutor;
-    }
-
-    @Override
-    public void setShutdownOnExit(boolean shutdown) {
-        shutdownExecutor = shutdown;
-    }
-
-    public int getSendBufferSize() {
-        return sendBufferSize;
-    }
-
-    /**
-     * @param sendSize Size (in bytes) of buffer to use when sending files
-     * @see ScpHelper#MIN_SEND_BUFFER_SIZE
-     */
-    public void setSendBufferSize(int sendSize) {
-        if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
-            throw new IllegalArgumentException("<ScpCommandFactory>() send buffer size "
-                    + "(" + sendSize + ") below minimum required (" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
-        }
-        sendBufferSize = sendSize;
-    }
-
-    public int getReceiveBufferSize() {
-        return receiveBufferSize;
-    }
-
-    /**
-     * @param receiveSize Size (in bytes) of buffer to use when receiving files
-     * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
-     */
-    public void setReceiveBufferSize(int receiveSize) {
-        if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
-            throw new IllegalArgumentException("<ScpCommandFactory>() receive buffer size "
-                    + "(" + receiveSize + ") below minimum required (" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
-        }
-        receiveBufferSize = receiveSize;
-    }
-
-    /**
-     * @param listener The {@link ScpTransferEventListener} to add
-     * @return {@code true} if this is a <U>new</U> listener instance,
-     * {@code false} if the listener is already registered
-     * @throws IllegalArgumentException if {@code null} listener
-     */
-    public boolean addEventListener(ScpTransferEventListener listener) {
-        if (listener == null) {
-            throw new IllegalArgumentException("No listener instance");
-        }
-
-        return listeners.add(listener);
-    }
-
-    /**
-     * @param listener The {@link ScpTransferEventListener} to remove
-     * @return {@code true} if the listener was registered and removed,
-     * {@code false} if the listener was not registered to begin with
-     * @throws IllegalArgumentException if {@code null} listener
-     */
-    public boolean removeEventListener(ScpTransferEventListener listener) {
-        if (listener == null) {
-            throw new IllegalArgumentException("No listener instance");
-        }
-
-        return listeners.remove(listener);
-    }
-
-    /**
-     * Parses a command string and verifies that the basic syntax is
-     * correct. If parsing fails the responsibility is delegated to
-     * the configured {@link CommandFactory} instance; if one exist.
-     *
-     * @param command command to parse
-     * @return configured {@link Command} instance
-     * @throws IllegalArgumentException if not an SCP command and no
-     *                                  delegate command factory is available
-     * @see ScpHelper#SCP_COMMAND_PREFIX
-     */
-    @Override
-    public Command createCommand(String command) {
-        if (command.startsWith(ScpHelper.SCP_COMMAND_PREFIX)) {
-            return new ScpCommand(command,
-                    getExecutorService(), isShutdownOnExit(),
-                    getSendBufferSize(), getReceiveBufferSize(),
-                    getScpFileOpener(), listenerProxy);
-        }
-
-        CommandFactory factory = getDelegateCommandFactory();
-        if (factory != null) {
-            return factory.createCommand(command);
-        }
-
-        throw new IllegalArgumentException("Unknown command, does not begin with '" + ScpHelper.SCP_COMMAND_PREFIX + "': " + command);
-    }
-
-    @Override
-    public ScpCommandFactory clone() {
-        try {
-            ScpCommandFactory other = getClass().cast(super.clone());
-            // clone the listeners set as well
-            other.listeners = new CopyOnWriteArraySet<>(this.listeners);
-            other.listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), other.listeners);
-            return other;
-        } catch (CloneNotSupportedException e) {
-            throw new RuntimeException(e);    // un-expected...
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java
deleted file mode 100644
index d847291..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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.server.scp;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.Objects;
-
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
-
-/**
- * Implementation of an unknown command that can be returned by <code>CommandFactory</code>
- * when the command is not known, as it is supposed to always
- * return a valid <code>Command</code> object.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class UnknownCommand implements Command, Runnable {
-
-    private final String command;
-    private final String message;
-    @SuppressWarnings("unused")
-    private InputStream in;
-    @SuppressWarnings("unused")
-    private OutputStream out;
-    private OutputStream err;
-    private ExitCallback callback;
-
-    public UnknownCommand(String command) {
-        this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
-        this.message = "Unknown command: " + command;
-    }
-
-    public String getCommand() {
-        return command;
-    }
-
-    public String getMessage() {
-        return message;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void run() {
-        String errorMessage = getMessage();
-        try {
-            try {
-                err.write(errorMessage.getBytes(StandardCharsets.UTF_8));
-                err.write('\n');
-            } finally {
-                err.flush();
-            }
-        } catch (IOException e) {
-            // ignored
-        }
-
-        if (callback != null) {
-            callback.onExit(1, errorMessage);
-        }
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        Thread thread = new Thread(this);
-        thread.setDaemon(true);
-        thread.start();
-    }
-
-    @Override
-    public void destroy() {
-        // ignored
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(getCommand());
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == null) {
-            return false;
-        }
-        if (obj == this) {
-            return true;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-
-        return Objects.equals(this.getCommand(), ((UnknownCommand) obj).getCommand());
-    }
-
-    @Override
-    public String toString() {
-        return getMessage();
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java
new file mode 100644
index 0000000..59d970c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java
@@ -0,0 +1,138 @@
+/*
+ * 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.server.shell;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+
+/**
+ * Implementation of an unknown command that can be returned by <code>CommandFactory</code>
+ * when the command is not known, as it is supposed to always
+ * return a valid <code>Command</code> object.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class UnknownCommand implements Command, Runnable {
+
+    private final String command;
+    private final String message;
+    @SuppressWarnings("unused")
+    private InputStream in;
+    @SuppressWarnings("unused")
+    private OutputStream out;
+    private OutputStream err;
+    private ExitCallback callback;
+
+    public UnknownCommand(String command) {
+        this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
+        this.message = "Unknown command: " + command;
+    }
+
+    public String getCommand() {
+        return command;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    @Override
+    public void setInputStream(InputStream in) {
+        this.in = in;
+    }
+
+    @Override
+    public void setOutputStream(OutputStream out) {
+        this.out = out;
+    }
+
+    @Override
+    public void setErrorStream(OutputStream err) {
+        this.err = err;
+    }
+
+    @Override
+    public void setExitCallback(ExitCallback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    public void run() {
+        String errorMessage = getMessage();
+        try {
+            try {
+                err.write(errorMessage.getBytes(StandardCharsets.UTF_8));
+                err.write('\n');
+            } finally {
+                err.flush();
+            }
+        } catch (IOException e) {
+            // ignored
+        }
+
+        if (callback != null) {
+            callback.onExit(1, errorMessage);
+        }
+    }
+
+    @Override
+    public void start(Environment env) throws IOException {
+        Thread thread = new Thread(this);
+        thread.setDaemon(true);
+        thread.start();
+    }
+
+    @Override
+    public void destroy() {
+        // ignored
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(getCommand());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        return Objects.equals(this.getCommand(), ((UnknownCommand) obj).getCommand());
+    }
+
+    @Override
+    public String toString() {
+        return getMessage();
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java
new file mode 100644
index 0000000..871ca60
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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.server.shell;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class UnknownCommandFactory implements CommandFactory {
+    public static final UnknownCommandFactory INSTANCE = new UnknownCommandFactory();
+
+    public UnknownCommandFactory() {
+        super();
+    }
+
+    @Override
+    public Command createCommand(String command) {
+        return new UnknownCommand(command);
+    }
+}


[8/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Posted by lg...@apache.org.
[SSHD-818] Split SCP code (client + server) to its own module


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/af415e5f
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/af415e5f
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/af415e5f

Branch: refs/heads/master
Commit: af415e5fec5fadf1c397371313cec20d3b4428f4
Parents: cb8982a
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Mon Apr 23 16:32:34 2018 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Wed Apr 25 08:01:09 2018 +0300

----------------------------------------------------------------------
 README.md                                       |  146 ++-
 assembly/pom.xml                                |    5 +
 pom.xml                                         |    1 +
 sshd-cli/pom.xml                                |    5 +
 .../apache/sshd/cli/client/ScpCommandMain.java  |   57 +-
 .../apache/sshd/cli/server/SshServerMain.java   |    1 -
 sshd-contrib/pom.xml                            |    5 +
 ...SimpleAccessControlScpEventListenerTest.java |    4 +-
 .../sshd/client/ClientFactoryManager.java       |    2 -
 .../java/org/apache/sshd/client/SshClient.java  |   12 -
 .../sshd/client/scp/AbstractScpClient.java      |  278 ----
 .../sshd/client/scp/CloseableScpClient.java     |   32 -
 .../sshd/client/scp/DefaultScpClient.java       |  159 ---
 .../client/scp/DefaultScpStreamResolver.java    |   88 --
 .../org/apache/sshd/client/scp/ScpClient.java   |  174 ---
 .../sshd/client/scp/ScpClientCreator.java       |   97 --
 .../client/session/AbstractClientSession.java   |   31 -
 .../sshd/client/session/ClientSession.java      |    6 +-
 .../client/simple/AbstractSimpleClient.java     |  106 --
 .../apache/sshd/client/simple/SimpleClient.java |    1 -
 .../sshd/client/simple/SimpleScpClient.java     |  159 ---
 ...AbstractScpTransferEventListenerAdapter.java |   74 --
 .../apache/sshd/common/scp/ScpException.java    |   56 -
 .../apache/sshd/common/scp/ScpFileOpener.java   |  284 -----
 .../sshd/common/scp/ScpFileOpenerHolder.java    |   37 -
 .../org/apache/sshd/common/scp/ScpHelper.java   |  837 ------------
 .../org/apache/sshd/common/scp/ScpLocation.java |  227 ----
 .../sshd/common/scp/ScpReceiveLineHandler.java  |   36 -
 .../common/scp/ScpSourceStreamResolver.java     |   73 --
 .../common/scp/ScpTargetStreamResolver.java     |   67 -
 .../apache/sshd/common/scp/ScpTimestamp.java    |   69 -
 .../common/scp/ScpTransferEventListener.java    |  105 --
 .../scp/helpers/DefaultScpFileOpener.java       |   75 --
 .../LocalFileScpSourceStreamResolver.java       |   97 --
 .../LocalFileScpTargetStreamResolver.java       |  159 ---
 .../common/util/io/functors/IOFunction.java     |   86 ++
 .../org/apache/sshd/server/scp/ScpCommand.java  |  350 -----
 .../sshd/server/scp/ScpCommandFactory.java      |  272 ----
 .../apache/sshd/server/scp/UnknownCommand.java  |  138 --
 .../sshd/server/shell/UnknownCommand.java       |  138 ++
 .../server/shell/UnknownCommandFactory.java     |   39 +
 .../org/apache/sshd/client/scp/ScpTest.java     | 1197 -----------------
 .../simple/BaseSimpleClientTestSupport.java     |   70 -
 .../sshd/client/simple/SimpleScpClientTest.java |  119 --
 .../client/simple/SimpleSessionClientTest.java  |    1 +
 .../server/command/ScpCommandFactoryTest.java   |  114 --
 .../sshd/util/test/UnknownCommandFactory.java   |   40 -
 .../java/org/apache/sshd/util/test/Utils.java   |    1 +
 .../simple/BaseSimpleClientTestSupport.java     |   71 ++
 .../sshd/git/AbstractGitCommandFactory.java     |    2 +-
 sshd-scp/pom.xml                                |  201 +++
 .../sshd/client/scp/AbstractScpClient.java      |  278 ++++
 .../client/scp/AbstractScpClientCreator.java    |   63 +
 .../sshd/client/scp/CloseableScpClient.java     |   32 +
 .../sshd/client/scp/DefaultScpClient.java       |  159 +++
 .../client/scp/DefaultScpClientCreator.java     |   42 +
 .../client/scp/DefaultScpStreamResolver.java    |   88 ++
 .../org/apache/sshd/client/scp/ScpClient.java   |  174 +++
 .../sshd/client/scp/ScpClientCreator.java       |  106 ++
 .../apache/sshd/client/scp/SimpleScpClient.java |  178 +++
 .../sshd/client/scp/SimpleScpClientImpl.java    |  153 +++
 ...AbstractScpTransferEventListenerAdapter.java |   74 ++
 .../apache/sshd/common/scp/ScpException.java    |   56 +
 .../apache/sshd/common/scp/ScpFileOpener.java   |  284 +++++
 .../sshd/common/scp/ScpFileOpenerHolder.java    |   37 +
 .../org/apache/sshd/common/scp/ScpHelper.java   |  837 ++++++++++++
 .../org/apache/sshd/common/scp/ScpLocation.java |  227 ++++
 .../sshd/common/scp/ScpReceiveLineHandler.java  |   36 +
 .../common/scp/ScpSourceStreamResolver.java     |   73 ++
 .../common/scp/ScpTargetStreamResolver.java     |   67 +
 .../apache/sshd/common/scp/ScpTimestamp.java    |   69 +
 .../common/scp/ScpTransferEventListener.java    |  105 ++
 .../scp/helpers/DefaultScpFileOpener.java       |   75 ++
 .../LocalFileScpSourceStreamResolver.java       |   97 ++
 .../LocalFileScpTargetStreamResolver.java       |  159 +++
 .../org/apache/sshd/server/scp/ScpCommand.java  |  350 +++++
 .../sshd/server/scp/ScpCommandFactory.java      |  272 ++++
 .../org/apache/sshd/client/scp/ScpTest.java     | 1203 ++++++++++++++++++
 .../sshd/client/scp/SimpleScpClientTest.java    |  121 ++
 .../sshd/server/scp/ScpCommandFactoryTest.java  |  113 ++
 .../sshd/client/simple/SimpleSftpClient.java    |  179 ---
 .../client/simple/SimpleSftpClientImpl.java     |  171 ---
 .../client/subsystem/sftp/SimpleSftpClient.java |  179 +++
 .../sftp/impl/SimpleSftpClientImpl.java         |  192 +++
 .../java/org/apache/sshd/client/ClientTest.java |  426 -------
 .../simple/BaseSimpleClientTestSupport.java     |   70 -
 .../client/simple/SimpleSftpClientTest.java     |  129 --
 .../sftp/AbstractSftpClientTestSupport.java     |    2 -
 .../sshd/client/subsystem/sftp/ClientTest.java  |  424 ++++++
 .../subsystem/sftp/SftpFileSystemTest.java      |    2 -
 .../subsystem/sftp/SimpleSftpClientTest.java    |  128 ++
 .../sftp/ApacheSshdSftpSessionFactoryTest.java  |    2 -
 92 files changed, 7168 insertions(+), 6668 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index f512a3f..5a1e249 100644
--- a/README.md
+++ b/README.md
@@ -533,17 +533,80 @@ it will be **closed** automatically when the stream using it is closed.
 
 ## SCP
 
-Besides the `ScpTransferEventListener`, the SCP module also uses a `ScpFileOpener` instance in order to access
-the local files - client or server-side. The default implementation simply opens an [InputStream](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html)
+Both client-side and server-side SCP are supported. Starting from version 2.0, the SCP related code is located in the `sshd-scp` module, so you need
+to add this additional dependency to your maven project:
+
+```xml
+
+    <dependency>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd-sscp</artifactId>
+        <version>...same as sshd-core...</version>
+    </dependency>
+
+```
+
+### Client-side SCP
+
+In order to obtain an `ScpClient` one needs to use an `ScpClientCreator`:
+
+```java
+
+ClientSession session = ... obtain an instance ...
+ScpClientCreator creator = ... obtain an instance ...
+ScpClient client = creator.createScpClient(session);
+
+```
+
+A default `ScpClientCreator` instance is provided as part of the module - see `ScpClientCreator.instance()`
+
+#### ScpFileOpener(s)
+
+As part of the `ScpClientCreator`, the SCP module also uses a `ScpFileOpener` instance in order to access
+the local files. The default implementation simply opens an [InputStream](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html)
 or [OutputStream](https://docs.oracle.com/javase/8/docs/api/java/io/OutputStream.html) on the requested local path. However,
-the user may replace it and intercept the calls - e.g., for logging, for wrapping/filtering the streams, etc... **Note:**
-due to SCP protocol limitations one cannot change the **size** of the input/output since it is passed as part of the command
+the user may replace it and intercept the calls - e.g., for logging, for wrapping/filtering the streams, etc... The user may
+attach a default opener that will be automatically attached to **all** clients created unless specifically overridden:
+
+```java
+
+ClientSession session = ... obtain an instance ...
+ScpClientCreator creator = ... obtain an instance ...
+creator.setScpFileOpener(new MySuperDuperOpener());
+
+ScpClient client1 = creator.createScpClient(session);   // <<== automatically uses MySuperDuperOpener
+ScpClient client2 = creator.createScpClient(session, new SomeOtherOpener());   // <<== uses SomeOtherOpener instead of MySuperDuperOpener
+
+```
+
+**Note:** due to SCP protocol limitations one cannot change the **size** of the input/output since it is passed as part of the command
 **before** the file opener is invoked - so there are a few limitations on what one can do within this interface implementation.
 
+#### ScpTransferEventListener(s)
+
+The `ScpClientCreator` can also be used to attach a default `ScpTransferEventListener` that will be attached to
+**all** created SCP client instances through that creator - unless specifically overridden:
+
+```java
+
+ClientSession session = ... obtain an instance ...
+ScpClientCreator creator = ... obtain an instance ...
+creator.setScpTransferEventListener(new MySuperDuperListener());
+
+ScpClient client1 = creator.createScpClient(session);   // <<== automatically uses MySuperDuperListener
+ScpClient client2 = creator.createScpClient(session, new SomeOtherListener());   // <<== uses SomeOtherListener instead of MySuperDuperListener
+
+```
+
+### Server-side SCP
+
+The `ScpCommandFactory` allows users to attach an `ScpFileOpener` and/or `ScpTransferEventListener` having the same behavior as the client - i.e.,
+monitoring and intervention on the accessed local files.
 
 ## SFTP
 
-Both client-side and server-side SFTP are supported.  Starting from SSHD 1.8.0, the SFTP related code is located in the `sshd-sftp`, so you need to add this additional dependency to your maven project:
+Both client-side and server-side SFTP are supported. Starting from version 2.0, the SFTP related code is located in the `sshd-sftp`, so you need to add
+this additional dependency to your maven project:
 
 ```xml
 
@@ -567,19 +630,48 @@ On the server side, the following code needs to be added:
 
 ```
 
+### `SftpEventListener`
+
+(See above more details...) - users may register an `SftpEventListener` (or more...) in the `SftpSubsystemFactory` in
+order to monitor and even intervene in the susbsytem's functionality.
+
 ### Client-side SFTP
 
+In order to obtain an `SftpClient` instance one needs to use an `SftpClientFactory`:
+
+
 ```java
 
-    SftpClient client = SftpClientFactory.instance().createSftpClient(session);
+    ClientSession session = ...obtain session...
+    SftpClientFactory factory = ...obtain factory...
+    SftpClient client = factory.createSftpClient(session);
 
 ```
 
-### `SftpEventListener`
+A default client factory implementations is provided in the module - see `SftpClientFactory.instance()`
+
+
+### Using a custom `SftpClientFactory`
+
+The code creates `SftpClient`-s and `SftpFileSystem`-s using a default built-in `SftpClientFactory` instance (see
+`DefaultSftpClientFactory`). Users may choose to use a custom factory in order to provide their own
+implementations - e.g., in order to override some default behavior - e.g.:
+
+```java
+
+    SshClient client = ... setup client...
+
+    try (ClientSession session = client.connect(user, host, port).verify(timeout).getSession()) {
+        session.addPasswordIdentity(password);
+        session.auth.verify(timeout);
 
-See above...
-In addition to the `SftpEventListener` there are a few more SFTP-related special interfaces and modules.
+        // User-specific factory
+        try (SftpClient sftp = MySpecialSessionSftpClientFactory.INSTANCE.createSftpClient(session)) {
+            ... instance created through SpecialSessionSftpClientFactory ...
+        }
+    }
 
+```
 
 ### Version selection via `SftpVersionSelector`
 
@@ -603,7 +695,8 @@ range.
         session.addPasswordIdentity(password);
         session.auth.verify(timeout);
 
-        try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session, myVersionSelector)) {
+        SftpClientFactory factory = SftpClientFactory.instance();
+        try (SftpClient sftp = factory.createSftpClient(session, myVersionSelector)) {
             ... do SFTP related stuff...
         }
     }
@@ -615,30 +708,6 @@ the version, and all we can do at the server is require a **specific** version v
 configuration key. For more advanced restrictions one needs to sub-class `SftpSubSystem` and provide a non-default
 `SftpSubsystemFactory` that uses the sub-classed code.
 
-
-### Using a custom `SftpClientFactory`
-
-The code creates `SftpClient`-s and `SftpFileSystem`-s using a default built-in `SftpClientFactory` instance (see
-`DefaultSftpClientFactory`). Users may choose to use a custom factory in order to provide their own
-implementations - e.g., in order to override some default behavior - e.g.:
-
-```java
-
-    SshClient client = ... setup client...
-
-    try (ClientSession session = client.connect(user, host, port).verify(timeout).getSession()) {
-        session.addPasswordIdentity(password);
-        session.auth.verify(timeout);
-
-        // User-specific factory
-        try (SftpClient sftp = MySpecialSessionSftpClientFactory.INSTANCE.createSftpClient(session)) {
-            ... instance created through SpecialSessionSftpClientFactory ...
-        }
-    }
-
-```
-
-
 ### Using `SftpFileSystemProvider` to create an `SftpFileSystem`
 
 
@@ -796,7 +865,8 @@ UTF-8 is used. **Note:** the value can be a charset name or a `java.nio.charset.
          PropertyResolverUtils.updateProperty(session, SftpClient.NAME_DECODING_CHARSET, "ISO-8859-4");
          session.authenticate(...);
 
-         try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session)) {
+         SftpClientFactory factory = SftpClientFactory.instance();
+         try (SftpClient sftp = factory.createSftpClient(session)) {
              for (DirEntry entry : sftp.readDir(...some path...)) {
                  ...handle entry assuming ISO-8859-4 (inherited from the session) encoded names...
              }
@@ -848,7 +918,8 @@ On the client side, all the supported extensions are classes that implement `Sft
         session.addPasswordIdentity(password);
         session.auth().verify(timeout);
 
-        try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session)) {
+        SftpClientFactory factory = SftpClientFactory.instance();
+        try (SftpClient sftp = factory.createSftpClient(session)) {
             Map<String, byte[]> extensions = sftp.getServerExtensions();
             // Key=extension name, value=registered parser instance
             Map<String, ?> data = ParserUtils.parse(extensions);
@@ -880,7 +951,8 @@ One can skip all the conditional code if a specific known extension is required:
         session.addPasswordIdentity(password);
         session.auth().verify(timeout);
 
-        try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session)) {
+        SftpClientFactory factory = SftpClientFactory.instance();
+        try (SftpClient sftp = factory.createSftpClient(session)) {
             // Returns null if extension is not supported by remote server
             SpaceAvailableExtension space = sftp.getExtension(SpaceAvailableExtension.class);
             if (space != null) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/assembly/pom.xml
----------------------------------------------------------------------
diff --git a/assembly/pom.xml b/assembly/pom.xml
index 53db1ba..7b103e1 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -43,6 +43,11 @@
         </dependency>
         <dependency>
             <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-scp</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
             <artifactId>sshd-sftp</artifactId>
             <version>${project.version}</version>
         </dependency>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index b88a195..f161f1f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1006,6 +1006,7 @@
 
     <modules>
         <module>sshd-core</module>
+        <module>sshd-scp</module>
         <module>sshd-sftp</module>
         <module>sshd-mina</module>
         <module>sshd-netty</module>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-cli/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-cli/pom.xml b/sshd-cli/pom.xml
index 0f9bb98..0925da8 100644
--- a/sshd-cli/pom.xml
+++ b/sshd-cli/pom.xml
@@ -44,6 +44,11 @@
         </dependency>
         <dependency>
             <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-scp</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
             <artifactId>sshd-sftp</artifactId>
             <version>${project.version}</version>
         </dependency>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
index 8a9e4a9..3f233ef 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
@@ -35,11 +35,13 @@ import java.util.logging.Level;
 
 import org.apache.sshd.client.scp.ScpClient;
 import org.apache.sshd.client.scp.ScpClient.Option;
+import org.apache.sshd.client.scp.ScpClientCreator;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.scp.ScpLocation;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.threads.ThreadUtils;
 
 /**
  * TODO Add javadoc
@@ -70,13 +72,14 @@ public class ScpCommandMain extends SshClientCliSupport {
             String argName = args[index];
             // handled by 'setupClientSession'
             if (isArgumentedOption(SCP_PORT_OPTION, argName)) {
-                if ((index + 1) >= numArgs) {
+                index++;
+                if (index >= numArgs) {
                     error = showError(stderr, "option requires an argument: " + argName);
                     break;
                 }
 
                 effective.add(argName);
-                effective.add(args[++index]);
+                effective.add(args[index]);
             } else if ("-r".equals(argName) || "-p".equals(argName)
                     || "-q".equals(argName) || "-C".equals(argName)
                     || "-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName)) {
@@ -85,13 +88,14 @@ public class ScpCommandMain extends SshClientCliSupport {
                 error = showError(stderr, "Unknown option: " + argName);
                 break;
             } else {
-                if ((index + 1) >= numArgs) {
+                index++;
+                if (index >= numArgs) {
                     error = showError(stderr, "Not enough arguments");
                     break;
                 }
 
                 ScpLocation source = new ScpLocation(argName);
-                ScpLocation target = new ScpLocation(args[++index]);
+                ScpLocation target = new ScpLocation(args[index]);
                 if (index < (numArgs - 1)) {
                     error = showError(stderr, "Unexpected extra arguments");
                     break;
@@ -117,6 +121,42 @@ public class ScpCommandMain extends SshClientCliSupport {
         return effective.toArray(new String[effective.size()]);
     }
 
+    public static ScpClientCreator resolveScpClientCreator(PrintStream stderr, String... args) {
+        String className = null;
+        for (int index = 0, numArgs = GenericUtils.length(args); index < numArgs; index++) {
+            String argName = args[index];
+            if ("-creator".equals(argName)) {
+                index++;
+                if (index >= numArgs) {
+                    showError(stderr, "option requires an argument: " + argName);
+                    return null;
+                }
+
+                className = args[index];
+            }
+        }
+
+        if (GenericUtils.isEmpty(className)) {
+            className = System.getProperty(ScpClientCreator.class.getName());
+        }
+
+        if (GenericUtils.isEmpty(className)) {
+            return ScpClientCreator.instance();
+        }
+
+        try {
+            ClassLoader cl = ThreadUtils.resolveDefaultClassLoader(ScpClientCreator.class);
+            Class<?> clazz = cl.loadClass(className);
+            return ScpClientCreator.class.cast(clazz.newInstance());
+        } catch (Exception e) {
+            stderr.append("Failed (").append(e.getClass().getSimpleName()).append(')')
+                .append(" to instantiate ").append(className)
+                .append(": ").println(e.getMessage());
+            stderr.flush();
+            return null;
+        }
+    }
+
     public static void main(String[] args) throws Exception {
         PrintStream stdout = System.out;
         PrintStream stderr = System.err;
@@ -134,11 +174,12 @@ public class ScpCommandMain extends SshClientCliSupport {
                 }
             }
 
-            ClientSession session = (logStream == null) || GenericUtils.isEmpty(args)
+            ScpClientCreator creator = resolveScpClientCreator(stderr, args);
+            ClientSession session = ((logStream == null) || (creator == null) || GenericUtils.isEmpty(args))
                 ? null : setupClientSession(SCP_PORT_OPTION, stdin, stdout, stderr, args);
             if (session == null) {
                 stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity] [-io nio2|mina|netty]"
-                         + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value]"
+                         + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value] [-o creator=class name]"
                          + " [-c cipherlist] [-m maclist] [-w password] [-C] <source> <target>");
                 stderr.println();
                 stderr.println("Where <source> or <target> are either 'user@host:file' or a local file path");
@@ -163,7 +204,7 @@ public class ScpCommandMain extends SshClientCliSupport {
                 }
 
                 if (!quiet) {
-                    session.setScpTransferEventListener(new ScpTransferEventListener() {
+                    creator.setScpTransferEventListener(new ScpTransferEventListener() {
                         @Override
                         public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
                             logEvent("startFolderEvent", op, file, -1L, perms, null);
@@ -200,7 +241,7 @@ public class ScpCommandMain extends SshClientCliSupport {
                     });
                 }
 
-                ScpClient client = session.createScpClient();
+                ScpClient client = creator.createScpClient(session);
                 ScpLocation source = new ScpLocation(args[numArgs - 2]);
                 ScpLocation target = new ScpLocation(args[numArgs - 1]);
                 if (source.isLocal()) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
index cba9d24..05c1069 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
@@ -56,7 +56,6 @@ public class SshServerMain extends SshServerCliSupport {
 
     public static void main(String[] args) throws Exception {
         int port = 8000;
-        String provider;
         boolean error = false;
         String hostKeyType = AbstractGeneratorHostKeyProvider.DEFAULT_ALGORITHM;
         int hostKeySize = 0;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-contrib/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-contrib/pom.xml b/sshd-contrib/pom.xml
index e904686..f0ff459 100644
--- a/sshd-contrib/pom.xml
+++ b/sshd-contrib/pom.xml
@@ -53,6 +53,11 @@
         </dependency>
         <dependency>
             <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-scp</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
             <artifactId>sshd-sftp</artifactId>
             <version>${project.version}</version>
         </dependency>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
----------------------------------------------------------------------
diff --git a/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java b/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
index 70bcd44..4312dda 100644
--- a/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
+++ b/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
@@ -28,6 +28,7 @@ import java.util.concurrent.TimeUnit;
 
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.scp.ScpClient;
+import org.apache.sshd.client.scp.ScpClientCreator;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
@@ -89,7 +90,8 @@ public class SimpleAccessControlScpEventListenerTest extends BaseTestSupport {
                 session.addPasswordIdentity(getCurrentTestName());
                 session.auth().verify(5L, TimeUnit.SECONDS);
 
-                ScpClient scp = session.createScpClient();
+                ScpClientCreator creator = ScpClientCreator.instance();
+                ScpClient scp = creator.createScpClient(session);
                 Path targetPath = detectTargetFolder();
                 Path parentPath = targetPath.getParent();
                 Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
index 31b2a22..26ca4bd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
@@ -23,7 +23,6 @@ import org.apache.sshd.client.config.keys.ClientIdentityLoader;
 import org.apache.sshd.client.session.ClientProxyConnectorHolder;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
-import org.apache.sshd.common.scp.ScpFileOpenerHolder;
 
 /**
  * The <code>ClientFactoryManager</code> enable the retrieval of additional
@@ -33,7 +32,6 @@ import org.apache.sshd.common.scp.ScpFileOpenerHolder;
  */
 public interface ClientFactoryManager
         extends FactoryManager,
-                ScpFileOpenerHolder,
                 ClientProxyConnectorHolder,
                 ClientAuthenticationManager {
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 1d034c1..e387bb6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -74,7 +74,6 @@ import org.apache.sshd.common.io.IoConnectFuture;
 import org.apache.sshd.common.io.IoConnector;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
-import org.apache.sshd.common.scp.ScpFileOpener;
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -162,7 +161,6 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     private ClientIdentityLoader clientIdentityLoader;
     private FilePasswordProvider filePasswordProvider;
     private PasswordIdentityProvider passwordIdentityProvider;
-    private ScpFileOpener scpOpener;
 
     private final List<Object> identities = new CopyOnWriteArrayList<>();
     private final AuthenticationIdentitiesProvider identitiesProvider;
@@ -191,16 +189,6 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     }
 
     @Override
-    public ScpFileOpener getScpFileOpener() {
-        return scpOpener;
-    }
-
-    @Override
-    public void setScpFileOpener(ScpFileOpener opener) {
-        scpOpener = opener;
-    }
-
-    @Override
     public ServerKeyVerifier getServerKeyVerifier() {
         return serverKeyVerifier;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
deleted file mode 100644
index 81c20db..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
deleted file mode 100644
index 40afaf7..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
deleted file mode 100644
index 16d0cb2..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
deleted file mode 100644
index e6362b8..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java
deleted file mode 100644
index b2a6091..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
deleted file mode 100644
index 8ae54a4..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.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 {
-    /**
-     * Create an SCP client from this session.
-     *
-     * @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() {
-        return createScpClient(getScpFileOpener(), getScpTransferEventListener());
-    }
-
-    /**
-     * Create an SCP client from this session.
-     *
-     * @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(ScpTransferEventListener listener) {
-        return createScpClient(getScpFileOpener(), listener);
-    }
-
-    /**
-     * Create an SCP client from this session.
-     *
-     * @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(ScpFileOpener opener) {
-        return createScpClient(opener, getScpTransferEventListener());
-    }
-
-    /**
-     * Create an SCP client from this session.
-     *
-     * @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(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-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index d57a799..1cb309a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -40,8 +40,6 @@ import org.apache.sshd.client.channel.ChannelShell;
 import org.apache.sshd.client.channel.ChannelSubsystem;
 import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
-import org.apache.sshd.client.scp.DefaultScpClient;
-import org.apache.sshd.client.scp.ScpClient;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.NamedResource;
@@ -59,8 +57,6 @@ import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.kex.KexState;
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.helpers.AbstractConnectionService;
@@ -82,8 +78,6 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private UserInteraction userInteraction;
     private PasswordIdentityProvider passwordIdentityProvider;
     private List<NamedFactory<UserAuth>> userAuthFactories;
-    private ScpTransferEventListener scpListener;
-    private ScpFileOpener scpOpener;
     private SocketAddress connectAddress;
     private ClientProxyConnector proxyConnector;
 
@@ -301,31 +295,6 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
-    public ScpFileOpener getScpFileOpener() {
-        return resolveEffectiveProvider(ScpFileOpener.class, scpOpener, getFactoryManager().getScpFileOpener());
-    }
-
-    @Override
-    public void setScpFileOpener(ScpFileOpener opener) {
-        scpOpener = opener;
-    }
-
-    @Override
-    public ScpTransferEventListener getScpTransferEventListener() {
-        return scpListener;
-    }
-
-    @Override
-    public void setScpTransferEventListener(ScpTransferEventListener listener) {
-        scpListener = listener;
-    }
-
-    @Override
-    public ScpClient createScpClient(ScpFileOpener opener, ScpTransferEventListener listener) {
-        return new DefaultScpClient(this, opener, listener);
-    }
-
-    @Override
     public SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException {
         ForwardingFilter filter = getForwardingFilter();
         return filter.startLocalPortForwarding(local, remote);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index 96caf2e..1d68a20 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -42,7 +42,6 @@ import org.apache.sshd.client.channel.ChannelSubsystem;
 import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.channel.ClientChannelEvent;
 import org.apache.sshd.client.future.AuthFuture;
-import org.apache.sshd.client.scp.ScpClientCreator;
 import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker;
 import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
 import org.apache.sshd.common.forward.PortForwardingManager;
@@ -81,9 +80,8 @@ import org.apache.sshd.common.util.net.SshdSocketAddress;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public interface ClientSession
-            extends Session, ScpClientCreator,
-            ClientProxyConnectorHolder, ClientAuthenticationManager,
-            PortForwardingManager {
+            extends Session, ClientProxyConnectorHolder,
+            ClientAuthenticationManager, PortForwardingManager {
     enum ClientSessionEvent {
         TIMEOUT,
         CLOSED,

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
index 247be60..dc5ef87 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
@@ -19,19 +19,6 @@
 
 package org.apache.sshd.client.simple;
 
-import java.io.IOException;
-import java.lang.reflect.Proxy;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-import java.security.KeyPair;
-import java.util.Objects;
-
-import org.apache.sshd.client.scp.CloseableScpClient;
-import org.apache.sshd.client.scp.ScpClient;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
 
 /**
@@ -41,97 +28,4 @@ public abstract class AbstractSimpleClient extends AbstractLoggingBean implement
     protected AbstractSimpleClient() {
         super();
     }
-
-    @Override
-    public CloseableScpClient scpLogin(String host, String username, String password) throws IOException {
-        return scpLogin(host, DEFAULT_PORT, username, password);
-    }
-
-    @Override
-    public CloseableScpClient scpLogin(String host, int port, String username, String password) throws IOException {
-        return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
-    }
-
-    @Override
-    public CloseableScpClient scpLogin(String host, String username, KeyPair identity) throws IOException {
-        return scpLogin(host, DEFAULT_PORT, username, identity);
-    }
-
-    @Override
-    public CloseableScpClient scpLogin(String host, int port, String username, KeyPair identity) throws IOException {
-        return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
-    }
-
-    @Override
-    public CloseableScpClient scpLogin(InetAddress host, String username, String password) throws IOException {
-        return scpLogin(host, DEFAULT_PORT, username, password);
-    }
-
-    @Override
-    public 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);
-    }
-
-    @Override
-    public CloseableScpClient scpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
-        return scpLogin(host, DEFAULT_PORT, username, identity);
-    }
-
-    @Override
-    public 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);
-    }
-
-    @Override
-    public CloseableScpClient scpLogin(SocketAddress target, String username, String password) throws IOException {
-        return createScpClient(sessionLogin(target, username, password));
-    }
-
-    @Override
-    public CloseableScpClient scpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
-        return createScpClient(sessionLogin(target, username, identity));
-    }
-
-    protected CloseableScpClient createScpClient(ClientSession session) throws IOException {
-        try {
-            ScpClient client = Objects.requireNonNull(session, "No client session").createScpClient();
-            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;
-                }
-            });
-        } 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);
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
index 6fff133..174f600 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
@@ -31,7 +31,6 @@ import java.nio.channels.Channel;
 public interface SimpleClient
         extends SimpleClientConfigurator,
                 SimpleSessionClient,
-                SimpleScpClient,
                 Channel {
     // marker interface
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java
deleted file mode 100644
index eeeb7a2..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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.simple;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.SocketAddress;
-import java.nio.channels.Channel;
-import java.security.KeyPair;
-
-import org.apache.sshd.client.scp.CloseableScpClient;
-
-/**
- * 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 SimpleClientConfigurator, 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
-     */
-    CloseableScpClient scpLogin(String host, String username, String password) throws IOException;
-
-    /**
-     * 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
-     */
-    CloseableScpClient scpLogin(String host, int port, String username, String password) throws IOException;
-
-    /**
-     * 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
-     */
-    CloseableScpClient scpLogin(String host, String username, KeyPair identity) throws IOException;
-
-    /**
-     * 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
-     */
-    CloseableScpClient scpLogin(String host, int port, String username, KeyPair identity) throws IOException;
-
-    /**
-     * 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
-     */
-    CloseableScpClient scpLogin(InetAddress host, String username, String password) throws IOException;
-
-    /**
-     * 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
-     */
-    CloseableScpClient scpLogin(InetAddress host, int port, String username, String password) throws IOException;
-
-    /**
-     * 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
-     */
-    CloseableScpClient scpLogin(InetAddress host, String username, KeyPair identity) throws IOException;
-
-    /**
-     * 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
-     */
-    CloseableScpClient scpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException;
-
-    /**
-     * 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;
-}


[3/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
new file mode 100644
index 0000000..1cbea2d
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
@@ -0,0 +1,837 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+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.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.file.util.MockPath;
+import org.apache.sshd.common.scp.ScpTransferEventListener.FileOperation;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.LimitInputStream;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@SuppressWarnings("PMD.AvoidUsingOctalValues")
+public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Session> {
+    /**
+     * Command prefix used to identify SCP commands
+     */
+    public static final String SCP_COMMAND_PREFIX = "scp";
+
+    public static final int OK = 0;
+    public static final int WARNING = 1;
+    public static final int ERROR = 2;
+
+    /**
+     * Default size (in bytes) of send / receive buffer size
+     */
+    public static final int DEFAULT_COPY_BUFFER_SIZE = IoUtils.DEFAULT_COPY_SIZE;
+    public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
+    public static final int DEFAULT_SEND_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
+
+    /**
+     * The minimum size for sending / receiving files
+     */
+    public static final int MIN_COPY_BUFFER_SIZE = Byte.MAX_VALUE;
+    public static final int MIN_RECEIVE_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
+    public static final int MIN_SEND_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
+
+    public static final int S_IRUSR = 0000400;
+    public static final int S_IWUSR = 0000200;
+    public static final int S_IXUSR = 0000100;
+    public static final int S_IRGRP = 0000040;
+    public static final int S_IWGRP = 0000020;
+    public static final int S_IXGRP = 0000010;
+    public static final int S_IROTH = 0000004;
+    public static final int S_IWOTH = 0000002;
+    public static final int S_IXOTH = 0000001;
+
+    public static final String DEFAULT_DIR_OCTAL_PERMISSIONS = "0755";
+    public static final String DEFAULT_FILE_OCTAL_PERMISSIONS = "0644";
+
+    protected final InputStream in;
+    protected final OutputStream out;
+    protected final FileSystem fileSystem;
+    protected final ScpFileOpener opener;
+    protected final ScpTransferEventListener listener;
+
+    private final Session sessionInstance;
+
+    public ScpHelper(Session session, InputStream in, OutputStream out,
+            FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) {
+        this.sessionInstance = Objects.requireNonNull(session, "No session");
+        this.in = Objects.requireNonNull(in, "No input stream");
+        this.out = Objects.requireNonNull(out, "No output stream");
+        this.fileSystem = fileSystem;
+        this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
+        this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
+    }
+
+    @Override
+    public Session getSession() {
+        return sessionInstance;
+    }
+
+    public void receiveFileStream(OutputStream local, int bufferSize) throws IOException {
+        receive((line, isDir, timestamp) -> {
+            if (isDir) {
+                throw new StreamCorruptedException("Cannot download a directory into a file stream: " + line);
+            }
+
+            Path path = new MockPath(line);
+            receiveStream(line, new ScpTargetStreamResolver() {
+                @Override
+                @SuppressWarnings("synthetic-access")
+                public OutputStream resolveTargetStream(
+                        Session session, String name, long length, Set<PosixFilePermission> perms, OpenOption... options)
+                            throws IOException {
+                    if (log.isDebugEnabled()) {
+                        log.debug("resolveTargetStream({}) name={}, perms={}, len={} - started local stream download",
+                                  ScpHelper.this, name, perms, length);
+                    }
+                    return local;
+                }
+
+                @Override
+                public Path getEventListenerFilePath() {
+                    return path;
+                }
+
+                @Override
+                @SuppressWarnings("synthetic-access")
+                public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+                    if (log.isDebugEnabled()) {
+                        log.debug("postProcessReceivedData({}) name={}, perms={}, preserve={} time={}",
+                                  ScpHelper.this, name, perms, preserve, time);
+                    }
+                }
+
+                @Override
+                public String toString() {
+                    return line;
+                }
+            }, timestamp, false, bufferSize);
+        });
+    }
+
+    public void receive(Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize) throws IOException {
+        Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+        Path path = opener.resolveIncomingReceiveLocation(localPath, recursive, shouldBeDir, preserve);
+        receive((line, isDir, time) -> {
+            if (recursive && isDir) {
+                receiveDir(line, path, time, preserve, bufferSize);
+            } else {
+                receiveFile(line, path, time, preserve, bufferSize);
+            }
+        });
+    }
+
+    protected void receive(ScpReceiveLineHandler handler) throws IOException {
+        ack();
+        ScpTimestamp time = null;
+        for (;;) {
+            String line;
+            boolean isDir = false;
+            int c = readAck(true);
+            switch (c) {
+                case -1:
+                    return;
+                case 'D':
+                    isDir = true;
+                    line = String.valueOf((char) c) + readLine();
+                    if (log.isDebugEnabled()) {
+                        log.debug("receive({}) - Received 'D' header: {}", this, line);
+                    }
+                    break;
+                case 'C':
+                    line = String.valueOf((char) c) + readLine();
+                    if (log.isDebugEnabled()) {
+                        log.debug("receive({}) - Received 'C' header: {}", this, line);
+                    }
+                    break;
+                case 'T':
+                    line = String.valueOf((char) c) + readLine();
+                    if (log.isDebugEnabled()) {
+                        log.debug("receive({}) - Received 'T' header: {}", this, line);
+                    }
+                    time = ScpTimestamp.parseTime(line);
+                    ack();
+                    continue;
+                case 'E':
+                    line = String.valueOf((char) c) + readLine();
+                    if (log.isDebugEnabled()) {
+                        log.debug("receive({}) - Received 'E' header: {}", this, line);
+                    }
+                    ack();
+                    return;
+                default:
+                    //a real ack that has been acted upon already
+                    continue;
+            }
+
+            try {
+                handler.process(line, isDir, time);
+            } finally {
+                time = null;
+            }
+        }
+    }
+
+    public void receiveDir(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
+        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+        if (log.isDebugEnabled()) {
+            log.debug("receiveDir({})[{}] Receiving directory {} - preserve={}, time={}, buffer-size={}",
+                      this, header, path, preserve, time, bufferSize);
+        }
+        if (!header.startsWith("D")) {
+            throw new IOException("Expected a 'D; message but got '" + header + "'");
+        }
+
+        Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
+        int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
+        String name = header.substring(header.indexOf(' ', 6) + 1);
+        if (length != 0) {
+            throw new IOException("Expected 0 length for directory=" + name + " but got " + length);
+        }
+
+        Path file = opener.resolveIncomingFilePath(path, name, preserve, perms, time);
+
+        ack();
+
+        time = null;
+        listener.startFolderEvent(FileOperation.RECEIVE, path, perms);
+        try {
+            for (;;) {
+                header = readLine();
+                if (log.isDebugEnabled()) {
+                    log.debug("receiveDir({})[{}] Received header: {}", this, file, header);
+                }
+                if (header.startsWith("C")) {
+                    receiveFile(header, file, time, preserve, bufferSize);
+                    time = null;
+                } else if (header.startsWith("D")) {
+                    receiveDir(header, file, time, preserve, bufferSize);
+                    time = null;
+                } else if (header.equals("E")) {
+                    ack();
+                    break;
+                } else if (header.startsWith("T")) {
+                    time = ScpTimestamp.parseTime(header);
+                    ack();
+                } else {
+                    throw new IOException("Unexpected message: '" + header + "'");
+                }
+            }
+        } catch (IOException | RuntimeException e) {
+            listener.endFolderEvent(FileOperation.RECEIVE, path, perms, e);
+            throw e;
+        }
+        listener.endFolderEvent(FileOperation.RECEIVE, path, perms, null);
+    }
+
+    public void receiveFile(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
+        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+        if (log.isDebugEnabled()) {
+            log.debug("receiveFile({})[{}] Receiving file {} - preserve={}, time={}, buffer-size={}",
+                      this, header, path, preserve, time, bufferSize);
+        }
+
+        receiveStream(header, opener.createScpTargetStreamResolver(path), time, preserve, bufferSize);
+    }
+
+    public void receiveStream(String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
+        if (!header.startsWith("C")) {
+            throw new IOException("receiveStream(" + resolver + ") Expected a C message but got '" + header + "'");
+        }
+
+        if (bufferSize < MIN_RECEIVE_BUFFER_SIZE) {
+            throw new IOException("receiveStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_RECEIVE_BUFFER_SIZE + ")");
+        }
+
+        Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
+        long length = Long.parseLong(header.substring(6, header.indexOf(' ', 6)));
+        String name = header.substring(header.indexOf(' ', 6) + 1);
+        if (length < 0L) { // TODO consider throwing an exception...
+            log.warn("receiveStream({})[{}] bad length in header: {}", this, resolver, header);
+        }
+
+        // if file size is less than buffer size allocate only expected file size
+        int bufSize;
+        boolean debugEnabled = log.isDebugEnabled();
+        if (length == 0L) {
+            if (debugEnabled) {
+                log.debug("receiveStream({})[{}] zero file size (perhaps special file) using copy buffer size={}",
+                          this, resolver, MIN_RECEIVE_BUFFER_SIZE);
+            }
+            bufSize = MIN_RECEIVE_BUFFER_SIZE;
+        } else {
+            bufSize = (int) Math.min(length, bufferSize);
+        }
+
+        if (bufSize < 0) { // TODO consider throwing an exception
+            log.warn("receiveStream({})[{}] bad buffer size ({}) using default ({})",
+                     this, resolver, bufSize, MIN_RECEIVE_BUFFER_SIZE);
+            bufSize = MIN_RECEIVE_BUFFER_SIZE;
+        }
+
+        try (
+                InputStream is = new LimitInputStream(this.in, length);
+                OutputStream os = resolver.resolveTargetStream(getSession(), name, length, perms)
+        ) {
+            ack();
+
+            Path file = resolver.getEventListenerFilePath();
+            listener.startFileEvent(FileOperation.RECEIVE, file, length, perms);
+            try {
+                IoUtils.copy(is, os, bufSize);
+            } catch (IOException | RuntimeException e) {
+                listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, e);
+                throw e;
+            }
+            listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, null);
+        }
+
+        resolver.postProcessReceivedData(name, preserve, perms, time);
+
+        ack();
+
+        int replyCode = readAck(false);
+        if (debugEnabled) {
+            log.debug("receiveStream({})[{}] ack reply code={}", this, resolver, replyCode);
+        }
+        validateAckReplyCode("receiveStream", resolver, replyCode, false);
+    }
+
+    public String readLine() throws IOException {
+        return readLine(false);
+    }
+
+    public String readLine(boolean canEof) throws IOException {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Byte.MAX_VALUE)) {
+            for (;;) {
+                int c = in.read();
+                if (c == '\n') {
+                    return baos.toString(StandardCharsets.UTF_8.name());
+                } else if (c == -1) {
+                    if (!canEof) {
+                        throw new EOFException("EOF while await end of line");
+                    }
+                    return null;
+                } else {
+                    baos.write(c);
+                }
+            }
+        }
+    }
+
+    public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
+        int readyCode = readAck(false);
+        boolean debugEnabled = log.isDebugEnabled();
+        if (debugEnabled) {
+            log.debug("send({}) ready code={}", paths, readyCode);
+        }
+        validateOperationReadyCode("send", "Paths", readyCode, false);
+
+        LinkOption[] options = IoUtils.getLinkOptions(true);
+        for (String pattern : paths) {
+            pattern = pattern.replace('/', File.separatorChar);
+
+            int idx = pattern.indexOf('*'); // check if wildcard used
+            if (idx >= 0) {
+                String basedir = "";
+                String fixedPart = pattern.substring(0, idx);
+                int lastSep = fixedPart.lastIndexOf(File.separatorChar);
+                if (lastSep >= 0) {
+                    basedir = pattern.substring(0, lastSep);
+                    pattern = pattern.substring(lastSep + 1);
+                }
+
+                Iterable<String> included = opener.getMatchingFilesToSend(basedir, pattern);
+                for (String path : included) {
+                    Path file = resolveLocalPath(basedir, path);
+                    if (opener.sendAsRegularFile(file, options)) {
+                        sendFile(file, preserve, bufferSize);
+                    } else if (opener.sendAsDirectory(file, options)) {
+                        if (!recursive) {
+                            if (debugEnabled) {
+                                log.debug("send({}) {}: not a regular file", this, path);
+                            }
+                            sendWarning(path.replace(File.separatorChar, '/') + " not a regular file");
+                        } else {
+                            sendDir(file, preserve, bufferSize);
+                        }
+                    } else {
+                        if (debugEnabled) {
+                            log.debug("send({}) {}: unknown file type", this, path);
+                        }
+                        sendWarning(path.replace(File.separatorChar, '/') + " unknown file type");
+                    }
+                }
+            } else {
+                send(resolveLocalPath(pattern), recursive, preserve, bufferSize, options);
+            }
+        }
+    }
+
+    public void sendPaths(Collection<? extends Path> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
+        int readyCode = readAck(false);
+        if (log.isDebugEnabled()) {
+            log.debug("sendPaths({}) ready code={}", paths, readyCode);
+        }
+        validateOperationReadyCode("sendPaths", "Paths", readyCode, false);
+
+        LinkOption[] options = IoUtils.getLinkOptions(true);
+        for (Path file : paths) {
+            send(file, recursive, preserve, bufferSize, options);
+        }
+    }
+
+    protected void send(Path local, boolean recursive, boolean preserve, int bufferSize, LinkOption... options) throws IOException {
+        Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+        Path file = opener.resolveOutgoingFilePath(localPath, options);
+        if (opener.sendAsRegularFile(file, options)) {
+            sendFile(file, preserve, bufferSize);
+        } else if (opener.sendAsDirectory(file, options)) {
+            if (!recursive) {
+                throw new IOException(file + " not a regular file");
+            } else {
+                sendDir(file, preserve, bufferSize);
+            }
+        } else {
+            throw new IOException(file + ": unknown file type");
+        }
+    }
+
+    public Path resolveLocalPath(String basedir, String subpath) throws IOException {
+        if (GenericUtils.isEmpty(basedir)) {
+            return resolveLocalPath(subpath);
+        } else {
+            return resolveLocalPath(basedir + File.separator + subpath);
+        }
+    }
+
+    /**
+     * @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
+     */
+    public Path resolveLocalPath(String commandPath) throws IOException, InvalidPathException {
+        Path p = opener.resolveLocalPath(fileSystem, commandPath);
+        if (log.isTraceEnabled()) {
+            log.trace("resolveLocalPath({}) {}: {}", this, commandPath, p);
+        }
+
+        return p;
+    }
+
+    public void sendFile(Path local, boolean preserve, int bufferSize) throws IOException {
+        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+        if (log.isDebugEnabled()) {
+            log.debug("sendFile({})[preserve={},buffer-size={}] Sending file {}", this, preserve, bufferSize, path);
+        }
+
+        sendStream(opener.createScpSourceStreamResolver(path), preserve, bufferSize);
+    }
+
+    public void sendStream(ScpSourceStreamResolver resolver, boolean preserve, int bufferSize) throws IOException {
+        if (bufferSize < MIN_SEND_BUFFER_SIZE) {
+            throw new IOException("sendStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_SEND_BUFFER_SIZE + ")");
+        }
+
+        long fileSize = resolver.getSize();
+        // if file size is less than buffer size allocate only expected file size
+        int bufSize;
+        boolean debugEnabled = log.isDebugEnabled();
+        if (fileSize <= 0L) {
+            if (debugEnabled) {
+                log.debug("sendStream({})[{}] unknown file size ({}) perhaps special file - using copy buffer size={}",
+                          this, resolver, fileSize, MIN_SEND_BUFFER_SIZE);
+            }
+            bufSize = MIN_SEND_BUFFER_SIZE;
+        } else {
+            bufSize = (int) Math.min(fileSize, bufferSize);
+        }
+
+        if (bufSize < 0) { // TODO consider throwing an exception
+            log.warn("sendStream({})[{}] bad buffer size ({}) using default ({})",
+                     this, resolver, bufSize, MIN_SEND_BUFFER_SIZE);
+            bufSize = MIN_SEND_BUFFER_SIZE;
+        }
+
+        ScpTimestamp time = resolver.getTimestamp();
+        if (preserve && (time != null)) {
+            String cmd = "T" + TimeUnit.MILLISECONDS.toSeconds(time.getLastModifiedTime())
+                    + " " + "0" + " " + TimeUnit.MILLISECONDS.toSeconds(time.getLastAccessTime())
+                    + " " + "0";
+            if (debugEnabled) {
+                log.debug("sendStream({})[{}] send timestamp={} command: {}", this, resolver, time, cmd);
+            }
+            out.write(cmd.getBytes(StandardCharsets.UTF_8));
+            out.write('\n');
+            out.flush();
+
+            int readyCode = readAck(false);
+            if (debugEnabled) {
+                log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, cmd, readyCode);
+            }
+            validateAckReplyCode(cmd, resolver, readyCode, false);
+        }
+
+        Set<PosixFilePermission> perms = EnumSet.copyOf(resolver.getPermissions());
+        String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_FILE_OCTAL_PERMISSIONS : getOctalPermissions(perms);
+        String fileName = resolver.getFileName();
+        String cmd = "C" + octalPerms + " " + fileSize + " " + fileName;
+        if (debugEnabled) {
+            log.debug("sendStream({})[{}] send 'C' command: {}", this, resolver, cmd);
+        }
+        out.write(cmd.getBytes(StandardCharsets.UTF_8));
+        out.write('\n');
+        out.flush();
+
+        int readyCode = readAck(false);
+        if (debugEnabled) {
+            log.debug("sendStream({})[{}] command='{}' ready code={}",
+                      this, resolver, cmd.substring(0, cmd.length() - 1), readyCode);
+        }
+        validateAckReplyCode(cmd, resolver, readyCode, false);
+
+        try (InputStream in = resolver.resolveSourceStream(getSession())) {
+            Path path = resolver.getEventListenerFilePath();
+            listener.startFileEvent(FileOperation.SEND, path, fileSize, perms);
+            try {
+                IoUtils.copy(in, out, bufSize);
+            } catch (IOException | RuntimeException e) {
+                listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, e);
+                throw e;
+            }
+            listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, null);
+        }
+        ack();
+
+        readyCode = readAck(false);
+        if (debugEnabled) {
+            log.debug("sendStream({})[{}] command='{}' reply code={}", this, resolver, cmd, readyCode);
+        }
+        validateAckReplyCode("sendStream", resolver, readyCode, false);
+    }
+
+    protected void validateOperationReadyCode(String command, Object location, int readyCode, boolean eofAllowed) throws IOException {
+        validateCommandStatusCode(command, location, readyCode, eofAllowed);
+    }
+
+    protected void validateAckReplyCode(String command, Object location, int replyCode, boolean eofAllowed) throws IOException {
+        validateCommandStatusCode(command, location, replyCode, eofAllowed);
+    }
+
+    protected void validateCommandStatusCode(String command, Object location, int statusCode, boolean eofAllowed) throws IOException {
+        switch (statusCode) {
+            case -1:
+                if (!eofAllowed) {
+                    throw new EOFException("Unexpected EOF for command='" + command + "' on " + location);
+                }
+                break;
+            case OK:
+                break;
+            case WARNING:
+                break;
+            default:
+                throw new ScpException("Bad reply code (" + statusCode + ") for command='" + command + "' on " + location, statusCode);
+        }
+    }
+
+    public void sendDir(Path local, boolean preserve, int bufferSize) throws IOException {
+        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+        boolean debugEnabled = log.isDebugEnabled();
+        if (debugEnabled) {
+            log.debug("sendDir({}) Sending directory {} - preserve={}, buffer-size={}",
+                      this, path, preserve, bufferSize);
+        }
+
+        LinkOption[] options = IoUtils.getLinkOptions(true);
+        if (preserve) {
+            BasicFileAttributes basic = opener.getLocalBasicFileAttributes(path, options);
+            FileTime lastModified = basic.lastModifiedTime();
+            FileTime lastAccess = basic.lastAccessTime();
+            String cmd = "T" + lastModified.to(TimeUnit.SECONDS) + " "
+                    + "0" + " " + lastAccess.to(TimeUnit.SECONDS) + " "
+                    + "0";
+            if (debugEnabled) {
+                log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}",
+                          this, path, lastModified,  lastAccess, cmd);
+            }
+
+            out.write(cmd.getBytes(StandardCharsets.UTF_8));
+            out.write('\n');
+            out.flush();
+
+            int readyCode = readAck(false);
+            if (debugEnabled) {
+                if (debugEnabled) {
+                    log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode);
+                }
+            }
+            validateAckReplyCode(cmd, path, readyCode, false);
+        }
+
+        Set<PosixFilePermission> perms = opener.getLocalFilePermissions(path, options);
+        String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_DIR_OCTAL_PERMISSIONS : getOctalPermissions(perms);
+        String cmd = "D" + octalPerms + " " + "0" + " " + Objects.toString(path.getFileName(), null);
+        if (debugEnabled) {
+            log.debug("sendDir({})[{}] send 'D' command: {}", this, path, cmd);
+        }
+        out.write(cmd.getBytes(StandardCharsets.UTF_8));
+        out.write('\n');
+        out.flush();
+
+        int readyCode = readAck(false);
+        if (debugEnabled) {
+            log.debug("sendDir({})[{}] command='{}' ready code={}",
+                      this, path, cmd.substring(0, cmd.length() - 1), readyCode);
+        }
+        validateAckReplyCode(cmd, path, readyCode, false);
+
+        try (DirectoryStream<Path> children = opener.getLocalFolderChildren(path)) {
+            listener.startFolderEvent(FileOperation.SEND, path, perms);
+
+            try {
+                for (Path child : children) {
+                    if (opener.sendAsRegularFile(child, options)) {
+                        sendFile(child, preserve, bufferSize);
+                    } else if (opener.sendAsDirectory(child, options)) {
+                        sendDir(child, preserve, bufferSize);
+                    }
+                }
+            } catch (IOException | RuntimeException e) {
+                listener.endFolderEvent(FileOperation.SEND, path, perms, e);
+                throw e;
+            }
+
+            listener.endFolderEvent(FileOperation.SEND, path, perms, null);
+        }
+
+        if (debugEnabled) {
+            log.debug("sendDir({})[{}] send 'E' command", this, path);
+        }
+        out.write("E\n".getBytes(StandardCharsets.UTF_8));
+        out.flush();
+
+        readyCode = readAck(false);
+        if (debugEnabled) {
+            log.debug("sendDir({})[{}] 'E' command reply code=", this, path, readyCode);
+        }
+        validateAckReplyCode("E", path, readyCode, false);
+    }
+
+    public static String getOctalPermissions(Collection<PosixFilePermission> perms) {
+        int pf = 0;
+
+        for (PosixFilePermission p : perms) {
+            switch (p) {
+                case OWNER_READ:
+                    pf |= S_IRUSR;
+                    break;
+                case OWNER_WRITE:
+                    pf |= S_IWUSR;
+                    break;
+                case OWNER_EXECUTE:
+                    pf |= S_IXUSR;
+                    break;
+                case GROUP_READ:
+                    pf |= S_IRGRP;
+                    break;
+                case GROUP_WRITE:
+                    pf |= S_IWGRP;
+                    break;
+                case GROUP_EXECUTE:
+                    pf |= S_IXGRP;
+                    break;
+                case OTHERS_READ:
+                    pf |= S_IROTH;
+                    break;
+                case OTHERS_WRITE:
+                    pf |= S_IWOTH;
+                    break;
+                case OTHERS_EXECUTE:
+                    pf |= S_IXOTH;
+                    break;
+                default:    // ignored
+            }
+        }
+
+        return String.format("%04o", pf);
+    }
+
+    public static Set<PosixFilePermission> parseOctalPermissions(String str) {
+        int perms = Integer.parseInt(str, 8);
+        Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class);
+        if ((perms & S_IRUSR) != 0) {
+            p.add(PosixFilePermission.OWNER_READ);
+        }
+        if ((perms & S_IWUSR) != 0) {
+            p.add(PosixFilePermission.OWNER_WRITE);
+        }
+        if ((perms & S_IXUSR) != 0) {
+            p.add(PosixFilePermission.OWNER_EXECUTE);
+        }
+        if ((perms & S_IRGRP) != 0) {
+            p.add(PosixFilePermission.GROUP_READ);
+        }
+        if ((perms & S_IWGRP) != 0) {
+            p.add(PosixFilePermission.GROUP_WRITE);
+        }
+        if ((perms & S_IXGRP) != 0) {
+            p.add(PosixFilePermission.GROUP_EXECUTE);
+        }
+        if ((perms & S_IROTH) != 0) {
+            p.add(PosixFilePermission.OTHERS_READ);
+        }
+        if ((perms & S_IWOTH) != 0) {
+            p.add(PosixFilePermission.OTHERS_WRITE);
+        }
+        if ((perms & S_IXOTH) != 0) {
+            p.add(PosixFilePermission.OTHERS_EXECUTE);
+        }
+
+        return p;
+    }
+
+    protected void sendWarning(String message) throws IOException {
+        sendResponseMessage(WARNING, message);
+    }
+
+    protected void sendError(String message) throws IOException {
+        sendResponseMessage(ERROR, message);
+    }
+
+    protected void sendResponseMessage(int level, String message) throws IOException {
+        sendResponseMessage(out, level, message);
+    }
+
+    public static <O extends OutputStream> O sendWarning(O out, String message) throws IOException {
+        return sendResponseMessage(out, WARNING, message);
+    }
+
+    public static <O extends OutputStream> O sendError(O out, String message) throws IOException {
+        return sendResponseMessage(out, ERROR, message);
+    }
+
+    public static <O extends OutputStream> O sendResponseMessage(O out, int level, String message) throws IOException {
+        out.write(level);
+        out.write(message.getBytes(StandardCharsets.UTF_8));
+        out.write('\n');
+        out.flush();
+        return out;
+    }
+
+    public static String getExitStatusName(Integer exitStatus) {
+        if (exitStatus == null) {
+            return "null";
+        }
+
+        switch (exitStatus) {
+            case OK:
+                return "OK";
+            case WARNING:
+                return "WARNING";
+            case ERROR:
+                return "ERROR";
+            default:
+                return exitStatus.toString();
+        }
+    }
+
+    public void ack() throws IOException {
+        out.write(0);
+        out.flush();
+    }
+
+    public int readAck(boolean canEof) throws IOException {
+        int c = in.read();
+        switch (c) {
+            case -1:
+                if (log.isDebugEnabled()) {
+                    log.debug("readAck({})[EOF={}] received EOF", this, canEof);
+                }
+                if (!canEof) {
+                    throw new EOFException("readAck - EOF before ACK");
+                }
+                break;
+            case OK:
+                if (log.isDebugEnabled()) {
+                    log.debug("readAck({})[EOF={}] read OK", this, canEof);
+                }
+                break;
+            case WARNING: {
+                if (log.isDebugEnabled()) {
+                    log.debug("readAck({})[EOF={}] read warning message", this, canEof);
+                }
+
+                String line = readLine();
+                log.warn("readAck({})[EOF={}] - Received warning: {}", this, canEof, line);
+                break;
+            }
+            case ERROR: {
+                if (log.isDebugEnabled()) {
+                    log.debug("readAck({})[EOF={}] read error message", this, canEof);
+                }
+                String line = readLine();
+                if (log.isDebugEnabled()) {
+                    log.debug("readAck({})[EOF={}] received error: {}", this, canEof, line);
+                }
+                throw new ScpException("Received nack: " + line, c);
+            }
+            default:
+                break;
+        }
+        return c;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + getSession() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
new file mode 100644
index 0000000..d2a9afc
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
@@ -0,0 +1,227 @@
+/*
+ * 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.Serializable;
+import java.util.Objects;
+
+import org.apache.sshd.common.auth.MutableUserHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * Represents a local or remote SCP location in the format {@code user@host:path}
+ * for a remote path and a simple path for a local one. If user is omitted for a
+ * remote path then current user is used.
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpLocation implements MutableUserHolder, Serializable, Cloneable {
+    public static final char HOST_PART_SEPARATOR = ':';
+    public static final char USERNAME_PART_SEPARATOR = '@';
+
+    private static final long serialVersionUID = 5450230457030600136L;
+
+    private String host;
+    private String username;
+    private String path;
+
+    public ScpLocation() {
+        this(null);
+    }
+
+    /**
+     * @param locSpec The location specification - ignored if {@code null}/empty
+     * @see #update(String, ScpLocation)
+     * @throws IllegalArgumentException if invalid specification
+     */
+    public ScpLocation(String locSpec) {
+        update(locSpec, this);
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public boolean isLocal() {
+        return GenericUtils.isEmpty(getHost());
+    }
+
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    /**
+     * Resolves the effective username to use for a remote location.
+     * If username not set then uses the current username
+     *
+     * @return The resolved username
+     * @see #getUsername()
+     * @see OsUtils#getCurrentUser()
+     */
+    public String resolveUsername() {
+        String user = getUsername();
+        if (GenericUtils.isEmpty(user)) {
+            return OsUtils.getCurrentUser();
+        } else {
+            return user;
+        }
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getHost(), resolveUsername(), OsUtils.getComparablePath(getPath()));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        ScpLocation other = (ScpLocation) obj;
+        if (this.isLocal() != other.isLocal()) {
+            return false;
+        }
+
+        String thisPath = OsUtils.getComparablePath(getPath());
+        String otherPath = OsUtils.getComparablePath(other.getPath());
+        if (!Objects.equals(thisPath, otherPath)) {
+            return false;
+        }
+
+        if (isLocal()) {
+            return true;
+        }
+
+        // we know other is also remote or we would not have reached this point
+        return Objects.equals(resolveUsername(), other.resolveUsername())
+            && Objects.equals(getHost(), other.getHost());
+    }
+
+    @Override
+    public ScpLocation clone() {
+        try {
+            return getClass().cast(super.clone());
+        } catch (CloneNotSupportedException e) {    // unexpected
+            throw new RuntimeException("Failed to clone " + toString(), e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        String p = getPath();
+        if (isLocal()) {
+            return p;
+        }
+
+        return resolveUsername() + String.valueOf(USERNAME_PART_SEPARATOR)
+             + getHost() + String.valueOf(HOST_PART_SEPARATOR) + p;
+    }
+
+    /**
+     * Parses a local or remote SCP location in the format {@code user@host:path}
+     *
+     * @param locSpec The location specification - ignored if {@code null}/empty
+     * @return The {@link ScpLocation} or {@code null} if no specification provider
+     * @throws IllegalArgumentException if invalid specification
+     * @see #update(String, ScpLocation)
+     */
+    public static ScpLocation parse(String locSpec) {
+        return GenericUtils.isEmpty(locSpec) ? null : update(locSpec, new ScpLocation());
+    }
+
+    /**
+     * Parses a local or remote SCP location in the format {@code user@host:path}
+     *
+     * @param <L> Type of {@link ScpLocation} being updated
+     * @param locSpec The location specification - ignored if {@code null}/empty
+     * @param location The {@link ScpLocation} to update - never {@code null}
+     * @return The updated location (unless no specification)
+     * @throws IllegalArgumentException if invalid specification
+     */
+    public static <L extends ScpLocation> L update(String locSpec, L location) {
+        Objects.requireNonNull(location, "No location to update");
+        if (GenericUtils.isEmpty(locSpec)) {
+            return location;
+        }
+
+        location.setHost(null);
+        location.setUsername(null);
+
+        int pos = locSpec.indexOf(HOST_PART_SEPARATOR);
+        if (pos < 0) {  // assume a local path
+            location.setPath(locSpec);
+            return location;
+        }
+
+        /*
+         * NOTE !!! in such a case there may be confusion with a host named 'a',
+         * but there is a limit to how smart we can be...
+         */
+        if ((pos == 1) && OsUtils.isWin32()) {
+            char drive = locSpec.charAt(0);
+            if (((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z'))) {
+                location.setPath(locSpec);
+                return location;
+            }
+        }
+
+        String login = locSpec.substring(0, pos);
+        ValidateUtils.checkTrue(pos < (locSpec.length() - 1), "Invalid remote specification (missing path): %s", locSpec);
+        location.setPath(locSpec.substring(pos + 1));
+
+        pos = login.indexOf(USERNAME_PART_SEPARATOR);
+        ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (missing username): %s", locSpec);
+        if (pos < 0) {
+            location.setHost(login);
+        } else {
+            location.setUsername(login.substring(0, pos));
+            ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (missing host): %s", locSpec);
+            location.setHost(login.substring(pos + 1));
+        }
+
+        return location;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
new file mode 100644
index 0000000..d0e611c
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface ScpReceiveLineHandler {
+    /**
+     * @param line  Received SCP input line
+     * @param isDir Does the input line refer to a directory
+     * @param time  The received {@link ScpTimestamp} - may be {@code null}
+     * @throws IOException If failed to process the line
+     */
+    void process(String line, boolean isDir, ScpTimestamp time) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
new file mode 100644
index 0000000..feeecbc
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
@@ -0,0 +1,73 @@
+/*
+ * 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.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.session.Session;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpSourceStreamResolver {
+    /**
+     * @return The uploaded file name
+     * @throws IOException If failed to resolve the name
+     */
+    String getFileName() throws IOException;
+
+    /**
+     * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
+     */
+    Path getEventListenerFilePath();
+
+    /**
+     * @return The permissions to be used for uploading a file
+     * @throws IOException If failed to generate the required permissions
+     */
+    Collection<PosixFilePermission> getPermissions() throws IOException;
+
+    /**
+     * @return The {@link ScpTimestamp} to use for uploading the file
+     * if {@code null} then no need to send this information
+     * @throws IOException If failed to generate the required data
+     */
+    ScpTimestamp getTimestamp() throws IOException;
+
+    /**
+     * @return An estimated size of the expected number of bytes to be uploaded.
+     * If non-positive then assumed to be unknown.
+     * @throws IOException If failed to generate an estimate
+     */
+    long getSize() throws IOException;
+
+    /**
+     * @param session The {@link Session} through which file is transmitted
+     * @param options The {@link OpenOption}s may be {@code null}/empty
+     * @return The {@link InputStream} containing the data to be uploaded
+     * @throws IOException If failed to create the stream
+     */
+    InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
new file mode 100644
index 0000000..9a70302
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
@@ -0,0 +1,67 @@
+/*
+ * 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.io.OutputStream;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.apache.sshd.common.session.Session;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpTargetStreamResolver {
+    /**
+     * Called when receiving a file in order to obtain an output stream
+     * for the incoming data
+     *
+     * @param session The associated {@link Session}
+     * @param name    File name as received from remote site
+     * @param length  Number of bytes expected to receive
+     * @param perms   The {@link Set} of {@link PosixFilePermission} expected
+     * @param options The {@link OpenOption}s to use - may be {@code null}/empty
+     * @return The {@link OutputStream} to write the incoming data
+     * @throws IOException If failed to create the stream
+     */
+    OutputStream resolveTargetStream(Session session, String name, long length,
+            Set<PosixFilePermission> perms, OpenOption... options) throws IOException;
+
+    /**
+     * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
+     */
+    Path getEventListenerFilePath();
+
+    /**
+     * Called after successful reception of the data (and after closing the stream)
+     *
+     * @param name     File name as received from remote site
+     * @param preserve If {@code true} then the resolver should attempt to preserve
+     *                 the specified permissions and timestamp
+     * @param perms    The {@link Set} of {@link PosixFilePermission} expected
+     * @param time     If not {@code null} then the required timestamp(s) on the
+     *                 incoming data
+     * @throws IOException If failed to post-process the incoming data
+     */
+    void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
new file mode 100644
index 0000000..e804de9
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
@@ -0,0 +1,69 @@
+/*
+ * 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.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * Represents an SCP timestamp definition
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpTimestamp {
+    private final long lastModifiedTime;
+    private final long lastAccessTime;
+
+    public ScpTimestamp(long modTime, long accTime) {
+        lastModifiedTime = modTime;
+        lastAccessTime = accTime;
+    }
+
+    public long getLastModifiedTime() {
+        return lastModifiedTime;
+    }
+
+    public long getLastAccessTime() {
+        return lastAccessTime;
+    }
+
+    @Override
+    public String toString() {
+        return "modified=" + new Date(lastModifiedTime)
+            + ";accessed=" + new Date(lastAccessTime);
+    }
+
+    /**
+     * @param line The time specification - format:
+     * {@code T<mtime-sec> <mtime-micros> <atime-sec> <atime-micros>}
+     * where specified times are in seconds since UTC
+     * @return The {@link ScpTimestamp} value with the timestamps converted to
+     * <U>milliseconds</U>
+     * @throws NumberFormatException if bad numerical values - <B>Note:</B>
+     * does not check if 1st character is 'T'.
+     * @see <A HREF="https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works">How the SCP protocol works</A>
+     */
+    public static ScpTimestamp parseTime(String line) throws NumberFormatException {
+        String[] numbers = GenericUtils.split(line.substring(1), ' ');
+        return new ScpTimestamp(TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[0])),
+                TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2])));
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
new file mode 100644
index 0000000..d7954e0
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
@@ -0,0 +1,105 @@
+/*
+ * 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.SshdEventListener;
+
+/**
+ * Can be registered in order to receive events about SCP transfers
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpTransferEventListener extends SshdEventListener {
+    enum FileOperation {
+        SEND,
+        RECEIVE
+    }
+
+    /**
+     * An &quot;empty&quot; implementation to be used instead of {@code null}s
+     */
+    ScpTransferEventListener EMPTY = new ScpTransferEventListener() {
+        @Override
+        public String toString() {
+            return "EMPTY";
+        }
+    };
+
+    /**
+     * @param op     The {@link FileOperation}
+     * @param file   The <U>local</U> referenced file {@link Path}
+     * @param length Size (in bytes) of transferred data
+     * @param perms  A {@link Set} of {@link PosixFilePermission}s to be applied
+     *               once transfer is complete
+     * @throws IOException If failed to handle the event
+     */
+    default void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) throws IOException {
+        // ignored
+    }
+
+    /**
+     * @param op     The {@link FileOperation}
+     * @param file   The <U>local</U> referenced file {@link Path}
+     * @param length Size (in bytes) of transferred data
+     * @param perms  A {@link Set} of {@link PosixFilePermission}s to be applied
+     *               once transfer is complete
+     * @param thrown The result of the operation attempt - if {@code null} then
+     *               reception was successful
+     * @throws IOException If failed to handle the event
+     */
+    default void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
+            throws IOException {
+                // ignored
+    }
+
+    /**
+     * @param op    The {@link FileOperation}
+     * @param file  The <U>local</U> referenced folder {@link Path}
+     * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
+     *              once transfer is complete
+     * @throws IOException If failed to handle the event
+     */
+    default void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
+        // ignored
+    }
+
+    /**
+     * @param op     The {@link FileOperation}
+     * @param file   The <U>local</U> referenced file {@link Path}
+     * @param perms  A {@link Set} of {@link PosixFilePermission}s to be applied
+     *               once transfer is complete
+     * @param thrown The result of the operation attempt - if {@code null} then
+     *               reception was successful
+     * @throws IOException If failed to handle the event
+     */
+    default void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
+            throws IOException {
+        // ignored
+    }
+
+    static <L extends ScpTransferEventListener> L validateListener(L listener) {
+        return SshdEventListener.validateListener(listener, ScpTransferEventListener.class.getSimpleName());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
new file mode 100644
index 0000000..bb6ae3b
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
@@ -0,0 +1,75 @@
+/*
+ * 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.helpers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpSourceStreamResolver;
+import org.apache.sshd.common.scp.ScpTargetStreamResolver;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpFileOpener extends AbstractLoggingBean implements ScpFileOpener {
+    public static final DefaultScpFileOpener INSTANCE = new DefaultScpFileOpener();
+
+    public DefaultScpFileOpener() {
+        super();
+    }
+
+    @Override
+    public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("openRead({}) file={}, options={}",
+                      session, file, Arrays.toString(options));
+        }
+
+        return Files.newInputStream(file, options);
+    }
+
+    @Override
+    public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("openWrite({}) file={}, options={}",
+                      session, file, Arrays.toString(options));
+        }
+
+        return Files.newOutputStream(file, options);
+    }
+
+    @Override
+    public ScpSourceStreamResolver createScpSourceStreamResolver(Path path) throws IOException {
+        return new LocalFileScpSourceStreamResolver(path, this);
+    }
+
+    @Override
+    public ScpTargetStreamResolver createScpTargetStreamResolver(Path path) throws IOException {
+        return new LocalFileScpTargetStreamResolver(path, this);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
new file mode 100644
index 0000000..8ce9b61
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
@@ -0,0 +1,97 @@
+/*
+ * 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.helpers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+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.PosixFilePermission;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpSourceStreamResolver;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.Session;
+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 class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implements ScpSourceStreamResolver {
+    protected final Path path;
+    protected final ScpFileOpener opener;
+    protected final Path name;
+    protected final Set<PosixFilePermission> perms;
+    protected final long size;
+    protected final ScpTimestamp time;
+
+    public LocalFileScpSourceStreamResolver(Path path, ScpFileOpener opener) throws IOException {
+        this.path = Objects.requireNonNull(path, "No path specified");
+        this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
+        this.name = path.getFileName();
+        this.perms = IoUtils.getPermissions(path);
+
+        BasicFileAttributes basic = Files.getFileAttributeView(path, BasicFileAttributeView.class).readAttributes();
+        this.size = basic.size();
+        this.time = new ScpTimestamp(basic.lastModifiedTime().toMillis(), basic.lastAccessTime().toMillis());
+    }
+
+    @Override
+    public String getFileName() throws IOException {
+        return name.toString();
+    }
+
+    @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 Path getEventListenerFilePath() {
+        return path;
+    }
+
+    @Override
+    public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
+        return opener.openRead(session, getEventListenerFilePath(), options);
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(getEventListenerFilePath());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
new file mode 100644
index 0000000..6b57443
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.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.common.scp.helpers;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.Files;
+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.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpTargetStreamResolver;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.Session;
+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 class LocalFileScpTargetStreamResolver extends AbstractLoggingBean implements ScpTargetStreamResolver {
+    protected final Path path;
+    protected final ScpFileOpener opener;
+    protected final Boolean status;
+    private Path file;
+
+    public LocalFileScpTargetStreamResolver(Path path, ScpFileOpener opener) throws IOException {
+        LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
+        this.status = IoUtils.checkFileExists(path, linkOptions);
+        if (status == null) {
+            throw new AccessDeniedException("Receive target file path existence status cannot be determined: " + path);
+        }
+
+        this.path = path;
+        this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
+    }
+
+    @Override
+    public OutputStream resolveTargetStream(Session session, String name, long length,
+            Set<PosixFilePermission> perms, OpenOption... options) throws IOException {
+        if (file != null) {
+            throw new StreamCorruptedException("resolveTargetStream(" + name + ")[" + perms + "] already resolved: " + file);
+        }
+
+        LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
+        if (status && Files.isDirectory(path, linkOptions)) {
+            String localName = name.replace('/', File.separatorChar);   // in case we are running on Windows
+            file = path.resolve(localName);
+        } else if (status && Files.isRegularFile(path, linkOptions)) {
+            file = path;
+        } else if (!status) {
+            Path parent = path.getParent();
+
+            Boolean parentStatus = IoUtils.checkFileExists(parent, linkOptions);
+            if (parentStatus == null) {
+                throw new AccessDeniedException("Receive file parent (" + parent + ") existence status cannot be determined for " + path);
+            }
+
+            if (parentStatus && Files.isDirectory(parent, linkOptions)) {
+                file = path;
+            }
+        }
+
+        if (file == null) {
+            throw new IOException("Can not write to " + path);
+        }
+
+        Boolean fileStatus = IoUtils.checkFileExists(file, linkOptions);
+        if (fileStatus == null) {
+            throw new AccessDeniedException("Receive file existence status cannot be determined: " + file);
+        }
+
+        if (fileStatus) {
+            if (Files.isDirectory(file, linkOptions)) {
+                throw new IOException("File is a directory: " + file);
+            }
+
+            if (!Files.isWritable(file)) {
+                throw new IOException("Can not write to file: " + file);
+            }
+        }
+
+        if (log.isTraceEnabled()) {
+            log.trace("resolveTargetStream(" + name + "): " + file);
+        }
+
+        return opener.openWrite(session, file, options);
+    }
+
+    @Override
+    public Path getEventListenerFilePath() {
+        if (file == null) {
+            return path;
+        } else {
+            return file;
+        }
+    }
+
+    @Override
+    public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+        if (file == null) {
+            throw new StreamCorruptedException("postProcessReceivedData(" + name + ")[" + perms + "] No currently resolved data");
+        }
+
+        if (preserve) {
+            updateFileProperties(name, file, perms, time);
+        }
+    }
+
+    protected void updateFileProperties(String name, Path path, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+        boolean traceEnabled = log.isTraceEnabled();
+        if (traceEnabled) {
+            log.trace("updateFileProperties(" + name + ")[" + path + "] permissions: " + perms);
+        }
+        IoUtils.setPermissions(path, perms);
+
+        if (time != null) {
+            BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class);
+            FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS);
+            FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS);
+            if (traceEnabled) {
+                log.trace("updateFileProperties(" + name + ")[" + path + "] last-modified=" + lastModified + ", last-access=" + lastAccess);
+            }
+
+            view.setTimes(lastModified, lastAccess, null);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(getEventListenerFilePath());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
new file mode 100644
index 0000000..e80f791
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
@@ -0,0 +1,350 @@
+/*
+ * 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.server.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileSystem;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.sshd.common.file.FileSystemAware;
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionHolder;
+
+/**
+ * This commands provide SCP support on both server and client side.
+ * Permissions and preservation of access / modification times on files
+ * are not supported.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpCommand
+        extends AbstractLoggingBean
+        implements Command, Runnable, FileSystemAware, SessionAware,
+                   SessionHolder<Session>, ServerSessionHolder, ExecutorServiceCarrier {
+
+    protected final String name;
+    protected final int sendBufferSize;
+    protected final int receiveBufferSize;
+    protected final ScpFileOpener opener;
+    protected boolean optR;
+    protected boolean optT;
+    protected boolean optF;
+    protected boolean optD;
+    protected boolean optP; // TODO: handle modification times
+    protected FileSystem fileSystem;
+    protected String path;
+    protected InputStream in;
+    protected OutputStream out;
+    protected OutputStream err;
+    protected ExitCallback callback;
+    protected IOException error;
+    protected Future<?> pendingFuture;
+    protected ScpTransferEventListener listener;
+    protected ServerSession serverSession;
+
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
+
+    /**
+     * @param command         The command to be executed
+     * @param executorService An {@link ExecutorService} to be used when
+     *                        {@link #start(Environment)}-ing execution. If {@code null} an ad-hoc
+     *                        single-threaded service is created and used.
+     * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()}
+     *                        will be called when command terminates - unless it is the ad-hoc
+     *                        service, which will be shutdown regardless
+     * @param sendSize        Size (in bytes) of buffer to use when sending files
+     * @param receiveSize     Size (in bytes) of buffer to use when receiving files
+     * @param fileOpener      The {@link ScpFileOpener} - if {@code null} then {@link DefaultScpFileOpener} is used
+     * @param eventListener   An {@link ScpTransferEventListener} - may be {@code null}
+     * @see ThreadUtils#newSingleThreadExecutor(String)
+     * @see ScpHelper#MIN_SEND_BUFFER_SIZE
+     * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
+     */
+    public ScpCommand(String command,
+            ExecutorService executorService, boolean shutdownOnExit,
+            int sendSize, int receiveSize,
+            ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
+        name = command;
+
+        if (executorService == null) {
+            String poolName = command.replace(' ', '_').replace('/', ':');
+            this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
+            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
+        } else {
+            this.executorService = executorService;
+            this.shutdownOnExit = shutdownOnExit;
+        }
+
+        if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
+            throw new IllegalArgumentException("<ScpCommmand>(" + command + ") send buffer size "
+                    + "(" + sendSize + ") below minimum required "
+                    + "(" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
+        }
+        sendBufferSize = sendSize;
+
+        if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
+            throw new IllegalArgumentException("<ScpCommmand>(" + command + ") receive buffer size "
+                    + "(" + sendSize + ") below minimum required "
+                    + "(" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
+        }
+        receiveBufferSize = receiveSize;
+
+        opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
+        listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
+
+        boolean debugEnabled = log.isDebugEnabled();
+        if (debugEnabled) {
+            log.debug("Executing command {}", command);
+        }
+
+        String[] args = GenericUtils.split(command, ' ');
+        int numArgs = GenericUtils.length(args);
+        for (int i = 1; i < numArgs; i++) {
+            String argVal = args[i];
+            if (argVal.charAt(0) == '-') {
+                for (int j = 1; j < argVal.length(); j++) {
+                    char option = argVal.charAt(j);
+                    switch (option) {
+                        case 'f':
+                            optF = true;
+                            break;
+                        case 'p':
+                            optP = true;
+                            break;
+                        case 'r':
+                            optR = true;
+                            break;
+                        case 't':
+                            optT = true;
+                            break;
+                        case 'd':
+                            optD = true;
+                            break;
+                        default:  // ignored
+                            if (debugEnabled) {
+                                log.debug("Unknown flag ('{}') in command={}", option, command);
+                            }
+                    }
+                }
+            } else {
+                String prevArg = args[i - 1];
+                path = command.substring(command.indexOf(prevArg) + prevArg.length() + 1);
+
+                int pathLen = path.length();
+                char startDelim = path.charAt(0);
+                char endDelim = (pathLen > 2) ? path.charAt(pathLen - 1) : '\0';
+                // remove quotes
+                if ((pathLen > 2) && (startDelim == endDelim) && ((startDelim == '\'') || (startDelim == '"'))) {
+                    path = path.substring(1, pathLen - 1);
+                }
+                break;
+            }
+        }
+
+        if ((!optF) && (!optT)) {
+            error = new IOException("Either -f or -t option should be set for " + command);
+        }
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    @Override
+    public Session getSession() {
+        return getServerSession();
+    }
+
+    @Override
+    public ServerSession getServerSession() {
+        return serverSession;
+    }
+
+    @Override
+    public void setSession(ServerSession session) {
+        serverSession = session;
+    }
+
+    @Override
+    public void setInputStream(InputStream in) {
+        this.in = in;
+    }
+
+    @Override
+    public void setOutputStream(OutputStream out) {
+        this.out = out;
+    }
+
+    @Override
+    public void setErrorStream(OutputStream err) {
+        this.err = err;
+    }
+
+    @Override
+    public void setExitCallback(ExitCallback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    public void setFileSystem(FileSystem fs) {
+        this.fileSystem = fs;
+    }
+
+    @Override
+    public void start(Environment env) throws IOException {
+        if (error != null) {
+            throw error;
+        }
+
+        try {
+            ExecutorService executors = getExecutorService();
+            pendingFuture = executors.submit(this);
+        } catch (RuntimeException e) {    // e.g., RejectedExecutionException
+            log.error("Failed (" + e.getClass().getSimpleName() + ") to start command=" + name + ": " + e.getMessage(), e);
+            throw new IOException(e);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        // if thread has not completed, cancel it
+        boolean debugEnabled = log.isDebugEnabled();
+        if ((pendingFuture != null) && (!pendingFuture.isDone())) {
+            boolean result = pendingFuture.cancel(true);
+            // TODO consider waiting some reasonable (?) amount of time for cancellation
+            if (debugEnabled) {
+                log.debug("destroy() - cancel pending future=" + result);
+            }
+        }
+
+        pendingFuture = null;
+
+        ExecutorService executors = getExecutorService();
+        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
+            Collection<Runnable> runners = executors.shutdownNow();
+            if (debugEnabled) {
+                log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
+            }
+        }
+        this.executorService = null;
+
+        try {
+            fileSystem.close();
+        } catch (UnsupportedOperationException e) {
+            // Ignore
+        } catch (IOException e) {
+            log.debug("Error closing FileSystem", e);
+        }
+    }
+
+    @Override
+    public void run() {
+        int exitValue = ScpHelper.OK;
+        String exitMessage = null;
+        ScpHelper helper = new ScpHelper(getServerSession(), in, out, fileSystem, opener, listener);
+        try {
+            if (optT) {
+                helper.receive(helper.resolveLocalPath(path), optR, optD, optP, receiveBufferSize);
+            } else if (optF) {
+                helper.send(Collections.singletonList(path), optR, optP, sendBufferSize);
+            } else {
+                throw new IOException("Unsupported mode");
+            }
+        } catch (IOException e) {
+            ServerSession session = getServerSession();
+            boolean debugEnabled = log.isDebugEnabled();
+            try {
+                Integer statusCode = null;
+                if (e instanceof ScpException) {
+                    statusCode = ((ScpException) e).getExitStatus();
+                }
+                exitValue = (statusCode == null) ? ScpHelper.ERROR : statusCode;
+                // this is an exception so status cannot be OK/WARNING
+                if ((exitValue == ScpHelper.OK) || (exitValue == ScpHelper.WARNING)) {
+                    if (debugEnabled) {
+                        log.debug("run({})[{}] normalize status code={}", session, name, exitValue);
+                    }
+                    exitValue = ScpHelper.ERROR;
+                }
+                exitMessage = GenericUtils.trimToEmpty(e.getMessage());
+                writeCommandResponseMessage(name, exitValue, exitMessage);
+            } catch (IOException e2) {
+                if (debugEnabled) {
+                    log.debug("run({})[{}] Failed ({}) to send error response: {}",
+                              session, name, e.getClass().getSimpleName(), e.getMessage());
+                }
+                if (log.isTraceEnabled()) {
+                    log.trace("run(" + session + ")[" + name + "] error response failure details", e2);
+                }
+            }
+
+            if (debugEnabled) {
+                log.debug("run({})[{}] Failed ({}) to run command: {}",
+                          session, name, e.getClass().getSimpleName(), e.getMessage());
+            }
+            if (log.isTraceEnabled()) {
+                log.trace("run(" + session + ")[" + name + "] command execution failure details", e);
+            }
+        } finally {
+            if (callback != null) {
+                callback.onExit(exitValue, GenericUtils.trimToEmpty(exitMessage));
+            }
+        }
+    }
+
+    protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("writeCommandResponseMessage({}) command='{}', exit-status={}: {}",
+                      getServerSession(), command, exitValue, exitMessage);
+        }
+        ScpHelper.sendResponseMessage(out, exitValue, exitMessage);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(" + getSession() + ") " + name;
+    }
+}
\ No newline at end of file


[7/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
deleted file mode 100644
index d929a07..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/common/scp/ScpException.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpException.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpException.java
deleted file mode 100644
index 9ae17c7..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpException.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
deleted file mode 100644
index 78e033f..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * 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-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
deleted file mode 100644
index b492129..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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);
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
deleted file mode 100644
index 1cbea2d..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
+++ /dev/null
@@ -1,837 +0,0 @@
-/*
- * 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.ByteArrayOutputStream;
-import java.io.EOFException;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.StreamCorruptedException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.DirectoryStream;
-import java.nio.file.FileSystem;
-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.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.file.util.MockPath;
-import org.apache.sshd.common.scp.ScpTransferEventListener.FileOperation;
-import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionHolder;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.LimitInputStream;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@SuppressWarnings("PMD.AvoidUsingOctalValues")
-public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Session> {
-    /**
-     * Command prefix used to identify SCP commands
-     */
-    public static final String SCP_COMMAND_PREFIX = "scp";
-
-    public static final int OK = 0;
-    public static final int WARNING = 1;
-    public static final int ERROR = 2;
-
-    /**
-     * Default size (in bytes) of send / receive buffer size
-     */
-    public static final int DEFAULT_COPY_BUFFER_SIZE = IoUtils.DEFAULT_COPY_SIZE;
-    public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
-    public static final int DEFAULT_SEND_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
-
-    /**
-     * The minimum size for sending / receiving files
-     */
-    public static final int MIN_COPY_BUFFER_SIZE = Byte.MAX_VALUE;
-    public static final int MIN_RECEIVE_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
-    public static final int MIN_SEND_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
-
-    public static final int S_IRUSR = 0000400;
-    public static final int S_IWUSR = 0000200;
-    public static final int S_IXUSR = 0000100;
-    public static final int S_IRGRP = 0000040;
-    public static final int S_IWGRP = 0000020;
-    public static final int S_IXGRP = 0000010;
-    public static final int S_IROTH = 0000004;
-    public static final int S_IWOTH = 0000002;
-    public static final int S_IXOTH = 0000001;
-
-    public static final String DEFAULT_DIR_OCTAL_PERMISSIONS = "0755";
-    public static final String DEFAULT_FILE_OCTAL_PERMISSIONS = "0644";
-
-    protected final InputStream in;
-    protected final OutputStream out;
-    protected final FileSystem fileSystem;
-    protected final ScpFileOpener opener;
-    protected final ScpTransferEventListener listener;
-
-    private final Session sessionInstance;
-
-    public ScpHelper(Session session, InputStream in, OutputStream out,
-            FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) {
-        this.sessionInstance = Objects.requireNonNull(session, "No session");
-        this.in = Objects.requireNonNull(in, "No input stream");
-        this.out = Objects.requireNonNull(out, "No output stream");
-        this.fileSystem = fileSystem;
-        this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
-        this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
-    }
-
-    @Override
-    public Session getSession() {
-        return sessionInstance;
-    }
-
-    public void receiveFileStream(OutputStream local, int bufferSize) throws IOException {
-        receive((line, isDir, timestamp) -> {
-            if (isDir) {
-                throw new StreamCorruptedException("Cannot download a directory into a file stream: " + line);
-            }
-
-            Path path = new MockPath(line);
-            receiveStream(line, new ScpTargetStreamResolver() {
-                @Override
-                @SuppressWarnings("synthetic-access")
-                public OutputStream resolveTargetStream(
-                        Session session, String name, long length, Set<PosixFilePermission> perms, OpenOption... options)
-                            throws IOException {
-                    if (log.isDebugEnabled()) {
-                        log.debug("resolveTargetStream({}) name={}, perms={}, len={} - started local stream download",
-                                  ScpHelper.this, name, perms, length);
-                    }
-                    return local;
-                }
-
-                @Override
-                public Path getEventListenerFilePath() {
-                    return path;
-                }
-
-                @Override
-                @SuppressWarnings("synthetic-access")
-                public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
-                    if (log.isDebugEnabled()) {
-                        log.debug("postProcessReceivedData({}) name={}, perms={}, preserve={} time={}",
-                                  ScpHelper.this, name, perms, preserve, time);
-                    }
-                }
-
-                @Override
-                public String toString() {
-                    return line;
-                }
-            }, timestamp, false, bufferSize);
-        });
-    }
-
-    public void receive(Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize) throws IOException {
-        Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
-        Path path = opener.resolveIncomingReceiveLocation(localPath, recursive, shouldBeDir, preserve);
-        receive((line, isDir, time) -> {
-            if (recursive && isDir) {
-                receiveDir(line, path, time, preserve, bufferSize);
-            } else {
-                receiveFile(line, path, time, preserve, bufferSize);
-            }
-        });
-    }
-
-    protected void receive(ScpReceiveLineHandler handler) throws IOException {
-        ack();
-        ScpTimestamp time = null;
-        for (;;) {
-            String line;
-            boolean isDir = false;
-            int c = readAck(true);
-            switch (c) {
-                case -1:
-                    return;
-                case 'D':
-                    isDir = true;
-                    line = String.valueOf((char) c) + readLine();
-                    if (log.isDebugEnabled()) {
-                        log.debug("receive({}) - Received 'D' header: {}", this, line);
-                    }
-                    break;
-                case 'C':
-                    line = String.valueOf((char) c) + readLine();
-                    if (log.isDebugEnabled()) {
-                        log.debug("receive({}) - Received 'C' header: {}", this, line);
-                    }
-                    break;
-                case 'T':
-                    line = String.valueOf((char) c) + readLine();
-                    if (log.isDebugEnabled()) {
-                        log.debug("receive({}) - Received 'T' header: {}", this, line);
-                    }
-                    time = ScpTimestamp.parseTime(line);
-                    ack();
-                    continue;
-                case 'E':
-                    line = String.valueOf((char) c) + readLine();
-                    if (log.isDebugEnabled()) {
-                        log.debug("receive({}) - Received 'E' header: {}", this, line);
-                    }
-                    ack();
-                    return;
-                default:
-                    //a real ack that has been acted upon already
-                    continue;
-            }
-
-            try {
-                handler.process(line, isDir, time);
-            } finally {
-                time = null;
-            }
-        }
-    }
-
-    public void receiveDir(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
-        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
-        if (log.isDebugEnabled()) {
-            log.debug("receiveDir({})[{}] Receiving directory {} - preserve={}, time={}, buffer-size={}",
-                      this, header, path, preserve, time, bufferSize);
-        }
-        if (!header.startsWith("D")) {
-            throw new IOException("Expected a 'D; message but got '" + header + "'");
-        }
-
-        Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
-        int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
-        String name = header.substring(header.indexOf(' ', 6) + 1);
-        if (length != 0) {
-            throw new IOException("Expected 0 length for directory=" + name + " but got " + length);
-        }
-
-        Path file = opener.resolveIncomingFilePath(path, name, preserve, perms, time);
-
-        ack();
-
-        time = null;
-        listener.startFolderEvent(FileOperation.RECEIVE, path, perms);
-        try {
-            for (;;) {
-                header = readLine();
-                if (log.isDebugEnabled()) {
-                    log.debug("receiveDir({})[{}] Received header: {}", this, file, header);
-                }
-                if (header.startsWith("C")) {
-                    receiveFile(header, file, time, preserve, bufferSize);
-                    time = null;
-                } else if (header.startsWith("D")) {
-                    receiveDir(header, file, time, preserve, bufferSize);
-                    time = null;
-                } else if (header.equals("E")) {
-                    ack();
-                    break;
-                } else if (header.startsWith("T")) {
-                    time = ScpTimestamp.parseTime(header);
-                    ack();
-                } else {
-                    throw new IOException("Unexpected message: '" + header + "'");
-                }
-            }
-        } catch (IOException | RuntimeException e) {
-            listener.endFolderEvent(FileOperation.RECEIVE, path, perms, e);
-            throw e;
-        }
-        listener.endFolderEvent(FileOperation.RECEIVE, path, perms, null);
-    }
-
-    public void receiveFile(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
-        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
-        if (log.isDebugEnabled()) {
-            log.debug("receiveFile({})[{}] Receiving file {} - preserve={}, time={}, buffer-size={}",
-                      this, header, path, preserve, time, bufferSize);
-        }
-
-        receiveStream(header, opener.createScpTargetStreamResolver(path), time, preserve, bufferSize);
-    }
-
-    public void receiveStream(String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
-        if (!header.startsWith("C")) {
-            throw new IOException("receiveStream(" + resolver + ") Expected a C message but got '" + header + "'");
-        }
-
-        if (bufferSize < MIN_RECEIVE_BUFFER_SIZE) {
-            throw new IOException("receiveStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_RECEIVE_BUFFER_SIZE + ")");
-        }
-
-        Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
-        long length = Long.parseLong(header.substring(6, header.indexOf(' ', 6)));
-        String name = header.substring(header.indexOf(' ', 6) + 1);
-        if (length < 0L) { // TODO consider throwing an exception...
-            log.warn("receiveStream({})[{}] bad length in header: {}", this, resolver, header);
-        }
-
-        // if file size is less than buffer size allocate only expected file size
-        int bufSize;
-        boolean debugEnabled = log.isDebugEnabled();
-        if (length == 0L) {
-            if (debugEnabled) {
-                log.debug("receiveStream({})[{}] zero file size (perhaps special file) using copy buffer size={}",
-                          this, resolver, MIN_RECEIVE_BUFFER_SIZE);
-            }
-            bufSize = MIN_RECEIVE_BUFFER_SIZE;
-        } else {
-            bufSize = (int) Math.min(length, bufferSize);
-        }
-
-        if (bufSize < 0) { // TODO consider throwing an exception
-            log.warn("receiveStream({})[{}] bad buffer size ({}) using default ({})",
-                     this, resolver, bufSize, MIN_RECEIVE_BUFFER_SIZE);
-            bufSize = MIN_RECEIVE_BUFFER_SIZE;
-        }
-
-        try (
-                InputStream is = new LimitInputStream(this.in, length);
-                OutputStream os = resolver.resolveTargetStream(getSession(), name, length, perms)
-        ) {
-            ack();
-
-            Path file = resolver.getEventListenerFilePath();
-            listener.startFileEvent(FileOperation.RECEIVE, file, length, perms);
-            try {
-                IoUtils.copy(is, os, bufSize);
-            } catch (IOException | RuntimeException e) {
-                listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, e);
-                throw e;
-            }
-            listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, null);
-        }
-
-        resolver.postProcessReceivedData(name, preserve, perms, time);
-
-        ack();
-
-        int replyCode = readAck(false);
-        if (debugEnabled) {
-            log.debug("receiveStream({})[{}] ack reply code={}", this, resolver, replyCode);
-        }
-        validateAckReplyCode("receiveStream", resolver, replyCode, false);
-    }
-
-    public String readLine() throws IOException {
-        return readLine(false);
-    }
-
-    public String readLine(boolean canEof) throws IOException {
-        try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Byte.MAX_VALUE)) {
-            for (;;) {
-                int c = in.read();
-                if (c == '\n') {
-                    return baos.toString(StandardCharsets.UTF_8.name());
-                } else if (c == -1) {
-                    if (!canEof) {
-                        throw new EOFException("EOF while await end of line");
-                    }
-                    return null;
-                } else {
-                    baos.write(c);
-                }
-            }
-        }
-    }
-
-    public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
-        int readyCode = readAck(false);
-        boolean debugEnabled = log.isDebugEnabled();
-        if (debugEnabled) {
-            log.debug("send({}) ready code={}", paths, readyCode);
-        }
-        validateOperationReadyCode("send", "Paths", readyCode, false);
-
-        LinkOption[] options = IoUtils.getLinkOptions(true);
-        for (String pattern : paths) {
-            pattern = pattern.replace('/', File.separatorChar);
-
-            int idx = pattern.indexOf('*'); // check if wildcard used
-            if (idx >= 0) {
-                String basedir = "";
-                String fixedPart = pattern.substring(0, idx);
-                int lastSep = fixedPart.lastIndexOf(File.separatorChar);
-                if (lastSep >= 0) {
-                    basedir = pattern.substring(0, lastSep);
-                    pattern = pattern.substring(lastSep + 1);
-                }
-
-                Iterable<String> included = opener.getMatchingFilesToSend(basedir, pattern);
-                for (String path : included) {
-                    Path file = resolveLocalPath(basedir, path);
-                    if (opener.sendAsRegularFile(file, options)) {
-                        sendFile(file, preserve, bufferSize);
-                    } else if (opener.sendAsDirectory(file, options)) {
-                        if (!recursive) {
-                            if (debugEnabled) {
-                                log.debug("send({}) {}: not a regular file", this, path);
-                            }
-                            sendWarning(path.replace(File.separatorChar, '/') + " not a regular file");
-                        } else {
-                            sendDir(file, preserve, bufferSize);
-                        }
-                    } else {
-                        if (debugEnabled) {
-                            log.debug("send({}) {}: unknown file type", this, path);
-                        }
-                        sendWarning(path.replace(File.separatorChar, '/') + " unknown file type");
-                    }
-                }
-            } else {
-                send(resolveLocalPath(pattern), recursive, preserve, bufferSize, options);
-            }
-        }
-    }
-
-    public void sendPaths(Collection<? extends Path> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
-        int readyCode = readAck(false);
-        if (log.isDebugEnabled()) {
-            log.debug("sendPaths({}) ready code={}", paths, readyCode);
-        }
-        validateOperationReadyCode("sendPaths", "Paths", readyCode, false);
-
-        LinkOption[] options = IoUtils.getLinkOptions(true);
-        for (Path file : paths) {
-            send(file, recursive, preserve, bufferSize, options);
-        }
-    }
-
-    protected void send(Path local, boolean recursive, boolean preserve, int bufferSize, LinkOption... options) throws IOException {
-        Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
-        Path file = opener.resolveOutgoingFilePath(localPath, options);
-        if (opener.sendAsRegularFile(file, options)) {
-            sendFile(file, preserve, bufferSize);
-        } else if (opener.sendAsDirectory(file, options)) {
-            if (!recursive) {
-                throw new IOException(file + " not a regular file");
-            } else {
-                sendDir(file, preserve, bufferSize);
-            }
-        } else {
-            throw new IOException(file + ": unknown file type");
-        }
-    }
-
-    public Path resolveLocalPath(String basedir, String subpath) throws IOException {
-        if (GenericUtils.isEmpty(basedir)) {
-            return resolveLocalPath(subpath);
-        } else {
-            return resolveLocalPath(basedir + File.separator + subpath);
-        }
-    }
-
-    /**
-     * @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
-     */
-    public Path resolveLocalPath(String commandPath) throws IOException, InvalidPathException {
-        Path p = opener.resolveLocalPath(fileSystem, commandPath);
-        if (log.isTraceEnabled()) {
-            log.trace("resolveLocalPath({}) {}: {}", this, commandPath, p);
-        }
-
-        return p;
-    }
-
-    public void sendFile(Path local, boolean preserve, int bufferSize) throws IOException {
-        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
-        if (log.isDebugEnabled()) {
-            log.debug("sendFile({})[preserve={},buffer-size={}] Sending file {}", this, preserve, bufferSize, path);
-        }
-
-        sendStream(opener.createScpSourceStreamResolver(path), preserve, bufferSize);
-    }
-
-    public void sendStream(ScpSourceStreamResolver resolver, boolean preserve, int bufferSize) throws IOException {
-        if (bufferSize < MIN_SEND_BUFFER_SIZE) {
-            throw new IOException("sendStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_SEND_BUFFER_SIZE + ")");
-        }
-
-        long fileSize = resolver.getSize();
-        // if file size is less than buffer size allocate only expected file size
-        int bufSize;
-        boolean debugEnabled = log.isDebugEnabled();
-        if (fileSize <= 0L) {
-            if (debugEnabled) {
-                log.debug("sendStream({})[{}] unknown file size ({}) perhaps special file - using copy buffer size={}",
-                          this, resolver, fileSize, MIN_SEND_BUFFER_SIZE);
-            }
-            bufSize = MIN_SEND_BUFFER_SIZE;
-        } else {
-            bufSize = (int) Math.min(fileSize, bufferSize);
-        }
-
-        if (bufSize < 0) { // TODO consider throwing an exception
-            log.warn("sendStream({})[{}] bad buffer size ({}) using default ({})",
-                     this, resolver, bufSize, MIN_SEND_BUFFER_SIZE);
-            bufSize = MIN_SEND_BUFFER_SIZE;
-        }
-
-        ScpTimestamp time = resolver.getTimestamp();
-        if (preserve && (time != null)) {
-            String cmd = "T" + TimeUnit.MILLISECONDS.toSeconds(time.getLastModifiedTime())
-                    + " " + "0" + " " + TimeUnit.MILLISECONDS.toSeconds(time.getLastAccessTime())
-                    + " " + "0";
-            if (debugEnabled) {
-                log.debug("sendStream({})[{}] send timestamp={} command: {}", this, resolver, time, cmd);
-            }
-            out.write(cmd.getBytes(StandardCharsets.UTF_8));
-            out.write('\n');
-            out.flush();
-
-            int readyCode = readAck(false);
-            if (debugEnabled) {
-                log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, cmd, readyCode);
-            }
-            validateAckReplyCode(cmd, resolver, readyCode, false);
-        }
-
-        Set<PosixFilePermission> perms = EnumSet.copyOf(resolver.getPermissions());
-        String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_FILE_OCTAL_PERMISSIONS : getOctalPermissions(perms);
-        String fileName = resolver.getFileName();
-        String cmd = "C" + octalPerms + " " + fileSize + " " + fileName;
-        if (debugEnabled) {
-            log.debug("sendStream({})[{}] send 'C' command: {}", this, resolver, cmd);
-        }
-        out.write(cmd.getBytes(StandardCharsets.UTF_8));
-        out.write('\n');
-        out.flush();
-
-        int readyCode = readAck(false);
-        if (debugEnabled) {
-            log.debug("sendStream({})[{}] command='{}' ready code={}",
-                      this, resolver, cmd.substring(0, cmd.length() - 1), readyCode);
-        }
-        validateAckReplyCode(cmd, resolver, readyCode, false);
-
-        try (InputStream in = resolver.resolveSourceStream(getSession())) {
-            Path path = resolver.getEventListenerFilePath();
-            listener.startFileEvent(FileOperation.SEND, path, fileSize, perms);
-            try {
-                IoUtils.copy(in, out, bufSize);
-            } catch (IOException | RuntimeException e) {
-                listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, e);
-                throw e;
-            }
-            listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, null);
-        }
-        ack();
-
-        readyCode = readAck(false);
-        if (debugEnabled) {
-            log.debug("sendStream({})[{}] command='{}' reply code={}", this, resolver, cmd, readyCode);
-        }
-        validateAckReplyCode("sendStream", resolver, readyCode, false);
-    }
-
-    protected void validateOperationReadyCode(String command, Object location, int readyCode, boolean eofAllowed) throws IOException {
-        validateCommandStatusCode(command, location, readyCode, eofAllowed);
-    }
-
-    protected void validateAckReplyCode(String command, Object location, int replyCode, boolean eofAllowed) throws IOException {
-        validateCommandStatusCode(command, location, replyCode, eofAllowed);
-    }
-
-    protected void validateCommandStatusCode(String command, Object location, int statusCode, boolean eofAllowed) throws IOException {
-        switch (statusCode) {
-            case -1:
-                if (!eofAllowed) {
-                    throw new EOFException("Unexpected EOF for command='" + command + "' on " + location);
-                }
-                break;
-            case OK:
-                break;
-            case WARNING:
-                break;
-            default:
-                throw new ScpException("Bad reply code (" + statusCode + ") for command='" + command + "' on " + location, statusCode);
-        }
-    }
-
-    public void sendDir(Path local, boolean preserve, int bufferSize) throws IOException {
-        Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
-        boolean debugEnabled = log.isDebugEnabled();
-        if (debugEnabled) {
-            log.debug("sendDir({}) Sending directory {} - preserve={}, buffer-size={}",
-                      this, path, preserve, bufferSize);
-        }
-
-        LinkOption[] options = IoUtils.getLinkOptions(true);
-        if (preserve) {
-            BasicFileAttributes basic = opener.getLocalBasicFileAttributes(path, options);
-            FileTime lastModified = basic.lastModifiedTime();
-            FileTime lastAccess = basic.lastAccessTime();
-            String cmd = "T" + lastModified.to(TimeUnit.SECONDS) + " "
-                    + "0" + " " + lastAccess.to(TimeUnit.SECONDS) + " "
-                    + "0";
-            if (debugEnabled) {
-                log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}",
-                          this, path, lastModified,  lastAccess, cmd);
-            }
-
-            out.write(cmd.getBytes(StandardCharsets.UTF_8));
-            out.write('\n');
-            out.flush();
-
-            int readyCode = readAck(false);
-            if (debugEnabled) {
-                if (debugEnabled) {
-                    log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode);
-                }
-            }
-            validateAckReplyCode(cmd, path, readyCode, false);
-        }
-
-        Set<PosixFilePermission> perms = opener.getLocalFilePermissions(path, options);
-        String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_DIR_OCTAL_PERMISSIONS : getOctalPermissions(perms);
-        String cmd = "D" + octalPerms + " " + "0" + " " + Objects.toString(path.getFileName(), null);
-        if (debugEnabled) {
-            log.debug("sendDir({})[{}] send 'D' command: {}", this, path, cmd);
-        }
-        out.write(cmd.getBytes(StandardCharsets.UTF_8));
-        out.write('\n');
-        out.flush();
-
-        int readyCode = readAck(false);
-        if (debugEnabled) {
-            log.debug("sendDir({})[{}] command='{}' ready code={}",
-                      this, path, cmd.substring(0, cmd.length() - 1), readyCode);
-        }
-        validateAckReplyCode(cmd, path, readyCode, false);
-
-        try (DirectoryStream<Path> children = opener.getLocalFolderChildren(path)) {
-            listener.startFolderEvent(FileOperation.SEND, path, perms);
-
-            try {
-                for (Path child : children) {
-                    if (opener.sendAsRegularFile(child, options)) {
-                        sendFile(child, preserve, bufferSize);
-                    } else if (opener.sendAsDirectory(child, options)) {
-                        sendDir(child, preserve, bufferSize);
-                    }
-                }
-            } catch (IOException | RuntimeException e) {
-                listener.endFolderEvent(FileOperation.SEND, path, perms, e);
-                throw e;
-            }
-
-            listener.endFolderEvent(FileOperation.SEND, path, perms, null);
-        }
-
-        if (debugEnabled) {
-            log.debug("sendDir({})[{}] send 'E' command", this, path);
-        }
-        out.write("E\n".getBytes(StandardCharsets.UTF_8));
-        out.flush();
-
-        readyCode = readAck(false);
-        if (debugEnabled) {
-            log.debug("sendDir({})[{}] 'E' command reply code=", this, path, readyCode);
-        }
-        validateAckReplyCode("E", path, readyCode, false);
-    }
-
-    public static String getOctalPermissions(Collection<PosixFilePermission> perms) {
-        int pf = 0;
-
-        for (PosixFilePermission p : perms) {
-            switch (p) {
-                case OWNER_READ:
-                    pf |= S_IRUSR;
-                    break;
-                case OWNER_WRITE:
-                    pf |= S_IWUSR;
-                    break;
-                case OWNER_EXECUTE:
-                    pf |= S_IXUSR;
-                    break;
-                case GROUP_READ:
-                    pf |= S_IRGRP;
-                    break;
-                case GROUP_WRITE:
-                    pf |= S_IWGRP;
-                    break;
-                case GROUP_EXECUTE:
-                    pf |= S_IXGRP;
-                    break;
-                case OTHERS_READ:
-                    pf |= S_IROTH;
-                    break;
-                case OTHERS_WRITE:
-                    pf |= S_IWOTH;
-                    break;
-                case OTHERS_EXECUTE:
-                    pf |= S_IXOTH;
-                    break;
-                default:    // ignored
-            }
-        }
-
-        return String.format("%04o", pf);
-    }
-
-    public static Set<PosixFilePermission> parseOctalPermissions(String str) {
-        int perms = Integer.parseInt(str, 8);
-        Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class);
-        if ((perms & S_IRUSR) != 0) {
-            p.add(PosixFilePermission.OWNER_READ);
-        }
-        if ((perms & S_IWUSR) != 0) {
-            p.add(PosixFilePermission.OWNER_WRITE);
-        }
-        if ((perms & S_IXUSR) != 0) {
-            p.add(PosixFilePermission.OWNER_EXECUTE);
-        }
-        if ((perms & S_IRGRP) != 0) {
-            p.add(PosixFilePermission.GROUP_READ);
-        }
-        if ((perms & S_IWGRP) != 0) {
-            p.add(PosixFilePermission.GROUP_WRITE);
-        }
-        if ((perms & S_IXGRP) != 0) {
-            p.add(PosixFilePermission.GROUP_EXECUTE);
-        }
-        if ((perms & S_IROTH) != 0) {
-            p.add(PosixFilePermission.OTHERS_READ);
-        }
-        if ((perms & S_IWOTH) != 0) {
-            p.add(PosixFilePermission.OTHERS_WRITE);
-        }
-        if ((perms & S_IXOTH) != 0) {
-            p.add(PosixFilePermission.OTHERS_EXECUTE);
-        }
-
-        return p;
-    }
-
-    protected void sendWarning(String message) throws IOException {
-        sendResponseMessage(WARNING, message);
-    }
-
-    protected void sendError(String message) throws IOException {
-        sendResponseMessage(ERROR, message);
-    }
-
-    protected void sendResponseMessage(int level, String message) throws IOException {
-        sendResponseMessage(out, level, message);
-    }
-
-    public static <O extends OutputStream> O sendWarning(O out, String message) throws IOException {
-        return sendResponseMessage(out, WARNING, message);
-    }
-
-    public static <O extends OutputStream> O sendError(O out, String message) throws IOException {
-        return sendResponseMessage(out, ERROR, message);
-    }
-
-    public static <O extends OutputStream> O sendResponseMessage(O out, int level, String message) throws IOException {
-        out.write(level);
-        out.write(message.getBytes(StandardCharsets.UTF_8));
-        out.write('\n');
-        out.flush();
-        return out;
-    }
-
-    public static String getExitStatusName(Integer exitStatus) {
-        if (exitStatus == null) {
-            return "null";
-        }
-
-        switch (exitStatus) {
-            case OK:
-                return "OK";
-            case WARNING:
-                return "WARNING";
-            case ERROR:
-                return "ERROR";
-            default:
-                return exitStatus.toString();
-        }
-    }
-
-    public void ack() throws IOException {
-        out.write(0);
-        out.flush();
-    }
-
-    public int readAck(boolean canEof) throws IOException {
-        int c = in.read();
-        switch (c) {
-            case -1:
-                if (log.isDebugEnabled()) {
-                    log.debug("readAck({})[EOF={}] received EOF", this, canEof);
-                }
-                if (!canEof) {
-                    throw new EOFException("readAck - EOF before ACK");
-                }
-                break;
-            case OK:
-                if (log.isDebugEnabled()) {
-                    log.debug("readAck({})[EOF={}] read OK", this, canEof);
-                }
-                break;
-            case WARNING: {
-                if (log.isDebugEnabled()) {
-                    log.debug("readAck({})[EOF={}] read warning message", this, canEof);
-                }
-
-                String line = readLine();
-                log.warn("readAck({})[EOF={}] - Received warning: {}", this, canEof, line);
-                break;
-            }
-            case ERROR: {
-                if (log.isDebugEnabled()) {
-                    log.debug("readAck({})[EOF={}] read error message", this, canEof);
-                }
-                String line = readLine();
-                if (log.isDebugEnabled()) {
-                    log.debug("readAck({})[EOF={}] received error: {}", this, canEof, line);
-                }
-                throw new ScpException("Received nack: " + line, c);
-            }
-            default:
-                break;
-        }
-        return c;
-    }
-
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + "[" + getSession() + "]";
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
deleted file mode 100644
index d2a9afc..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * 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.Serializable;
-import java.util.Objects;
-
-import org.apache.sshd.common.auth.MutableUserHolder;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-
-/**
- * Represents a local or remote SCP location in the format {@code user@host:path}
- * for a remote path and a simple path for a local one. If user is omitted for a
- * remote path then current user is used.
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ScpLocation implements MutableUserHolder, Serializable, Cloneable {
-    public static final char HOST_PART_SEPARATOR = ':';
-    public static final char USERNAME_PART_SEPARATOR = '@';
-
-    private static final long serialVersionUID = 5450230457030600136L;
-
-    private String host;
-    private String username;
-    private String path;
-
-    public ScpLocation() {
-        this(null);
-    }
-
-    /**
-     * @param locSpec The location specification - ignored if {@code null}/empty
-     * @see #update(String, ScpLocation)
-     * @throws IllegalArgumentException if invalid specification
-     */
-    public ScpLocation(String locSpec) {
-        update(locSpec, this);
-    }
-
-    public String getHost() {
-        return host;
-    }
-
-    public void setHost(String host) {
-        this.host = host;
-    }
-
-    public boolean isLocal() {
-        return GenericUtils.isEmpty(getHost());
-    }
-
-    @Override
-    public String getUsername() {
-        return username;
-    }
-
-    @Override
-    public void setUsername(String username) {
-        this.username = username;
-    }
-
-    /**
-     * Resolves the effective username to use for a remote location.
-     * If username not set then uses the current username
-     *
-     * @return The resolved username
-     * @see #getUsername()
-     * @see OsUtils#getCurrentUser()
-     */
-    public String resolveUsername() {
-        String user = getUsername();
-        if (GenericUtils.isEmpty(user)) {
-            return OsUtils.getCurrentUser();
-        } else {
-            return user;
-        }
-    }
-
-    public String getPath() {
-        return path;
-    }
-
-    public void setPath(String path) {
-        this.path = path;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(getHost(), resolveUsername(), OsUtils.getComparablePath(getPath()));
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == null) {
-            return false;
-        }
-        if (this == obj) {
-            return true;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-
-        ScpLocation other = (ScpLocation) obj;
-        if (this.isLocal() != other.isLocal()) {
-            return false;
-        }
-
-        String thisPath = OsUtils.getComparablePath(getPath());
-        String otherPath = OsUtils.getComparablePath(other.getPath());
-        if (!Objects.equals(thisPath, otherPath)) {
-            return false;
-        }
-
-        if (isLocal()) {
-            return true;
-        }
-
-        // we know other is also remote or we would not have reached this point
-        return Objects.equals(resolveUsername(), other.resolveUsername())
-            && Objects.equals(getHost(), other.getHost());
-    }
-
-    @Override
-    public ScpLocation clone() {
-        try {
-            return getClass().cast(super.clone());
-        } catch (CloneNotSupportedException e) {    // unexpected
-            throw new RuntimeException("Failed to clone " + toString(), e);
-        }
-    }
-
-    @Override
-    public String toString() {
-        String p = getPath();
-        if (isLocal()) {
-            return p;
-        }
-
-        return resolveUsername() + String.valueOf(USERNAME_PART_SEPARATOR)
-             + getHost() + String.valueOf(HOST_PART_SEPARATOR) + p;
-    }
-
-    /**
-     * Parses a local or remote SCP location in the format {@code user@host:path}
-     *
-     * @param locSpec The location specification - ignored if {@code null}/empty
-     * @return The {@link ScpLocation} or {@code null} if no specification provider
-     * @throws IllegalArgumentException if invalid specification
-     * @see #update(String, ScpLocation)
-     */
-    public static ScpLocation parse(String locSpec) {
-        return GenericUtils.isEmpty(locSpec) ? null : update(locSpec, new ScpLocation());
-    }
-
-    /**
-     * Parses a local or remote SCP location in the format {@code user@host:path}
-     *
-     * @param <L> Type of {@link ScpLocation} being updated
-     * @param locSpec The location specification - ignored if {@code null}/empty
-     * @param location The {@link ScpLocation} to update - never {@code null}
-     * @return The updated location (unless no specification)
-     * @throws IllegalArgumentException if invalid specification
-     */
-    public static <L extends ScpLocation> L update(String locSpec, L location) {
-        Objects.requireNonNull(location, "No location to update");
-        if (GenericUtils.isEmpty(locSpec)) {
-            return location;
-        }
-
-        location.setHost(null);
-        location.setUsername(null);
-
-        int pos = locSpec.indexOf(HOST_PART_SEPARATOR);
-        if (pos < 0) {  // assume a local path
-            location.setPath(locSpec);
-            return location;
-        }
-
-        /*
-         * NOTE !!! in such a case there may be confusion with a host named 'a',
-         * but there is a limit to how smart we can be...
-         */
-        if ((pos == 1) && OsUtils.isWin32()) {
-            char drive = locSpec.charAt(0);
-            if (((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z'))) {
-                location.setPath(locSpec);
-                return location;
-            }
-        }
-
-        String login = locSpec.substring(0, pos);
-        ValidateUtils.checkTrue(pos < (locSpec.length() - 1), "Invalid remote specification (missing path): %s", locSpec);
-        location.setPath(locSpec.substring(pos + 1));
-
-        pos = login.indexOf(USERNAME_PART_SEPARATOR);
-        ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (missing username): %s", locSpec);
-        if (pos < 0) {
-            location.setHost(login);
-        } else {
-            location.setUsername(login.substring(0, pos));
-            ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (missing host): %s", locSpec);
-            location.setHost(login.substring(pos + 1));
-        }
-
-        return location;
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
deleted file mode 100644
index d0e611c..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FunctionalInterface
-public interface ScpReceiveLineHandler {
-    /**
-     * @param line  Received SCP input line
-     * @param isDir Does the input line refer to a directory
-     * @param time  The received {@link ScpTimestamp} - may be {@code null}
-     * @throws IOException If failed to process the line
-     */
-    void process(String line, boolean isDir, ScpTimestamp time) throws IOException;
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
deleted file mode 100644
index feeecbc..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.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.session.Session;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpSourceStreamResolver {
-    /**
-     * @return The uploaded file name
-     * @throws IOException If failed to resolve the name
-     */
-    String getFileName() throws IOException;
-
-    /**
-     * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
-     */
-    Path getEventListenerFilePath();
-
-    /**
-     * @return The permissions to be used for uploading a file
-     * @throws IOException If failed to generate the required permissions
-     */
-    Collection<PosixFilePermission> getPermissions() throws IOException;
-
-    /**
-     * @return The {@link ScpTimestamp} to use for uploading the file
-     * if {@code null} then no need to send this information
-     * @throws IOException If failed to generate the required data
-     */
-    ScpTimestamp getTimestamp() throws IOException;
-
-    /**
-     * @return An estimated size of the expected number of bytes to be uploaded.
-     * If non-positive then assumed to be unknown.
-     * @throws IOException If failed to generate an estimate
-     */
-    long getSize() throws IOException;
-
-    /**
-     * @param session The {@link Session} through which file is transmitted
-     * @param options The {@link OpenOption}s may be {@code null}/empty
-     * @return The {@link InputStream} containing the data to be uploaded
-     * @throws IOException If failed to create the stream
-     */
-    InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException;
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
deleted file mode 100644
index 9a70302..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.io.OutputStream;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Set;
-
-import org.apache.sshd.common.session.Session;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpTargetStreamResolver {
-    /**
-     * Called when receiving a file in order to obtain an output stream
-     * for the incoming data
-     *
-     * @param session The associated {@link Session}
-     * @param name    File name as received from remote site
-     * @param length  Number of bytes expected to receive
-     * @param perms   The {@link Set} of {@link PosixFilePermission} expected
-     * @param options The {@link OpenOption}s to use - may be {@code null}/empty
-     * @return The {@link OutputStream} to write the incoming data
-     * @throws IOException If failed to create the stream
-     */
-    OutputStream resolveTargetStream(Session session, String name, long length,
-            Set<PosixFilePermission> perms, OpenOption... options) throws IOException;
-
-    /**
-     * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
-     */
-    Path getEventListenerFilePath();
-
-    /**
-     * Called after successful reception of the data (and after closing the stream)
-     *
-     * @param name     File name as received from remote site
-     * @param preserve If {@code true} then the resolver should attempt to preserve
-     *                 the specified permissions and timestamp
-     * @param perms    The {@link Set} of {@link PosixFilePermission} expected
-     * @param time     If not {@code null} then the required timestamp(s) on the
-     *                 incoming data
-     * @throws IOException If failed to post-process the incoming data
-     */
-    void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException;
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
deleted file mode 100644
index e804de9..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.util.Date;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.util.GenericUtils;
-
-/**
- * Represents an SCP timestamp definition
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ScpTimestamp {
-    private final long lastModifiedTime;
-    private final long lastAccessTime;
-
-    public ScpTimestamp(long modTime, long accTime) {
-        lastModifiedTime = modTime;
-        lastAccessTime = accTime;
-    }
-
-    public long getLastModifiedTime() {
-        return lastModifiedTime;
-    }
-
-    public long getLastAccessTime() {
-        return lastAccessTime;
-    }
-
-    @Override
-    public String toString() {
-        return "modified=" + new Date(lastModifiedTime)
-            + ";accessed=" + new Date(lastAccessTime);
-    }
-
-    /**
-     * @param line The time specification - format:
-     * {@code T<mtime-sec> <mtime-micros> <atime-sec> <atime-micros>}
-     * where specified times are in seconds since UTC
-     * @return The {@link ScpTimestamp} value with the timestamps converted to
-     * <U>milliseconds</U>
-     * @throws NumberFormatException if bad numerical values - <B>Note:</B>
-     * does not check if 1st character is 'T'.
-     * @see <A HREF="https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works">How the SCP protocol works</A>
-     */
-    public static ScpTimestamp parseTime(String line) throws NumberFormatException {
-        String[] numbers = GenericUtils.split(line.substring(1), ' ');
-        return new ScpTimestamp(TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[0])),
-                TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2])));
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
deleted file mode 100644
index d7954e0..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.SshdEventListener;
-
-/**
- * Can be registered in order to receive events about SCP transfers
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpTransferEventListener extends SshdEventListener {
-    enum FileOperation {
-        SEND,
-        RECEIVE
-    }
-
-    /**
-     * An &quot;empty&quot; implementation to be used instead of {@code null}s
-     */
-    ScpTransferEventListener EMPTY = new ScpTransferEventListener() {
-        @Override
-        public String toString() {
-            return "EMPTY";
-        }
-    };
-
-    /**
-     * @param op     The {@link FileOperation}
-     * @param file   The <U>local</U> referenced file {@link Path}
-     * @param length Size (in bytes) of transferred data
-     * @param perms  A {@link Set} of {@link PosixFilePermission}s to be applied
-     *               once transfer is complete
-     * @throws IOException If failed to handle the event
-     */
-    default void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) throws IOException {
-        // ignored
-    }
-
-    /**
-     * @param op     The {@link FileOperation}
-     * @param file   The <U>local</U> referenced file {@link Path}
-     * @param length Size (in bytes) of transferred data
-     * @param perms  A {@link Set} of {@link PosixFilePermission}s to be applied
-     *               once transfer is complete
-     * @param thrown The result of the operation attempt - if {@code null} then
-     *               reception was successful
-     * @throws IOException If failed to handle the event
-     */
-    default void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
-            throws IOException {
-                // ignored
-    }
-
-    /**
-     * @param op    The {@link FileOperation}
-     * @param file  The <U>local</U> referenced folder {@link Path}
-     * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
-     *              once transfer is complete
-     * @throws IOException If failed to handle the event
-     */
-    default void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
-        // ignored
-    }
-
-    /**
-     * @param op     The {@link FileOperation}
-     * @param file   The <U>local</U> referenced file {@link Path}
-     * @param perms  A {@link Set} of {@link PosixFilePermission}s to be applied
-     *               once transfer is complete
-     * @param thrown The result of the operation attempt - if {@code null} then
-     *               reception was successful
-     * @throws IOException If failed to handle the event
-     */
-    default void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
-            throws IOException {
-        // ignored
-    }
-
-    static <L extends ScpTransferEventListener> L validateListener(L listener) {
-        return SshdEventListener.validateListener(listener, ScpTransferEventListener.class.getSimpleName());
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
deleted file mode 100644
index bb6ae3b..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.helpers;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.util.Arrays;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpSourceStreamResolver;
-import org.apache.sshd.common.scp.ScpTargetStreamResolver;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class DefaultScpFileOpener extends AbstractLoggingBean implements ScpFileOpener {
-    public static final DefaultScpFileOpener INSTANCE = new DefaultScpFileOpener();
-
-    public DefaultScpFileOpener() {
-        super();
-    }
-
-    @Override
-    public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("openRead({}) file={}, options={}",
-                      session, file, Arrays.toString(options));
-        }
-
-        return Files.newInputStream(file, options);
-    }
-
-    @Override
-    public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("openWrite({}) file={}, options={}",
-                      session, file, Arrays.toString(options));
-        }
-
-        return Files.newOutputStream(file, options);
-    }
-
-    @Override
-    public ScpSourceStreamResolver createScpSourceStreamResolver(Path path) throws IOException {
-        return new LocalFileScpSourceStreamResolver(path, this);
-    }
-
-    @Override
-    public ScpTargetStreamResolver createScpTargetStreamResolver(Path path) throws IOException {
-        return new LocalFileScpTargetStreamResolver(path, this);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
deleted file mode 100644
index 8ce9b61..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.helpers;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-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.PosixFilePermission;
-import java.util.Collection;
-import java.util.Objects;
-import java.util.Set;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpSourceStreamResolver;
-import org.apache.sshd.common.scp.ScpTimestamp;
-import org.apache.sshd.common.session.Session;
-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 class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implements ScpSourceStreamResolver {
-    protected final Path path;
-    protected final ScpFileOpener opener;
-    protected final Path name;
-    protected final Set<PosixFilePermission> perms;
-    protected final long size;
-    protected final ScpTimestamp time;
-
-    public LocalFileScpSourceStreamResolver(Path path, ScpFileOpener opener) throws IOException {
-        this.path = Objects.requireNonNull(path, "No path specified");
-        this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
-        this.name = path.getFileName();
-        this.perms = IoUtils.getPermissions(path);
-
-        BasicFileAttributes basic = Files.getFileAttributeView(path, BasicFileAttributeView.class).readAttributes();
-        this.size = basic.size();
-        this.time = new ScpTimestamp(basic.lastModifiedTime().toMillis(), basic.lastAccessTime().toMillis());
-    }
-
-    @Override
-    public String getFileName() throws IOException {
-        return name.toString();
-    }
-
-    @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 Path getEventListenerFilePath() {
-        return path;
-    }
-
-    @Override
-    public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
-        return opener.openRead(session, getEventListenerFilePath(), options);
-    }
-
-    @Override
-    public String toString() {
-        return String.valueOf(getEventListenerFilePath());
-    }
-}


[5/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
deleted file mode 100644
index 2212d7d..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
+++ /dev/null
@@ -1,1197 +0,0 @@
-/*
- * 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.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.channels.FileChannel;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import com.jcraft.jsch.ChannelExec;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.Factory;
-import org.apache.sshd.common.channel.Channel;
-import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
-import org.apache.sshd.common.random.Random;
-import org.apache.sshd.common.scp.ScpException;
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.scp.ScpCommand;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.apache.sshd.util.test.JSchLogger;
-import org.apache.sshd.util.test.SimpleUserInfo;
-import org.apache.sshd.util.test.Utils;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-import ch.ethz.ssh2.Connection;
-import ch.ethz.ssh2.ConnectionInfo;
-import ch.ethz.ssh2.SCPClient;
-
-/**
- * Test for SCP support.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class ScpTest extends BaseTestSupport {
-    private static final ScpTransferEventListener DEBUG_LISTENER = new ScpTransferEventListener() {
-        @Override
-        public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
-            logEvent("starFolderEvent", op, file, false, -1L, perms, null);
-        }
-
-        @Override
-        public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
-            logEvent("startFileEvent", op, file, true, length, perms, null);
-
-        }
-
-        @Override
-        public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
-            logEvent("endFolderEvent", op, file, false, -1L, perms, thrown);
-        }
-
-        @Override
-        public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
-            logEvent("endFileEvent", op, file, true, length, perms, thrown);
-        }
-
-        private void logEvent(String type, FileOperation op, Path path, boolean isFile, long length, Collection<PosixFilePermission> perms, Throwable t) {
-            if (!OUTPUT_DEBUG_MESSAGES) {
-                return; // just in case
-            }
-            StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
-            sb.append('\t').append(type)
-                    .append('[').append(op).append(']')
-                    .append(' ').append(isFile ? "File" : "Directory").append('=').append(path)
-                    .append(' ').append("length=").append(length)
-                    .append(' ').append("perms=").append(perms);
-            if (t != null) {
-                sb.append(' ').append("ERROR=").append(t.getClass().getSimpleName()).append(": ").append(t.getMessage());
-            }
-            outputDebugMessage(sb.toString());
-        }
-    };
-
-    private static SshServer sshd;
-    private static int port;
-    private static SshClient client;
-    private final FileSystemFactory fileSystemFactory;
-
-    public ScpTest() throws IOException {
-        Path targetPath = detectTargetFolder();
-        Path parentPath = targetPath.getParent();
-        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
-    }
-
-    @BeforeClass
-    public static void setupClientAndServer() throws Exception {
-        JSchLogger.init();
-        sshd = Utils.setupTestServer(ScpTest.class);
-        sshd.setCommandFactory(new ScpCommandFactory());
-        sshd.start();
-        port = sshd.getPort();
-
-        client = Utils.setupTestClient(ScpTest.class);
-        client.start();
-    }
-
-    @AfterClass
-    public static void tearDownClientAndServer() throws Exception {
-        if (sshd != null) {
-            try {
-                sshd.stop(true);
-            } finally {
-                sshd = null;
-            }
-        }
-
-        if (client != null) {
-            try {
-                client.stop();
-            } finally {
-                client = null;
-            }
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        sshd.setFileSystemFactory(fileSystemFactory);
-    }
-
-    @Test
-    public void testNormalizedScpRemotePaths() throws Exception {
-        Path targetPath = detectTargetFolder();
-        Path parentPath = targetPath.getParent();
-        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-        Utils.deleteRecursive(scpRoot);
-
-        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-        Path localFile = localDir.resolve("file.txt");
-        byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
-        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-        Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
-        String localPath = localFile.toString();
-        String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-        String[] remoteComps = GenericUtils.split(remotePath, '/');
-        Factory<? extends Random> factory = client.getRandomFactory();
-        Random rnd = factory.create();
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(5L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            StringBuilder sb = new StringBuilder(remotePath.length() + Long.SIZE);
-            for (int i = 0; i < Math.max(Long.SIZE, remoteComps.length); i++) {
-                if (sb.length() > 0) {
-                    sb.setLength(0);    // start again
-                }
-
-                sb.append(remoteComps[0]);
-                for (int j = 1; j < remoteComps.length; j++) {
-                    String name = remoteComps[j];
-                    slashify(sb, rnd);
-                    sb.append(name);
-                }
-                slashify(sb, rnd);
-
-                String path = sb.toString();
-                scp.upload(localPath, path);
-                assertTrue("Remote file not ready for " + path, waitForFile(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L)));
-
-                byte[] actual = Files.readAllBytes(remoteFile);
-                assertArrayEquals("Mismatched uploaded data for " + path, data, actual);
-                Files.delete(remoteFile);
-                assertFalse("Remote file (" + remoteFile + ") not deleted for " + path, Files.exists(remoteFile));
-            }
-        }
-    }
-
-    private static int slashify(StringBuilder sb, Random rnd) {
-        int slashes = 1 /* at least one slash */ + rnd.random(Byte.SIZE);
-        for (int k = 0; k < slashes; k++) {
-            sb.append('/');
-        }
-
-        return slashes;
-    }
-
-    @Test
-    public void testUploadAbsoluteDriveLetter() throws Exception {
-        Path targetPath = detectTargetFolder();
-        Path parentPath = targetPath.getParent();
-        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-        Utils.deleteRecursive(scpRoot);
-
-        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-        Path localFile = localDir.resolve("file-1.txt");
-        byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
-        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-        Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
-        String localPath = localFile.toString();
-        String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(5L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            scp.upload(localPath, remotePath);
-            assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L));
-
-            Path secondRemote = remoteDir.resolve("file-2.txt");
-            String secondPath = Utils.resolveRelativeRemotePath(parentPath, secondRemote);
-            scp.upload(localPath, secondPath);
-            assertFileLength(secondRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
-
-            Path pathRemote = remoteDir.resolve("file-path.txt");
-            String pathPath = Utils.resolveRelativeRemotePath(parentPath, pathRemote);
-            scp.upload(localFile, pathPath);
-            assertFileLength(pathRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
-        }
-    }
-
-    @Test
-    public void testScpUploadOverwrite() throws Exception {
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(5L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
-
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-            Path localFile = localDir.resolve("file.txt");
-            Utils.writeFile(localFile, data);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            Path remoteFile = remoteDir.resolve(localFile.getFileName());
-            Utils.writeFile(remoteFile, data + data);
-
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-            scp.upload(localFile.toString(), remotePath);
-            assertFileLength(remoteFile, data.length(), TimeUnit.SECONDS.toMillis(5L));
-        }
-    }
-
-    @Test
-    public void testScpUploadZeroLengthFile() throws Exception {
-        Path targetPath = detectTargetFolder();
-        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-        Path zeroLocal = localDir.resolve("zero.txt");
-
-        try (FileChannel fch = FileChannel.open(zeroLocal, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
-            if (fch.size() > 0L) {
-                fch.truncate(0L);
-            }
-        }
-        assertEquals("Non-zero size for local file=" + zeroLocal, 0L, Files.size(zeroLocal));
-
-        Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
-        if (Files.exists(zeroRemote)) {
-            Files.delete(zeroRemote);
-        }
-
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(5L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
-            scp.upload(zeroLocal.toString(), remotePath);
-            assertFileLength(zeroRemote, 0L, TimeUnit.SECONDS.toMillis(5L));
-        }
-    }
-
-    @Test
-    public void testScpDownloadZeroLengthFile() throws Exception {
-        Path targetPath = detectTargetFolder();
-        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-        Path zeroLocal = localDir.resolve(getCurrentTestName());
-        if (Files.exists(zeroLocal)) {
-            Files.delete(zeroLocal);
-        }
-
-        Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
-        try (FileChannel fch = FileChannel.open(zeroRemote, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
-            if (fch.size() > 0L) {
-                fch.truncate(0L);
-            }
-        }
-        assertEquals("Non-zero size for remote file=" + zeroRemote, 0L, Files.size(zeroRemote));
-
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(5L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
-            scp.download(remotePath, zeroLocal.toString());
-            assertFileLength(zeroLocal, 0L, TimeUnit.SECONDS.toMillis(5L));
-        }
-    }
-
-    @Test
-    public void testScpNativeOnSingleFile() throws Exception {
-        String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
-
-        Path targetPath = detectTargetFolder();
-        Path parentPath = targetPath.getParent();
-        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-        Utils.deleteRecursive(scpRoot);
-
-        Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-        Path localOutFile = localDir.resolve("file-1.txt");
-        Path remoteDir = scpRoot.resolve("remote");
-        Path remoteOutFile = remoteDir.resolve(localOutFile.getFileName());
-
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            Utils.writeFile(localOutFile, data);
-
-            assertFalse("Remote folder already exists: " + remoteDir, Files.exists(remoteDir));
-
-            String localOutPath = localOutFile.toString();
-            String remoteOutPath = Utils.resolveRelativeRemotePath(parentPath, remoteOutFile);
-            outputDebugMessage("Expect upload failure %s => %s", localOutPath, remoteOutPath);
-            try {
-                scp.upload(localOutPath, remoteOutPath);
-                fail("Expected IOException for 1st time " + remoteOutPath);
-            } catch (IOException e) {
-                // ok
-            }
-
-            assertHierarchyTargetFolderExists(remoteDir);
-            outputDebugMessage("Expect upload success %s => %s", localOutPath, remoteOutPath);
-            scp.upload(localOutPath, remoteOutPath);
-            assertFileLength(remoteOutFile, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
-            Path secondLocal = localDir.resolve(localOutFile.getFileName());
-            String downloadTarget = Utils.resolveRelativeRemotePath(parentPath, secondLocal);
-            outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
-            scp.download(remoteOutPath, downloadTarget);
-            assertFileLength(secondLocal, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
-            Path localPath = localDir.resolve("file-path.txt");
-            downloadTarget = Utils.resolveRelativeRemotePath(parentPath, localPath);
-            outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
-            scp.download(remoteOutPath, downloadTarget);
-            assertFileLength(localPath, data.length(), TimeUnit.SECONDS.toMillis(11L));
-        }
-    }
-
-    @Test
-    public void testScpNativeOnMultipleFiles() throws Exception {
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-            Path local1 = localDir.resolve("file-1.txt");
-            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
-            Path local2 = localDir.resolve("file-2.txt");
-            Files.write(local2, data);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            Path remote1 = remoteDir.resolve(local1.getFileName());
-            String remote1Path = Utils.resolveRelativeRemotePath(parentPath, remote1);
-            String[] locals = {local1.toString(), local2.toString()};
-            try {
-                scp.upload(locals, remote1Path);
-                fail("Unexpected upload success to missing remote file: " + remote1Path);
-            } catch (IOException e) {
-                // Ok
-            }
-
-            Files.write(remote1, data);
-            try {
-                scp.upload(locals, remote1Path);
-                fail("Unexpected upload success to existing remote file: " + remote1Path);
-            } catch (IOException e) {
-                // Ok
-            }
-
-            Path remoteSubDir = assertHierarchyTargetFolderExists(remoteDir.resolve("dir"));
-            scp.upload(locals, Utils.resolveRelativeRemotePath(parentPath, remoteSubDir));
-
-            Path remoteSub1 = remoteSubDir.resolve(local1.getFileName());
-            assertFileLength(remoteSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            Path remoteSub2 = remoteSubDir.resolve(local2.getFileName());
-            assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            String[] remotes = {
-                Utils.resolveRelativeRemotePath(parentPath, remoteSub1),
-                Utils.resolveRelativeRemotePath(parentPath, remoteSub2),
-            };
-
-            try {
-                scp.download(remotes, Utils.resolveRelativeRemotePath(parentPath, local1));
-                fail("Unexpected download success to existing local file: " + local1);
-            } catch (IOException e) {
-                // Ok
-            }
-
-            Path localSubDir = localDir.resolve("dir");
-            try {
-                scp.download(remotes, localSubDir);
-                fail("Unexpected download success to non-existing folder: " + localSubDir);
-            } catch (IOException e) {
-                // Ok
-            }
-
-            assertHierarchyTargetFolderExists(localSubDir);
-            scp.download(remotes, localSubDir);
-
-            assertFileLength(localSubDir.resolve(remoteSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertFileLength(localSubDir.resolve(remoteSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-        }
-    }
-
-    @Test
-    public void testScpNativeOnRecursiveDirs() throws Exception {
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path localDir = scpRoot.resolve("local");
-            Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
-            Path localSub1 = localSubDir.resolve("file-1.txt");
-            byte[] data = Utils.writeFile(localSub1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-            Path localSub2 = localSubDir.resolve("file-2.txt");
-            Files.write(localSub2, data);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            scp.upload(localSubDir, Utils.resolveRelativeRemotePath(parentPath, remoteDir), ScpClient.Option.Recursive);
-
-            Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
-            assertFileLength(remoteSubDir.resolve(localSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            Utils.deleteRecursive(localSubDir);
-
-            scp.download(Utils.resolveRelativeRemotePath(parentPath, remoteSubDir), localDir, ScpClient.Option.Recursive);
-            assertFileLength(localSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
-        }
-    }
-
-    @Test
-    public void testScpNativeOnDirWithPattern() throws Exception {
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-            Path local1 = localDir.resolve("file-1.txt");
-            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-            Path local2 = localDir.resolve("file-2.txt");
-            Files.write(local2, data);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
-            scp.upload(localDir.toString() + File.separator + "*", remotePath);
-            assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertFileLength(remoteDir.resolve(local2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            Files.delete(local1);
-            Files.delete(local2);
-            scp.download(remotePath + "/*", localDir);
-            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertFileLength(local2, data.length, TimeUnit.SECONDS.toMillis(11L));
-        }
-    }
-
-    @Test
-    public void testScpNativeOnMixedDirAndFiles() throws Exception {
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path localDir = scpRoot.resolve("local");
-            Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
-            Path local1 = localDir.resolve("file-1.txt");
-            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-            Path localSub2 = localSubDir.resolve("file-2.txt");
-            Files.write(localSub2, data);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
-            scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive);
-            assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
-            assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            Files.delete(local1);
-            Utils.deleteRecursive(localSubDir);
-
-            scp.download(remotePath + "/*", localDir);
-            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertFalse("Unexpected recursive local file: " + localSub2, Files.exists(localSub2));
-
-            Files.delete(local1);
-            scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive);
-            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L));
-            assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(5L));
-        }
-    }
-
-    @Test
-    public void testScpNativePreserveAttributes() throws Exception {
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path localDir = scpRoot.resolve("local");
-            Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
-            // convert everything to seconds since this is the SCP timestamps granularity
-            final long lastModMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
-            final long lastModSecs = TimeUnit.MILLISECONDS.toSeconds(lastModMillis);
-            Path local1 = localDir.resolve("file-1.txt");
-            byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
-            File lclFile1 = local1.toFile();
-            boolean lcl1ModSet = lclFile1.setLastModified(lastModMillis);
-            lclFile1.setExecutable(true, true);
-            lclFile1.setWritable(false, false);
-
-            Path localSub2 = localSubDir.resolve("file-2.txt");
-            Files.write(localSub2, data);
-            File lclSubFile2 = localSub2.toFile();
-            boolean lclSub2ModSet = lclSubFile2.setLastModified(lastModMillis);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
-            scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
-
-            Path remote1 = remoteDir.resolve(local1.getFileName());
-            assertFileLength(remote1, data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            File remFile1 = remote1.toFile();
-            assertLastModifiedTimeEquals(remFile1, lcl1ModSet, lastModSecs);
-
-            Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
-            Path remoteSub2 = remoteSubDir.resolve(localSub2.getFileName());
-            assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            File remSubFile2 = remoteSub2.toFile();
-            assertLastModifiedTimeEquals(remSubFile2, lclSub2ModSet, lastModSecs);
-
-            Utils.deleteRecursive(localDir);
-            assertHierarchyTargetFolderExists(localDir);
-
-            scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
-            assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertLastModifiedTimeEquals(lclFile1, lcl1ModSet, lastModSecs);
-            assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
-            assertLastModifiedTimeEquals(lclSubFile2, lclSub2ModSet, lastModSecs);
-        }
-    }
-
-    @Test
-    public void testStreamsUploadAndDownload() throws Exception {
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = createScpClient(session);
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            Path remoteFile = remoteDir.resolve("file.txt");
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-            byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
-            outputDebugMessage("Upload data to %s", remotePath);
-            scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
-            assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            byte[] uploaded = Files.readAllBytes(remoteFile);
-            assertArrayEquals("Mismatched uploaded data", data, uploaded);
-
-            outputDebugMessage("Download data from %s", remotePath);
-            byte[] downloaded = scp.downloadBytes(remotePath);
-            assertArrayEquals("Mismatched downloaded data", uploaded, downloaded);
-        }
-    }
-
-    @Test   // see SSHD-649
-    public void testScpFileOpener() throws Exception {
-        class TrackingFileOpener extends DefaultScpFileOpener {
-            private final AtomicInteger readCount = new AtomicInteger(0);
-            private final AtomicInteger writeCount = new AtomicInteger(0);
-
-            TrackingFileOpener() {
-                super();
-            }
-
-            public AtomicInteger getReadCount() {
-                return readCount;
-            }
-
-            public AtomicInteger getWriteCount() {
-                return writeCount;
-            }
-
-            @Override
-            public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
-                int count = readCount.incrementAndGet();
-                outputDebugMessage("openRead(%s)[%s] count=%d", session, file, count);
-                return super.openRead(session, file, options);
-            }
-
-            @Override
-            public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
-                int count = writeCount.incrementAndGet();
-                outputDebugMessage("openWrite(%s)[%s] count=%d", session, file, count);
-                return super.openWrite(session, file, options);
-            }
-        }
-
-        ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
-        ScpFileOpener opener = factory.getScpFileOpener();
-        TrackingFileOpener serverOpener = new TrackingFileOpener();
-        factory.setScpFileOpener(serverOpener);
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            TrackingFileOpener clientOpener = new TrackingFileOpener();
-            ScpClient scp = session.createScpClient(clientOpener);
-
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot);
-            Path localFile = remoteDir.resolve("data.txt");
-            byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
-            Files.write(localFile, data);
-
-            Path remoteFile = remoteDir.resolve("upload.txt");
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-            outputDebugMessage("Upload data to %s", remotePath);
-            scp.upload(localFile, remotePath);
-            assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            AtomicInteger serverRead = serverOpener.getReadCount();
-            assertEquals("Mismatched server upload open read count", 0, serverRead.get());
-
-            AtomicInteger serverWrite = serverOpener.getWriteCount();
-            assertEquals("Mismatched server upload write count", 1, serverWrite.getAndSet(0));
-
-            AtomicInteger clientRead = clientOpener.getReadCount();
-            assertEquals("Mismatched client upload read count", 1, clientRead.getAndSet(0));
-
-            AtomicInteger clientWrite = clientOpener.getWriteCount();
-            assertEquals("Mismatched client upload write count", 0, clientWrite.get());
-
-            Files.delete(localFile);
-            scp.download(remotePath, localFile);
-            assertFileLength(localFile, data.length, TimeUnit.SECONDS.toMillis(11L));
-
-            assertEquals("Mismatched server download open read count", 1, serverRead.getAndSet(0));
-            assertEquals("Mismatched server download write count", 0, serverWrite.get());
-            assertEquals("Mismatched client download read count", 0, clientRead.get());
-            assertEquals("Mismatched client download write count", 1, clientWrite.getAndSet(0));
-        } finally {
-            factory.setScpFileOpener(opener);
-        }
-    }
-
-    @Test   // see SSHD-628
-    public void testScpExitStatusPropagation() throws Exception {
-        final int testExitValue = 7365;
-        class InternalScpCommand extends ScpCommand implements ExitCallback {
-            private ExitCallback delegate;
-
-            InternalScpCommand(String command, ExecutorService executorService, boolean shutdownOnExit,
-                    int sendSize, int receiveSize, ScpFileOpener opener, ScpTransferEventListener eventListener) {
-                super(command, executorService, shutdownOnExit, sendSize, receiveSize, opener, eventListener);
-            }
-
-            @Override
-            protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
-                outputDebugMessage("writeCommandResponseMessage(%s) status=%d", command, exitValue);
-                super.writeCommandResponseMessage(command, testExitValue, exitMessage);
-            }
-
-            @Override
-            public void setExitCallback(ExitCallback callback) {
-                delegate = callback;
-                super.setExitCallback(this);
-            }
-
-            @Override
-            public void onExit(int exitValue) {
-                onExit(exitValue, Integer.toString(exitValue));
-            }
-
-            @Override
-            public void onExit(int exitValue, String exitMessage) {
-                outputDebugMessage("onExit(%s) status=%d", this, exitValue);
-                if (exitValue == ScpHelper.OK) {
-                    delegate.onExit(testExitValue, exitMessage);
-                } else {
-                    delegate.onExit(exitValue, exitMessage);
-                }
-            }
-        }
-
-        ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
-        sshd.setCommandFactory(new ScpCommandFactory() {
-            @Override
-            public Command createCommand(String command) {
-                ValidateUtils.checkTrue(command.startsWith(ScpHelper.SCP_COMMAND_PREFIX), "Bad SCP command: %s", command);
-                return new InternalScpCommand(command,
-                        getExecutorService(), isShutdownOnExit(),
-                        getSendBufferSize(), getReceiveBufferSize(),
-                        DefaultScpFileOpener.INSTANCE,
-                        ScpTransferEventListener.EMPTY);
-            }
-        });
-
-        try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
-            session.addPasswordIdentity(getCurrentTestName());
-            session.auth().verify(11L, TimeUnit.SECONDS);
-
-            ScpClient scp = session.createScpClient();
-            Path targetPath = detectTargetFolder();
-            Path parentPath = targetPath.getParent();
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            Path remoteFile = remoteDir.resolve("file.txt");
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-            byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
-            outputDebugMessage("Upload data to %s", remotePath);
-            try {
-                scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
-                outputDebugMessage("Upload success to %s", remotePath);
-            } catch (ScpException e) {
-                Integer exitCode = e.getExitStatus();
-                assertNotNull("No upload exit status", exitCode);
-                assertEquals("Mismatched upload exit status", testExitValue, exitCode.intValue());
-            }
-
-            if (Files.deleteIfExists(remoteFile)) {
-                outputDebugMessage("Deleted remote file %s", remoteFile);
-            }
-
-            try (OutputStream out = Files.newOutputStream(remoteFile)) {
-                out.write(data);
-            }
-
-            try {
-                byte[] downloaded = scp.downloadBytes(remotePath);
-                outputDebugMessage("Download success to %s: %s", remotePath, new String(downloaded, StandardCharsets.UTF_8));
-            } catch (ScpException e) {
-                Integer exitCode = e.getExitStatus();
-                assertNotNull("No download exit status", exitCode);
-                assertEquals("Mismatched download exit status", testExitValue, exitCode.intValue());
-            }
-        } finally {
-            sshd.setCommandFactory(factory);
-        }
-    }
-
-    // see http://stackoverflow.com/questions/2717936/file-createnewfile-creates-files-with-last-modified-time-before-actual-creatio
-    // See https://msdn.microsoft.com/en-us/library/ms724290(VS.85).aspx
-    private static void assertLastModifiedTimeEquals(File file, boolean modSuccess, long expectedSeconds) {
-        long expectedMillis = TimeUnit.SECONDS.toMillis(expectedSeconds);
-        long actualMillis = file.lastModified();
-        long actualSeconds = TimeUnit.MILLISECONDS.toSeconds(actualMillis);
-        // if failed to set the local file time, don't expect it to be the same
-        if (!modSuccess) {
-            System.err.append("Failed to set last modified time of ").append(file.getAbsolutePath())
-                      .append(" to ").append(String.valueOf(expectedMillis))
-                      .append(" - ").println(new Date(expectedMillis));
-            System.err.append("\t\t").append("Current value: ").append(String.valueOf(actualMillis))
-                      .append(" - ").println(new Date(actualMillis));
-            return;
-        }
-
-        if (OsUtils.isWin32()) {
-            // The NTFS file system delays updates to the last access time for a file by up to 1 hour after the last access
-            if (expectedSeconds != actualSeconds) {
-                System.err.append("Mismatched last modified time for ").append(file.getAbsolutePath())
-                          .append(" - expected=").append(String.valueOf(expectedSeconds))
-                          .append('[').append(new Date(expectedMillis).toString()).append(']')
-                          .append(", actual=").append(String.valueOf(actualSeconds))
-                          .append('[').append(new Date(actualMillis).toString()).append(']')
-                          .println();
-            }
-        } else {
-            assertEquals("Mismatched last modified time for " + file.getAbsolutePath(), expectedSeconds, actualSeconds);
-        }
-    }
-
-    @Test
-    public void testJschScp() throws Exception {
-        com.jcraft.jsch.Session session = getJschSession();
-        try {
-            String data = getCurrentTestName() + "\n";
-
-            String unixDir = "target/scp";
-            String fileName = getCurrentTestName() + ".txt";
-            String unixPath = unixDir + "/" + fileName;
-            File root = new File(unixDir);
-            File target = new File(unixPath);
-            Utils.deleteRecursive(root);
-            root.mkdirs();
-            assertTrue("Failed to ensure existence of " + root, root.exists());
-
-            target.delete();
-            assertFalse("Failed to delete 1st time: " + target, target.exists());
-            sendFile(session, unixPath, target, data);
-            assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
-            target.delete();
-            assertFalse("Failed to delete 2nd time: " + target, target.exists());
-            sendFile(session, unixDir, target, data);
-            assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
-            sendFileError(session, "target", ScpHelper.SCP_COMMAND_PREFIX, data);
-
-            readFileError(session, unixDir);
-
-            assertEquals("Mismatched file data", data, readFile(session, unixPath, target));
-            assertEquals("Mismatched dir data", data, readDir(session, unixDir, target));
-
-            target.delete();
-            root.delete();
-
-            sendDir(session, "target", ScpHelper.SCP_COMMAND_PREFIX, fileName, data);
-            assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
-        } finally {
-            session.disconnect();
-        }
-    }
-
-    protected com.jcraft.jsch.Session getJschSession() throws JSchException {
-        JSch sch = new JSch();
-        com.jcraft.jsch.Session session = sch.getSession(getCurrentTestName(), TEST_LOCALHOST, port);
-        session.setUserInfo(new SimpleUserInfo(getCurrentTestName()));
-        session.connect();
-        return session;
-    }
-
-    @Test
-    public void testWithGanymede() throws Exception {
-        Path targetPath = detectTargetFolder();
-        Path parentPath = targetPath.getParent();
-        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-        Utils.deleteRecursive(scpRoot);
-
-        byte[] expected = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
-        Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-        String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
-        String fileName = "file.txt";
-        Path remoteFile = remoteDir.resolve(fileName);
-        String mode = ScpHelper.getOctalPermissions(EnumSet.of(
-                PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE,
-                PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
-                PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE
-        ));
-
-        ch.ethz.ssh2.log.Logger.enabled = true;
-        Connection conn = new Connection(TEST_LOCALHOST, port);
-        try {
-            ConnectionInfo info = conn.connect(null, (int) TimeUnit.SECONDS.toMillis(5L), (int) TimeUnit.SECONDS.toMillis(13L));
-            outputDebugMessage("Connected: kex=%s, key-type=%s, c2senc=%s, s2cenc=%s, c2mac=%s, s2cmac=%s",
-                    info.keyExchangeAlgorithm, info.serverHostKeyAlgorithm,
-                    info.clientToServerCryptoAlgorithm, info.serverToClientCryptoAlgorithm,
-                    info.clientToServerMACAlgorithm, info.serverToClientMACAlgorithm);
-            assertTrue("Failed to authenticate", conn.authenticateWithPassword(getCurrentTestName(), getCurrentTestName()));
-
-            SCPClient scpClient = new SCPClient(conn);
-            try (OutputStream output = scpClient.put(fileName, expected.length, remotePath, mode)) {
-                output.write(expected);
-            }
-
-            assertTrue("Remote file not created: " + remoteFile, Files.exists(remoteFile));
-            byte[] remoteData = Files.readAllBytes(remoteFile);
-            assertArrayEquals("Mismatched remote put data", expected, remoteData);
-
-            Arrays.fill(remoteData, (byte) 0);  // make sure we start with a clean slate
-            try (InputStream input = scpClient.get(remotePath + "/" + fileName)) {
-                int readLen = input.read(remoteData);
-                assertEquals("Mismatched remote get data size", expected.length, readLen);
-                // make sure we reached EOF
-                assertEquals("Unexpected extra data after read expected size", -1, input.read());
-            }
-
-            assertArrayEquals("Mismatched remote get data", expected, remoteData);
-        } finally {
-            conn.close();
-        }
-    }
-
-    protected String readFile(com.jcraft.jsch.Session session, String path, File target) throws Exception {
-        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
-        c.setCommand("scp -f " + path);
-        c.connect();
-
-        String fileName = target.getName();
-        try (OutputStream os = c.getOutputStream();
-             InputStream is = c.getInputStream()) {
-
-            os.write(0);
-            os.flush();
-
-            String header = readLine(is);
-            String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + fileName;
-            assertEquals("Mismatched header for " + path, expHeader, header);
-
-            String lenValue = header.substring(6, header.indexOf(' ', 6));
-            int length = Integer.parseInt(lenValue);
-            os.write(0);
-            os.flush();
-
-            byte[] buffer = new byte[length];
-            length = is.read(buffer, 0, buffer.length);
-            assertEquals("Mismatched read data length for " + path, length, buffer.length);
-            assertAckReceived(is, "Read data of " + path);
-
-            os.write(0);
-            os.flush();
-
-            return new String(buffer, StandardCharsets.UTF_8);
-        } finally {
-            c.disconnect();
-        }
-    }
-
-    protected String readDir(com.jcraft.jsch.Session session, String path, File target) throws Exception {
-        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
-        c.setCommand("scp -r -f " + path);
-        c.connect();
-
-        try (OutputStream os = c.getOutputStream();
-             InputStream is = c.getInputStream()) {
-            os.write(0);
-            os.flush();
-
-            String header = readLine(is);
-            String expPrefix = "D" + ScpHelper.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 ";
-            assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith(expPrefix));
-            os.write(0);
-            os.flush();
-
-            header = readLine(is);
-            String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + target.getName();
-            assertEquals("Mismatched dir header for " + path, expHeader, header);
-            int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
-            os.write(0);
-            os.flush();
-
-            byte[] buffer = new byte[length];
-            length = is.read(buffer, 0, buffer.length);
-            assertEquals("Mismatched read buffer size for " + path, length, buffer.length);
-            assertAckReceived(is, "Read date of " + path);
-
-            os.write(0);
-            os.flush();
-
-            header = readLine(is);
-            assertEquals("Mismatched end value for " + path, "E", header);
-            os.write(0);
-            os.flush();
-
-            return new String(buffer, StandardCharsets.UTF_8);
-        } finally {
-            c.disconnect();
-        }
-    }
-
-    protected void readFileError(com.jcraft.jsch.Session session, String path) throws Exception {
-        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
-        String command = "scp -f " + path;
-        c.setCommand(command);
-        c.connect();
-
-        try (OutputStream os = c.getOutputStream();
-             InputStream is = c.getInputStream()) {
-
-            os.write(0);
-            os.flush();
-            assertEquals("Mismatched response for command: " + command, 2, is.read());
-        } finally {
-            c.disconnect();
-        }
-    }
-
-    protected void sendFile(com.jcraft.jsch.Session session, String path, File target, String data) throws Exception {
-        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
-        String command = "scp -t " + path;
-        c.setCommand(command);
-        c.connect();
-
-        try (OutputStream os = c.getOutputStream();
-             InputStream is = c.getInputStream()) {
-
-            assertAckReceived(is, command);
-
-            File parent = target.getParentFile();
-            Collection<PosixFilePermission> perms = IoUtils.getPermissions(parent.toPath());
-            String octalPerms = ScpHelper.getOctalPermissions(perms);
-            String name = target.getName();
-            assertAckReceived(os, is, "C" + octalPerms + " " + data.length() + " " + name);
-
-            os.write(data.getBytes(StandardCharsets.UTF_8));
-            os.flush();
-            assertAckReceived(is, "Sent data (length=" + data.length() + ") for " + path + "[" + name + "]");
-
-            os.write(0);
-            os.flush();
-
-            Thread.sleep(100);
-        } finally {
-            c.disconnect();
-        }
-    }
-
-    protected void assertAckReceived(OutputStream os, InputStream is, String command) throws IOException {
-        os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
-        os.flush();
-        assertAckReceived(is, command);
-    }
-
-    protected void assertAckReceived(InputStream is, String command) throws IOException {
-        assertEquals("No ACK for command=" + command, 0, is.read());
-    }
-
-    protected void sendFileError(com.jcraft.jsch.Session session, String path, String name, String data) throws Exception {
-        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
-        String command = "scp -t " + path;
-        c.setCommand(command);
-        c.connect();
-
-        try (OutputStream os = c.getOutputStream();
-             InputStream is = c.getInputStream()) {
-
-            assertAckReceived(is, command);
-
-            command = "C7777 " + data.length() + " " + name;
-            os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
-            os.flush();
-            assertEquals("Mismatched response for command=" + command, 2, is.read());
-        } finally {
-            c.disconnect();
-        }
-    }
-
-    protected void sendDir(com.jcraft.jsch.Session session, String path, String dirName, String fileName, String data) throws Exception {
-        ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
-        String command = "scp -t -r " + path;
-        c.setCommand(command);
-        c.connect();
-
-        try (OutputStream os = c.getOutputStream();
-             InputStream is = c.getInputStream()) {
-
-            assertAckReceived(is, command);
-            assertAckReceived(os, is, "D0755 0 " + dirName);
-            assertAckReceived(os, is, "C7777 " + data.length() + " " + fileName);
-
-            os.write(data.getBytes(StandardCharsets.UTF_8));
-            os.flush();
-            assertAckReceived(is, "Send data of " + path);
-
-            os.write(0);
-            os.flush();
-
-            os.write("E\n".getBytes(StandardCharsets.UTF_8));
-            os.flush();
-            assertAckReceived(is, "Signal end of " + path);
-        } finally {
-            c.disconnect();
-        }
-    }
-
-    private static String readLine(InputStream in) throws IOException {
-        try (OutputStream baos = new ByteArrayOutputStream()) {
-            for (;;) {
-                int c = in.read();
-                if (c == '\n') {
-                    return baos.toString();
-                } else if (c == -1) {
-                    throw new IOException("End of stream");
-                } else {
-                    baos.write(c);
-                }
-            }
-        }
-    }
-
-    private static ScpClient createScpClient(ClientSession session) {
-        return session.createScpClient(getScpTransferEventListener(session));
-    }
-
-    private static ScpTransferEventListener getScpTransferEventListener(ClientSession session) {
-        return OUTPUT_DEBUG_MESSAGES ? DEBUG_LISTENER : ScpTransferEventListener.EMPTY;
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
deleted file mode 100644
index 60b9403..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.simple;
-
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.junit.After;
-import org.junit.Before;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class BaseSimpleClientTestSupport extends BaseTestSupport {
-    public static final long CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
-    public static final long AUTH_TIMEOUT = TimeUnit.SECONDS.toMillis(7L);
-
-    protected SshServer sshd;
-    protected SshClient client;
-    protected int port;
-    protected SimpleClient simple;
-
-    protected BaseSimpleClientTestSupport() {
-        super();
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        sshd = setupTestServer();
-        sshd.start();
-        port = sshd.getPort();
-        client = setupTestClient();
-
-        simple = SshClient.wrapAsSimpleClient(client);
-        simple.setConnectTimeout(CONNECT_TIMEOUT);
-        simple.setAuthenticationTimeout(AUTH_TIMEOUT);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (sshd != null) {
-            sshd.stop(true);
-        }
-        if (simple != null) {
-            simple.close();
-        }
-        if (client != null) {
-            client.stop();
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java
deleted file mode 100644
index ab96bc1..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.simple;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import org.apache.sshd.client.scp.CloseableScpClient;
-import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-import org.apache.sshd.util.test.Utils;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class SimpleScpClientTest extends BaseSimpleClientTestSupport {
-    private final Path targetPath;
-    private final Path parentPath;
-    private final FileSystemFactory fileSystemFactory;
-
-    public SimpleScpClientTest() throws Exception {
-        targetPath = detectTargetFolder();
-        parentPath = targetPath.getParent();
-        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
-    }
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        sshd.setCommandFactory(new ScpCommandFactory());
-        sshd.setFileSystemFactory(fileSystemFactory);
-        client.start();
-    }
-
-    @Test
-    public void testSessionClosedWhenClientClosed() throws Exception {
-        try (CloseableScpClient scp = login()) {
-            assertTrue("SCP not open", scp.isOpen());
-
-            Session session = scp.getClientSession();
-            assertTrue("Session not open", session.isOpen());
-
-            scp.close();
-            assertFalse("Session not closed", session.isOpen());
-            assertFalse("SCP not closed", scp.isOpen());
-        }
-    }
-
-    @Test
-    public void testScpUploadProxy() throws Exception {
-        try (CloseableScpClient scp = login()) {
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-            Path localFile = localDir.resolve("file.txt");
-            String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
-            byte[] written = Utils.writeFile(localFile, data);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            Path remoteFile = remoteDir.resolve(localFile.getFileName());
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-            scp.upload(localFile, remotePath);
-
-            byte[] uploaded = Files.readAllBytes(remoteFile);
-            assertArrayEquals("Mismatched uploaded data", written, uploaded);
-        }
-    }
-
-    @Test
-    public void testScpDownloadProxy() throws Exception {
-        try (CloseableScpClient scp = login()) {
-            Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
-            Utils.deleteRecursive(scpRoot);
-
-            Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
-            Path remoteFile = remoteDir.resolve("file.txt");
-            String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
-            byte[] written = Utils.writeFile(remoteFile, data);
-            Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
-            Path localFile = localDir.resolve(remoteFile.getFileName());
-            String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
-            scp.download(remotePath, localFile);
-
-            byte[] downloaded = Files.readAllBytes(localFile);
-            assertArrayEquals("Mismatched downloaded data", written, downloaded);
-        }
-    }
-
-    protected CloseableScpClient login() throws IOException {
-        return simple.scpLogin(TEST_LOCALHOST, port, getCurrentTestName(), getCurrentTestName());
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
index a77e807..ba3ee68 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
@@ -33,6 +33,7 @@ import org.apache.sshd.server.auth.password.PasswordAuthenticator;
 import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
 import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
 import org.apache.sshd.util.test.Utils;
+import org.apache.sshd.util.test.client.simple.BaseSimpleClientTestSupport;
 import org.junit.FixMethodOrder;
 import org.junit.Test;
 import org.junit.runners.MethodSorters;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java b/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java
deleted file mode 100644
index 8ffe6ac..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.server.command;
-
-import java.util.concurrent.ExecutorService;
-
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.apache.sshd.util.test.NoIoTestCase;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.runners.MethodSorters;
-import org.mockito.Mockito;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Category({ NoIoTestCase.class })
-public class ScpCommandFactoryTest extends BaseTestSupport {
-    public ScpCommandFactoryTest() {
-        super();
-    }
-
-    /**
-     * Make sure that the builder returns a factory with the default values
-     * if no {@code withXXX} method is invoked
-     */
-    @Test
-    public void testBuilderDefaultFactoryValues() {
-        ScpCommandFactory factory = new ScpCommandFactory.Builder().build();
-        assertNull("Mismatched delegate", factory.getDelegateCommandFactory());
-        assertNull("Mismatched executor", factory.getExecutorService());
-        assertEquals("Mismatched send size", ScpHelper.MIN_SEND_BUFFER_SIZE, factory.getSendBufferSize());
-        assertEquals("Mismatched receive size", ScpHelper.MIN_RECEIVE_BUFFER_SIZE, factory.getReceiveBufferSize());
-        assertFalse("Mismatched shutdown state", factory.isShutdownOnExit());
-    }
-
-    /**
-     * Make sure that the builder initializes correctly the built factory
-     */
-    @Test
-    public void testBuilderCorrectlyInitializesFactory() {
-        CommandFactory delegate = dummyFactory();
-        ExecutorService service = dummyExecutor();
-        int receiveSize = Short.MAX_VALUE;
-        int sendSize = receiveSize + Long.SIZE;
-        ScpCommandFactory factory = new ScpCommandFactory.Builder()
-                .withDelegate(delegate)
-                .withExecutorService(service)
-                .withSendBufferSize(sendSize)
-                .withReceiveBufferSize(receiveSize)
-                .withShutdownOnExit(true)
-                .build();
-        assertSame("Mismatched delegate", delegate, factory.getDelegateCommandFactory());
-        assertSame("Mismatched executor", service, factory.getExecutorService());
-        assertEquals("Mismatched send size", sendSize, factory.getSendBufferSize());
-        assertEquals("Mismatched receive size", receiveSize, factory.getReceiveBufferSize());
-        assertTrue("Mismatched shutdown state", factory.isShutdownOnExit());
-    }
-
-    /**
-     * <UL>
-     * <LI>
-     * Make sure the builder returns new instances on every call to
-     * {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
-     * </LI>
-     *
-     * <LI>
-     * Make sure values are preserved between successive invocations
-     * of the {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
-     * </LI>
-     * </UL
-     */
-    @Test
-    public void testBuilderUniqueInstance() {
-        ScpCommandFactory.Builder builder = new ScpCommandFactory.Builder();
-        ScpCommandFactory f1 = builder.withDelegate(dummyFactory()).build();
-        ScpCommandFactory f2 = builder.build();
-        assertNotSame("No new instance built", f1, f2);
-        assertSame("Mismatched delegate", f1.getDelegateCommandFactory(), f2.getDelegateCommandFactory());
-
-        ScpCommandFactory f3 = builder.withDelegate(dummyFactory()).build();
-        assertNotSame("Delegate not changed", f1.getDelegateCommandFactory(), f3.getDelegateCommandFactory());
-    }
-
-    private static ExecutorService dummyExecutor() {
-        return Mockito.mock(ExecutorService.class);
-    }
-
-    private static CommandFactory dummyFactory() {
-        return Mockito.mock(CommandFactory.class);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java b/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java
deleted file mode 100644
index 6beb011..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.util.test;
-
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class UnknownCommandFactory implements CommandFactory {
-    public static final UnknownCommandFactory INSTANCE = new UnknownCommandFactory();
-
-    public UnknownCommandFactory() {
-        super();
-    }
-
-    @Override
-    public Command createCommand(String command) {
-        return new UnknownCommand(command);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java b/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
index 73e8f7c..a007239 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
@@ -67,6 +67,7 @@ import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.apache.sshd.server.shell.UnknownCommandFactory;
 
 public final class Utils {
     /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java
new file mode 100644
index 0000000..f42a550
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util.test.client.simple;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.simple.SimpleClient;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class BaseSimpleClientTestSupport extends BaseTestSupport {
+    public static final long CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
+    public static final long AUTH_TIMEOUT = TimeUnit.SECONDS.toMillis(7L);
+
+    protected SshServer sshd;
+    protected SshClient client;
+    protected int port;
+    protected SimpleClient simple;
+
+    protected BaseSimpleClientTestSupport() {
+        super();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        sshd = setupTestServer();
+        sshd.start();
+        port = sshd.getPort();
+        client = setupTestClient();
+
+        simple = SshClient.wrapAsSimpleClient(client);
+        simple.setConnectTimeout(CONNECT_TIMEOUT);
+        simple.setAuthenticationTimeout(AUTH_TIMEOUT);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (sshd != null) {
+            sshd.stop(true);
+        }
+        if (simple != null) {
+            simple.close();
+        }
+        if (client != null) {
+            client.stop();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
index 0d42626..64eab6a 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
@@ -25,7 +25,7 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
+import org.apache.sshd.server.shell.UnknownCommand;
 
 /**
  * TODO Add javadoc

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-scp/pom.xml b/sshd-scp/pom.xml
new file mode 100644
index 0000000..2af3446
--- /dev/null
+++ b/sshd-scp/pom.xml
@@ -0,0 +1,201 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>sshd-scp</artifactId>
+    <name>Apache Mina SSHD :: SCP</name>
+    <packaging>jar</packaging>
+    <inceptionYear>2018</inceptionYear>
+
+    <properties>
+        <projectRoot>${project.basedir}/..</projectRoot>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.ethz.ganymed</groupId>
+            <artifactId>ganymed-ssh2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jzlib</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/filtered-resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                    <reportsDirectory>${project.build.directory}/surefire-reports-nio2</reportsDirectory>
+                    <systemProperties>
+                        <org.apache.sshd.common.io.IoServiceFactoryFactory>org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory</org.apache.sshd.common.io.IoServiceFactoryFactory>
+                    </systemProperties>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <additionalparam>-Xdoclint:none</additionalparam>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>test-mina</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+
+            <dependencies>
+		        <dependency>
+		            <groupId>org.apache.sshd</groupId>
+		            <artifactId>sshd-mina</artifactId>
+		            <version>${project.version}</version>
+		            <scope>test</scope>
+		        </dependency>
+            </dependencies>
+
+            <build>
+                <plugins>
+		            <plugin>
+		                <groupId>org.apache.maven.plugins</groupId>
+		                <artifactId>maven-surefire-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>mina</id>
+                                <goals>
+                                    <goal>test</goal>
+                                </goals>
+                                <configuration>
+                                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+				                    <reportsDirectory>${project.build.directory}/surefire-reports-mina</reportsDirectory>
+				                    <systemProperties>
+				                        <org.apache.sshd.common.io.IoServiceFactoryFactory>org.apache.sshd.common.io.mina.MinaServiceFactoryFactory</org.apache.sshd.common.io.IoServiceFactoryFactory>
+				                    </systemProperties>
+                                </configuration>
+                            </execution>
+                        </executions>
+		            </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+        <profile>
+            <id>test-netty</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+
+            <dependencies>
+		        <dependency>
+		            <groupId>org.apache.sshd</groupId>
+		            <artifactId>sshd-netty</artifactId>
+		            <version>${project.version}</version>
+		            <scope>test</scope>
+		        </dependency>
+            </dependencies>
+
+            <build>
+                <plugins>
+		            <plugin>
+		                <groupId>org.apache.maven.plugins</groupId>
+		                <artifactId>maven-surefire-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>netty</id>
+                                <goals>
+                                    <goal>test</goal>
+                                </goals>
+                                <configuration>
+                                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+				                    <reportsDirectory>${project.build.directory}/surefire-reports-netty</reportsDirectory>
+				                    <systemProperties>
+				                        <org.apache.sshd.common.io.IoServiceFactoryFactory>org.apache.sshd.netty.NettyIoServiceFactoryFactory</org.apache.sshd.common.io.IoServiceFactoryFactory>
+				                    </systemProperties>
+                                </configuration>
+                            </execution>
+                        </executions>
+		            </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>


[4/8] mina-sshd git commit: [SSHD-818] Split SCP code (client + server) to its own module

Posted by lg...@apache.org.
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);
+}