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 2016/02/18 15:36:29 UTC

mina-sshd git commit: [SSHD-649] Provide pluggable file data stream opener for SCP command

Repository: mina-sshd
Updated Branches:
  refs/heads/master 8628bd2d9 -> 4a8c81717


[SSHD-649] Provide pluggable file data stream opener for SCP command


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

Branch: refs/heads/master
Commit: 4a8c81717829bc445bec87fabf51fea3951c0c84
Parents: 8628bd2
Author: Lyor Goldstein <ly...@gmail.com>
Authored: Thu Feb 18 16:37:10 2016 +0200
Committer: Lyor Goldstein <ly...@gmail.com>
Committed: Thu Feb 18 16:37:10 2016 +0200

----------------------------------------------------------------------
 .../sshd/client/scp/DefaultScpClient.java       |  22 +--
 .../client/scp/DefaultScpStreamResolver.java    |   4 +-
 .../sshd/client/scp/ScpClientCreator.java       | 103 ++++++++++++
 .../client/session/AbstractClientSession.java   |  29 +++-
 .../sshd/client/session/ClientSession.java      |  41 +----
 .../scp/LocalFileScpSourceStreamResolver.java   |  90 -----------
 .../scp/LocalFileScpTargetStreamResolver.java   | 150 ------------------
 .../apache/sshd/common/scp/ScpFileOpener.java   |  59 +++++++
 .../org/apache/sshd/common/scp/ScpHelper.java   |  28 ++--
 .../common/scp/ScpSourceStreamResolver.java     |   7 +-
 .../common/scp/ScpTargetStreamResolver.java     |  14 +-
 .../common/scp/impl/DefaultScpFileOpener.java   |  63 ++++++++
 .../impl/LocalFileScpSourceStreamResolver.java  |  97 ++++++++++++
 .../impl/LocalFileScpTargetStreamResolver.java  | 158 +++++++++++++++++++
 .../org/apache/sshd/server/scp/ScpCommand.java  |  31 +++-
 .../sshd/server/scp/ScpCommandFactory.java      |  24 ++-
 .../org/apache/sshd/client/scp/ScpTest.java     | 103 +++++++++++-
 17 files changed, 704 insertions(+), 319 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
index c3b9d02..5c2fd74 100644
--- 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
@@ -42,10 +42,12 @@ 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.ScpLocation;
 import org.apache.sshd.common.scp.ScpTimestamp;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.impl.DefaultScpFileOpener;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
@@ -55,15 +57,13 @@ import org.apache.sshd.common.util.io.NoCloseInputStream;
  */
 public class DefaultScpClient extends AbstractScpClient {
 
-    private final ClientSession clientSession;
-    private final ScpTransferEventListener listener;
+    protected final ClientSession clientSession;
+    protected final ScpFileOpener opener;
+    protected final ScpTransferEventListener listener;
 
-    public DefaultScpClient(ClientSession clientSession) {
-        this(clientSession, ScpTransferEventListener.EMPTY);
-    }
-
-    public DefaultScpClient(ClientSession clientSession, ScpTransferEventListener eventListener) {
+    public DefaultScpClient(ClientSession clientSession, ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
         this.clientSession = ValidateUtils.checkNotNull(clientSession, "No client session");
+        this.opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
         this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
     }
 
@@ -80,7 +80,7 @@ public class DefaultScpClient extends AbstractScpClient {
         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), listener);
+            ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
             helper.receiveFileStream(local, ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
             handleCommandExitStatus(cmd, channel);
         } finally {
@@ -95,7 +95,7 @@ public class DefaultScpClient extends AbstractScpClient {
         ChannelExec channel = openCommandChannel(session, cmd);
         try (InputStream invOut = channel.getInvertedOut();
              OutputStream invIn = channel.getInvertedIn()) {
-            ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, listener);
+            ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
             helper.receive(local,
                     options.contains(Option.Recursive),
                     options.contains(Option.TargetIsDirectory),
@@ -119,7 +119,7 @@ public class DefaultScpClient extends AbstractScpClient {
         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), listener);
+            ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
             final Path mockPath = new MockPath(remote);
             helper.sendStream(new DefaultScpStreamResolver(name, mockPath, perms, time, size, local, cmd),
                               time != null, ScpHelper.DEFAULT_SEND_BUFFER_SIZE);
@@ -147,7 +147,7 @@ public class DefaultScpClient extends AbstractScpClient {
 
             try (InputStream invOut = channel.getInvertedOut();
                  OutputStream invIn = channel.getInvertedIn()) {
-                ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, listener);
+                ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
                 executor.execute(helper, local, options);
             } finally {
                 try {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
index 4ae22fb..e6362b8 100644
--- 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
@@ -20,12 +20,14 @@ 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>
@@ -75,7 +77,7 @@ public class DefaultScpStreamResolver implements ScpSourceStreamResolver {
     }
 
     @Override
-    public InputStream resolveSourceStream() throws IOException {
+    public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
         return local;
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
new file mode 100644
index 0000000..cfe598e
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpClientCreator {
+    /**
+     * 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)
+     */
+    ScpClient createScpClient();
+
+    /**
+     * 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
+     */
+    ScpClient createScpClient(ScpTransferEventListener 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
+     */
+    ScpClient createScpClient(ScpFileOpener opener);
+
+    /**
+     * 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 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
+     * @see #createScpClient(ScpFileOpener)
+     * @see #createScpClient(ScpFileOpener, ScpTransferEventListener)
+     */
+    void setScpFileOpener(ScpFileOpener opener);
+
+    /**
+     * @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/4a8c8171/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 195d5ef..25e441c 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
@@ -50,6 +50,7 @@ import org.apache.sshd.common.channel.Channel;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.forward.TcpipForwarder;
 import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.scp.ScpFileOpener;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.session.AbstractSession;
 import org.apache.sshd.common.session.ConnectionService;
@@ -68,6 +69,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private PasswordIdentityProvider passwordIdentityProvider;
     private List<NamedFactory<UserAuth>> userAuthFactories;
     private ScpTransferEventListener scpListener;
+    private ScpFileOpener scpOpener;
     private SocketAddress connectAddress;
 
     protected AbstractClientSession(ClientFactoryManager factoryManager, IoSession ioSession) {
@@ -245,6 +247,16 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
+    public ScpFileOpener getScpFileOpener() {
+        return scpOpener;
+    }
+
+    @Override
+    public void setScpFileOpener(ScpFileOpener opener) {
+        scpOpener = opener;
+    }
+
+    @Override
     public ScpTransferEventListener getScpTransferEventListener() {
         return scpListener;
     }
@@ -254,17 +266,26 @@ public abstract class AbstractClientSession extends AbstractSession implements C
         scpListener = listener;
     }
 
-    @Override
+    @Override   // TODO make this a default method in JDK-8
     public ScpClient createScpClient() {
-        return createScpClient(getScpTransferEventListener());
+        return createScpClient(getScpFileOpener(), getScpTransferEventListener());
     }
 
-    @Override
+    @Override   // TODO make this a default method in JDK-8
     public ScpClient createScpClient(ScpTransferEventListener listener) {
-        return new DefaultScpClient(this, listener);
+        return createScpClient(getScpFileOpener(), listener);
     }
 
     @Override   // TODO make this a default method in JDK-8
+    public ScpClient createScpClient(ScpFileOpener opener) {
+        return createScpClient(opener, getScpTransferEventListener());
+    }
+
+    @Override
+    public ScpClient createScpClient(ScpFileOpener opener, ScpTransferEventListener listener) {
+        return new DefaultScpClient(this, opener, listener);
+    }
+    @Override   // TODO make this a default method in JDK-8
     public SftpClient createSftpClient() throws IOException {
         return createSftpClient(SftpVersionSelector.CURRENT);
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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 db3f10f..d9633b0 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
@@ -33,11 +33,10 @@ 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.AuthFuture;
-import org.apache.sshd.client.scp.ScpClient;
+import org.apache.sshd.client.scp.ScpClientCreator;
 import org.apache.sshd.client.subsystem.sftp.SftpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
 import org.apache.sshd.common.future.KeyExchangeFuture;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
 
@@ -69,7 +68,9 @@ 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, ClientAuthenticationManager {
+public interface ClientSession
+            extends Session, ScpClientCreator,
+                    ClientAuthenticationManager {
     enum ClientSessionEvent {
         TIMEOUT,
         CLOSED,
@@ -158,40 +159,6 @@ public interface ClientSession extends Session, ClientAuthenticationManager {
     ChannelDirectTcpip createDirectTcpipChannel(SshdSocketAddress local, SshdSocketAddress remote) throws IOException;
 
     /**
-     * Create an SCP client from this session.
-     *
-     * @return An {@link ScpClient} instance. <B>Note:</B> uses the currently
-     * registered {@link ScpTransferEventListener} if any
-     * @see #setScpTransferEventListener(ScpTransferEventListener)
-     */
-    ScpClient createScpClient();
-
-    /**
-     * 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
-     */
-    ScpClient createScpClient(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);
-
-    /**
      * Create an SFTP client from this session.
      *
      * @return The created {@link SftpClient}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpSourceStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpSourceStreamResolver.java
deleted file mode 100644
index 7e7d5f2..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpSourceStreamResolver.java
+++ /dev/null
@@ -1,90 +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.Files;
-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.Set;
-
-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 class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implements ScpSourceStreamResolver {
-    private final Path path;
-    private final Path name;
-    private final Set<PosixFilePermission> perms;
-    private final long size;
-    private final ScpTimestamp time;
-
-    public LocalFileScpSourceStreamResolver(Path path) throws IOException {
-        this.path = ValidateUtils.checkNotNull(path, "No path specified");
-        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() throws IOException {
-        return Files.newInputStream(getEventListenerFilePath());
-    }
-
-    @Override
-    public String toString() {
-        return String.valueOf(getEventListenerFilePath());
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpTargetStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpTargetStreamResolver.java
deleted file mode 100644
index f6b86d9..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/LocalFileScpTargetStreamResolver.java
+++ /dev/null
@@ -1,150 +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.OutputStream;
-import java.io.StreamCorruptedException;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-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.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 {
-    private final Path path;
-    private final Boolean status;
-    private Path file;
-
-    public LocalFileScpTargetStreamResolver(Path path) throws IOException {
-        LinkOption[] options = IoUtils.getLinkOptions(false);
-        this.status = IoUtils.checkFileExists(path, options);
-        if (status == null) {
-            throw new AccessDeniedException("Receive target file path existence status cannot be determined: " + path);
-        }
-
-        this.path = path;
-    }
-
-    @Override
-    public OutputStream resolveTargetStream(String name, long length, Set<PosixFilePermission> perms) throws IOException {
-        if (file != null) {
-            throw new StreamCorruptedException("resolveTargetStream(" + name + ")[" + perms + "] already resolved: " + file);
-        }
-
-        LinkOption[] options = IoUtils.getLinkOptions(false);
-        if (status && Files.isDirectory(path, options)) {
-            String localName = name.replace('/', File.separatorChar);   // in case we are running on Windows
-            file = path.resolve(localName);
-        } else if (status && Files.isRegularFile(path, options)) {
-            file = path;
-        } else if (!status) {
-            Path parent = path.getParent();
-
-            Boolean parentStatus = IoUtils.checkFileExists(parent, options);
-            if (parentStatus == null) {
-                throw new AccessDeniedException("Receive file parent (" + parent + ") existence status cannot be determined for " + path);
-            }
-
-            if (parentStatus && Files.isDirectory(parent, options)) {
-                file = path;
-            }
-        }
-
-        if (file == null) {
-            throw new IOException("Can not write to " + path);
-        }
-
-        Boolean fileStatus = IoUtils.checkFileExists(file, options);
-        if (fileStatus == null) {
-            throw new AccessDeniedException("Receive file existence status cannot be determined: " + file);
-        }
-
-        if (fileStatus) {
-            if (Files.isDirectory(file, options)) {
-                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 Files.newOutputStream(file);
-    }
-
-    @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 {
-        if (log.isTraceEnabled()) {
-            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 (log.isTraceEnabled()) {
-                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/4a8c8171/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
new file mode 100644
index 0000000..e2db9b8
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
@@ -0,0 +1,59 @@
+/*
+ * 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.io.OutputStream;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+
+import org.apache.sshd.common.session.Session;
+
+/**
+ * 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 {
+    /**
+     * 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;
+
+    /**
+     * 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 OutputStream} never {@code null}
+     * @throws IOException If failed to open the file
+     */
+    OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
index 9a0f806..f415db3 100644
--- 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
@@ -32,6 +32,7 @@ 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;
@@ -45,6 +46,9 @@ import java.util.concurrent.TimeUnit;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.file.util.MockPath;
 import org.apache.sshd.common.scp.ScpTransferEventListener.FileOperation;
+import org.apache.sshd.common.scp.impl.DefaultScpFileOpener;
+import org.apache.sshd.common.scp.impl.LocalFileScpSourceStreamResolver;
+import org.apache.sshd.common.scp.impl.LocalFileScpTargetStreamResolver;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.SessionHolder;
 import org.apache.sshd.common.util.GenericUtils;
@@ -92,24 +96,27 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
     public static final int S_IWOTH = 0000002;
     public static final int S_IXOTH = 0000001;
 
-    protected final FileSystem fileSystem;
     protected final InputStream in;
     protected final OutputStream out;
+    protected final FileSystem fileSystem;
+    protected final ScpFileOpener opener;
     protected final ScpTransferEventListener listener;
 
-    private final Session session;
+    private final Session sessionInstance;
 
-    public ScpHelper(Session session, InputStream in, OutputStream out, FileSystem fileSystem, ScpTransferEventListener eventListener) {
-        this.session = ValidateUtils.checkNotNull(session, "No session");
+    public ScpHelper(Session session, InputStream in, OutputStream out,
+            FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) {
+        this.sessionInstance = ValidateUtils.checkNotNull(session, "No session");
         this.in = ValidateUtils.checkNotNull(in, "No input stream");
         this.out = ValidateUtils.checkNotNull(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 session;
+        return sessionInstance;
     }
 
     public void receiveFileStream(final OutputStream local, final int bufferSize) throws IOException {
@@ -124,7 +131,8 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
                 receiveStream(line, new ScpTargetStreamResolver() {
                     @SuppressWarnings("synthetic-access")
                     @Override
-                    public OutputStream resolveTargetStream(String name, long length, Set<PosixFilePermission> perms) throws IOException {
+                    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);
@@ -331,7 +339,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
                       this, header, path, preserve, time, bufferSize);
         }
 
-        receiveStream(header, new LocalFileScpTargetStreamResolver(path), time, preserve, bufferSize);
+        receiveStream(header, new LocalFileScpTargetStreamResolver(path, opener), time, preserve, bufferSize);
     }
 
     public void receiveStream(String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
@@ -370,7 +378,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
 
         try (
                 InputStream is = new LimitInputStream(this.in, length);
-                OutputStream os = resolver.resolveTargetStream(name, length, perms)
+                OutputStream os = resolver.resolveTargetStream(getSession(), name, length, perms)
         ) {
             ack();
 
@@ -551,7 +559,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
             log.debug("sendFile({})[preserve={},buffer-size={}] Sending file {}", this, preserve, bufferSize, path);
         }
 
-        sendStream(new LocalFileScpSourceStreamResolver(path), preserve, bufferSize);
+        sendStream(new LocalFileScpSourceStreamResolver(path, opener), preserve, bufferSize);
     }
 
     public void sendStream(ScpSourceStreamResolver resolver, boolean preserve, int bufferSize) throws IOException {
@@ -615,7 +623,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
         }
         validateAckReplyCode(cmd, resolver, readyCode, false);
 
-        try (InputStream in = resolver.resolveSourceStream()) {
+        try (InputStream in = resolver.resolveSourceStream(getSession())) {
             Path path = resolver.getEventListenerFilePath();
             try {
                 listener.startFileEvent(FileOperation.SEND, path, fileSize, perms);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
index 73af189..feeecbc 100644
--- 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
@@ -21,10 +21,13 @@ 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>
  */
@@ -61,8 +64,10 @@ public interface ScpSourceStreamResolver {
     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() throws IOException;
+    InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException;
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
index 8b95b4f..9a70302 100644
--- 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
@@ -21,10 +21,13 @@ 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>
  */
@@ -33,13 +36,16 @@ public interface ScpTargetStreamResolver {
      * Called when receiving a file in order to obtain an output stream
      * for the incoming data
      *
-     * @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 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(String name, long length, Set<PosixFilePermission> perms) throws IOException;
+    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}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/DefaultScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/DefaultScpFileOpener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/DefaultScpFileOpener.java
new file mode 100644
index 0000000..0545185
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/DefaultScpFileOpener.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.common.scp.impl;
+
+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.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);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/LocalFileScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/LocalFileScpSourceStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/LocalFileScpSourceStreamResolver.java
new file mode 100644
index 0000000..9a3a5c3
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/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.impl;
+
+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.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.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 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 = ValidateUtils.checkNotNull(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/4a8c8171/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/LocalFileScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/LocalFileScpTargetStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/LocalFileScpTargetStreamResolver.java
new file mode 100644
index 0000000..7d1b78b
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/impl/LocalFileScpTargetStreamResolver.java
@@ -0,0 +1,158 @@
+/*
+ * 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.impl;
+
+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(false);
+        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(false);
+        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 {
+        if (log.isTraceEnabled()) {
+            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 (log.isTraceEnabled()) {
+                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/4a8c8171/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
index 2a48a9a..9a197f8 100644
--- 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
@@ -29,8 +29,12 @@ 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.impl.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.ThreadUtils;
@@ -50,9 +54,14 @@ import org.apache.sshd.server.session.ServerSessionHolder;
  */
 public class ScpCommand
         extends AbstractLoggingBean
-        implements Command, Runnable, FileSystemAware, SessionAware, ServerSessionHolder {
+        implements Command, Runnable, FileSystemAware, SessionAware,
+                   SessionHolder<Session>, ServerSessionHolder {
+
+    protected final String name;
+    protected final int sendBufferSize;
+    protected final int receiveBufferSize;
+    protected final ScpFileOpener opener;
 
-    protected String name;
     protected boolean optR;
     protected boolean optT;
     protected boolean optF;
@@ -68,8 +77,6 @@ public class ScpCommand
     protected ExecutorService executors;
     protected boolean shutdownExecutor;
     protected Future<?> pendingFuture;
-    protected int sendBufferSize;
-    protected int receiveBufferSize;
     protected ScpTransferEventListener listener;
     protected ServerSession serverSession;
 
@@ -83,12 +90,16 @@ public class ScpCommand
      *                        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, ScpTransferEventListener eventListener) {
+    public ScpCommand(String command,
+            ExecutorService executorService, boolean shutdownOnExit,
+            int sendSize, int receiveSize,
+            ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
         name = command;
 
         if (executorService == null) {
@@ -114,6 +125,7 @@ public class ScpCommand
         }
         receiveBufferSize = receiveSize;
 
+        opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
         listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
 
         if (log.isDebugEnabled()) {
@@ -169,6 +181,11 @@ public class ScpCommand
     }
 
     @Override
+    public Session getSession() {
+        return getServerSession();
+    }
+
+    @Override
     public ServerSession getServerSession() {
         return serverSession;
     }
@@ -252,7 +269,7 @@ public class ScpCommand
     public void run() {
         int exitValue = ScpHelper.OK;
         String exitMessage = null;
-        ScpHelper helper = new ScpHelper(getServerSession(), in, out, fileSystem, listener);
+        ScpHelper helper = new ScpHelper(getServerSession(), in, out, fileSystem, opener, listener);
         try {
             if (optT) {
                 helper.receive(helper.resolveLocalPath(path), optR, optD, optP, receiveBufferSize);
@@ -312,6 +329,6 @@ public class ScpCommand
 
     @Override
     public String toString() {
-        return name;
+        return getClass().getSimpleName() + "(" + getSession() + ") " + name;
     }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
index 1ffde01..1f52ea4 100644
--- 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
@@ -22,6 +22,7 @@ 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.ScpHelper;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.util.EventListenerUtils;
@@ -49,6 +50,11 @@ public class ScpCommandFactory implements CommandFactory, Cloneable, ExecutorSer
             super();
         }
 
+        public Builder withFileOpener(ScpFileOpener opener) {
+            factory.setFileOpener(opener);
+            return this;
+        }
+
         public Builder withDelegate(CommandFactory delegate) {
             factory.setDelegateCommandFactory(delegate);
             return this;
@@ -97,6 +103,7 @@ public class ScpCommandFactory implements CommandFactory, Cloneable, ExecutorSer
     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<>();
@@ -106,6 +113,18 @@ public class ScpCommandFactory implements CommandFactory, Cloneable, ExecutorSer
         listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), listeners);
     }
 
+    public ScpFileOpener getFileOpener() {
+        return fileOpener;
+    }
+
+    /**
+     * @param fileOpener The {@link ScpFileOpener} to use - if {@code null}.
+     * the a default opener is used
+     */
+    public void setFileOpener(ScpFileOpener fileOpener) {
+        this.fileOpener = fileOpener;
+    }
+
     public CommandFactory getDelegateCommandFactory() {
         return delegate;
     }
@@ -226,7 +245,10 @@ public class ScpCommandFactory implements CommandFactory, Cloneable, ExecutorSer
     @Override
     public Command createCommand(String command) {
         if (command.startsWith(ScpHelper.SCP_COMMAND_PREFIX)) {
-            return new ScpCommand(command, getExecutorService(), isShutdownOnExit(), getSendBufferSize(), getReceiveBufferSize(), listenerProxy);
+            return new ScpCommand(command,
+                    getExecutorService(), isShutdownOnExit(),
+                    getSendBufferSize(), getReceiveBufferSize(),
+                    getFileOpener(), listenerProxy);
         }
 
         CommandFactory factory = getDelegateCommandFactory();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4a8c8171/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
index 9f110ea..054ace8 100644
--- 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
@@ -26,6 +26,7 @@ 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;
@@ -36,6 +37,7 @@ 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 org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
@@ -45,8 +47,11 @@ 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.impl.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;
@@ -728,6 +733,95 @@ public class ScpTest extends BaseTestSupport {
         }
     }
 
+    @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();
+        TrackingFileOpener serverOpener = new TrackingFileOpener();
+        factory.setFileOpener(serverOpener);
+
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, 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(5L));
+
+                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(5L));
+
+                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 {
+                client.stop();
+            }
+        }
+    }
+
     @Test   // see SSHD-628
     public void testScpExitStatusPropagation() throws Exception {
         final int TEST_EXIT_VALUE = 7365;
@@ -735,8 +829,8 @@ public class ScpTest extends BaseTestSupport {
             private ExitCallback delegate;
 
             public InternalScpCommand(String command, ExecutorService executorService, boolean shutdownOnExit,
-                    int sendSize, int receiveSize, ScpTransferEventListener eventListener) {
-                super(command, executorService, shutdownOnExit, sendSize, receiveSize, eventListener);
+                    int sendSize, int receiveSize, ScpFileOpener opener, ScpTransferEventListener eventListener) {
+                super(command, executorService, shutdownOnExit, sendSize, receiveSize, opener, eventListener);
             }
 
             @Override
@@ -770,7 +864,10 @@ public class ScpTest extends BaseTestSupport {
             @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(), ScpTransferEventListener.EMPTY);
+                return new InternalScpCommand(command,
+                        getExecutorService(), isShutdownOnExit(),
+                        getSendBufferSize(), getReceiveBufferSize(),
+                        DefaultScpFileOpener.INSTANCE, ScpTransferEventListener.EMPTY);
             }
         });