You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2013/07/22 20:37:02 UTC

[5/9] git commit: Provide an SCP client

Provide an SCP client

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

Branch: refs/heads/master
Commit: 16fc6a80714f2322479eaa5dbffea4e6f179a887
Parents: c91e7de
Author: Guillaume Nodet <gn...@apache.org>
Authored: Sun Jul 21 21:56:59 2013 +0200
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Sun Jul 21 21:56:59 2013 +0200

----------------------------------------------------------------------
 .../java/org/apache/sshd/ClientChannel.java     |   4 +
 .../java/org/apache/sshd/ClientSession.java     |   6 +
 .../main/java/org/apache/sshd/SshServer.java    |   3 +-
 .../java/org/apache/sshd/client/ScpClient.java  |  23 ++
 .../client/channel/AbstractClientChannel.java   |  10 +
 .../sshd/client/channel/ChannelSession.java     |  15 +-
 .../sshd/client/scp/DefaultScpClient.java       | 151 ++++++++
 .../sshd/client/session/ClientSessionImpl.java  |   6 +
 .../org/apache/sshd/common/FactoryManager.java  |   4 +-
 .../org/apache/sshd/common/scp/ScpHelper.java   | 363 +++++++++++++++++++
 .../apache/sshd/server/command/ScpCommand.java  | 331 ++---------------
 11 files changed, 601 insertions(+), 315 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/ClientChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/ClientChannel.java
index d19d12f..4b01b0a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/ClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/ClientChannel.java
@@ -56,6 +56,10 @@ public interface ClientChannel {
      */
     OutputStream getInvertedIn();
 
+    InputStream getInvertedOut();
+
+    InputStream getInvertedErr();
+
     /**
      * Set an input stream that will be read by this channel and forwarded to
      * the remote channel.  Note that using such a stream will create an additional

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
index 8cd7f22..6b7b1af 100644
--- a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
@@ -23,6 +23,7 @@ import java.security.KeyPair;
 import java.util.Map;
 
 import org.apache.sshd.client.ClientFactoryManager;
+import org.apache.sshd.client.ScpClient;
 import org.apache.sshd.client.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ChannelShell;
@@ -112,6 +113,11 @@ public interface ClientSession extends Session {
     ChannelDirectTcpip createDirectTcpipChannel(SshdSocketAddress local, SshdSocketAddress remote) throws IOException;
 
     /**
+     * Create an SCP client from this session.
+     */
+    ScpClient createScpClient();
+
+    /**
      * Start forwarding the given local address on the client to the given address on the server.
      */
     SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/SshServer.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/SshServer.java b/sshd-core/src/main/java/org/apache/sshd/SshServer.java
index 4edab9b..acc1ca1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/SshServer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/SshServer.java
@@ -60,6 +60,7 @@ import org.apache.sshd.common.cipher.ARCFOUR256;
 import org.apache.sshd.common.cipher.BlowfishCBC;
 import org.apache.sshd.common.cipher.TripleDESCBC;
 import org.apache.sshd.common.compression.CompressionNone;
+import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory;
 import org.apache.sshd.common.forward.DefaultForwardingAcceptorFactory;
 import org.apache.sshd.common.forward.DefaultTcpipForwarderFactory;
 import org.apache.sshd.common.forward.TcpipServerChannel;
@@ -79,7 +80,6 @@ import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.SecurityUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.server.PasswordAuthenticator;
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.apache.sshd.server.ServerFactoryManager;
@@ -90,7 +90,6 @@ import org.apache.sshd.server.auth.UserAuthPublicKey;
 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
 import org.apache.sshd.server.auth.gss.UserAuthGSS;
 import org.apache.sshd.server.channel.ChannelSession;
-import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory;
 import org.apache.sshd.server.kex.DHG1;
 import org.apache.sshd.server.kex.DHG14;
 import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
new file mode 100644
index 0000000..fc6bfb8
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
@@ -0,0 +1,23 @@
+package org.apache.sshd.client;
+
+import java.io.IOException;
+
+/**
+ */
+public interface ScpClient {
+
+    void download(String remote, String local) throws IOException;
+
+    void download(String remote, String local, boolean recursive) throws IOException;
+
+    void download(String[] remote, String local) throws Exception;
+
+    void download(String[] remote, String local, boolean recursive) throws Exception;
+
+    void upload(String remote, String local) throws IOException;
+
+    void upload(String remote, String local, boolean recursive) throws IOException;
+
+    void upload(String[] local, String remote, boolean recursive) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
index 715ae82..40015c3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
@@ -45,7 +45,9 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
     protected InputStream in;
     protected OutputStream invertedIn;
     protected OutputStream out;
+    protected InputStream invertedOut;
     protected OutputStream err;
+    protected InputStream invertedErr;
     protected Integer exitStatus;
     protected String exitSignal;
     protected int openFailureReason;
@@ -68,6 +70,10 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
         this.in = in;
     }
 
+    public InputStream getInvertedOut() {
+        return invertedOut;
+    }
+
     public OutputStream getOut() {
         return out;
     }
@@ -76,6 +82,10 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
         this.out = out;
     }
 
+    public InputStream getInvertedErr() {
+        return invertedErr;
+    }
+
     public OutputStream getErr() {
         return err;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
index fe2ef23..ee48e0d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
@@ -24,6 +24,8 @@ import java.io.InputStream;
 import org.apache.sshd.client.future.OpenFuture;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.channel.ChannelOutputStream;
+import org.apache.sshd.common.channel.ChannelPipedInputStream;
+import org.apache.sshd.common.channel.ChannelPipedOutputStream;
 import org.apache.sshd.common.util.Buffer;
 
 /**
@@ -41,8 +43,17 @@ public class ChannelSession extends AbstractClientChannel {
 
     public OpenFuture open() throws IOException {
         invertedIn = new ChannelOutputStream(this, remoteWindow, log, SshConstants.Message.SSH_MSG_CHANNEL_DATA);
-        if (out == null || err == null) {
-            throw new IllegalStateException("in, out and err streams should be set before opening channel");
+        if (out == null) {
+            ChannelPipedInputStream pis = new ChannelPipedInputStream(localWindow);
+            ChannelPipedOutputStream pos = new ChannelPipedOutputStream(pis);
+            out = pos;
+            invertedOut = pis;
+        }
+        if (err == null) {
+            ChannelPipedInputStream pis = new ChannelPipedInputStream(localWindow);
+            ChannelPipedOutputStream pos = new ChannelPipedOutputStream(pis);
+            err = pos;
+            invertedErr = pis;
         }
         return internalOpen();
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/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
new file mode 100644
index 0000000..2bd6985
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
@@ -0,0 +1,151 @@
+package org.apache.sshd.client.scp;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Arrays;
+
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.client.ScpClient;
+import org.apache.sshd.client.channel.ChannelExec;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.FileSystemView;
+import org.apache.sshd.common.file.SshFile;
+import org.apache.sshd.common.scp.ScpHelper;
+
+/**
+ */
+public class DefaultScpClient implements ScpClient {
+
+    private final ClientSession clientSession;
+
+    public DefaultScpClient(ClientSession clientSession) {
+        this.clientSession = clientSession;
+    }
+
+    public void download(String remote, String local) throws IOException {
+        download(new String[] { remote }, local, false, false);
+    }
+
+    public void download(String remote, String local, boolean recursive) throws IOException {
+        download(new String[] { remote }, local, recursive, false);
+    }
+
+    public void download(String[] remote, String local) throws IOException {
+        download(remote, local, false, true);
+    }
+
+    public void download(String[] remote, String local, boolean recursive) throws IOException {
+        download(remote, local, recursive, true);
+    }
+
+    private void download(String[] remote, String local, boolean recursive, boolean shouldBeDir) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        StringBuilder sb = new StringBuilder("scp");
+        if (recursive) {
+            sb.append(" -r");
+        }
+        sb.append(" -f");
+        for (String r : remote) {
+            r = checkNotNullAndNotEmpty(r, "Invalid argument remote: {}");
+            sb.append(" ").append(r);
+        }
+
+        FileSystemFactory factory = clientSession.getFactoryManager().getFileSystemFactory();
+        FileSystemView fs = factory.createFileSystemView(clientSession);
+        SshFile target = fs.getFile(local);
+        if (shouldBeDir) {
+            if (!target.doesExist()) {
+                throw new SshException("Target directory " + target.toString() + " does not exists");
+            }
+            if (!target.isDirectory()) {
+                throw new SshException("Target directory " + target.toString() + " is not a directory");
+            }
+        }
+
+        ChannelExec channel = clientSession.createExecChannel(sb.toString());
+        try {
+            channel.open().await();
+        } catch (InterruptedException e) {
+            throw (IOException) new InterruptedIOException().initCause(e);
+        }
+
+        ScpHelper helper = new ScpHelper(channel.getInvertedOut(), channel.getInvertedIn(), fs);
+
+        helper.receive(target, recursive, shouldBeDir);
+
+        channel.close(false);
+    }
+
+    public void upload(String remote, String local) throws IOException {
+        upload(new String[] { remote }, local, false, false);
+    }
+
+    public void upload(String remote, String local, boolean recursive) throws IOException {
+        upload(new String[] { remote }, local, recursive, false);
+    }
+
+    public void upload(String[] local, String remote) throws IOException {
+        upload(local, remote, false, true);
+    }
+
+    public void upload(String[] local, String remote, boolean recursive) throws IOException {
+        upload(local, remote, false, true);
+    }
+
+    private void upload(String[] local, String remote, boolean recursive, boolean shouldBeDir) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        StringBuilder sb = new StringBuilder("scp");
+        if (recursive) {
+            sb.append(" -r");
+        }
+        if (shouldBeDir) {
+            sb.append(" -d");
+        }
+        sb.append(" -t");
+        for (String r : local) {
+            r = checkNotNullAndNotEmpty(r, "Invalid argument remote: {}");
+            sb.append(" ").append(r);
+        }
+        ChannelExec channel = clientSession.createExecChannel(sb.toString());
+        try {
+            channel.open().await();
+        } catch (InterruptedException e) {
+            throw (IOException) new InterruptedIOException().initCause(e);
+        }
+
+        FileSystemFactory factory = clientSession.getFactoryManager().getFileSystemFactory();
+        FileSystemView fs = factory.createFileSystemView(clientSession);
+        ScpHelper helper = new ScpHelper(channel.getInvertedOut(), channel.getInvertedIn(), fs);
+        SshFile target = fs.getFile(remote);
+
+        helper.send(Arrays.asList(local), recursive);
+
+        channel.close(false);
+    }
+
+    private <T> T checkNotNull(T t, String message) {
+        if (t == null) {
+            throw new IllegalStateException(String.format(message, t));
+        }
+        return t;
+    }
+
+    private String checkNotNullAndNotEmpty(String t, String message) {
+        t = checkNotNull(t, message).trim();
+        if (t.isEmpty()) {
+            throw new IllegalArgumentException(String.format(message, t));
+        }
+        return t;
+    }
+
+    private <T> T[] checkNotNullAndNotEmpty(T[] t, String message) {
+        t = checkNotNull(t, message);
+        if (t.length == 0) {
+            throw new IllegalArgumentException(String.format(message, t));
+        }
+        return t;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index ccc21bc..10825b7 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -29,6 +29,7 @@ import org.apache.mina.core.session.IoSession;
 import org.apache.sshd.ClientChannel;
 import org.apache.sshd.ClientSession;
 import org.apache.sshd.client.ClientFactoryManager;
+import org.apache.sshd.client.ScpClient;
 import org.apache.sshd.client.ServerKeyVerifier;
 import org.apache.sshd.client.UserAuth;
 import org.apache.sshd.client.UserInteraction;
@@ -43,6 +44,7 @@ import org.apache.sshd.client.channel.ChannelSubsystem;
 import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.future.DefaultAuthFuture;
 import org.apache.sshd.client.future.OpenFuture;
+import org.apache.sshd.client.scp.DefaultScpClient;
 import org.apache.sshd.common.Channel;
 import org.apache.sshd.common.KeyExchange;
 import org.apache.sshd.common.KeyPairProvider;
@@ -250,6 +252,10 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession
         return channel;
     }
 
+    public ScpClient createScpClient() {
+        return new DefaultScpClient(this);
+    }
+
     public SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException {
         return getTcpipForwarder().startLocalPortForwarding(local, remote);
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
index 84886a6..28f5f4a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
@@ -175,8 +175,8 @@ public interface FactoryManager {
     /**
      * Retrieve the <code>FileSystemFactory</code> to be used to traverse the file system.
      *
-     * @return a valid <code>FileSystemFactory</code> object or <code>null</code> if commands
-     *         are not supported on this server
+     * @return a valid <code>FileSystemFactory</code> object or <code>null</code> if file based
+     *         interactions are not supported on this server
      */
     FileSystemFactory getFileSystemFactory();
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/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
new file mode 100644
index 0000000..17b55c1
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
@@ -0,0 +1,363 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.file.FileSystemView;
+import org.apache.sshd.common.file.SshFile;
+import org.apache.sshd.common.util.DirectoryScanner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpHelper {
+
+    protected static final Logger log = LoggerFactory.getLogger(ScpHelper.class);
+
+    public static final int OK = 0;
+    public static final int WARNING = 1;
+    public static final int ERROR = 2;
+
+    protected final FileSystemView root;
+    protected final InputStream in;
+    protected final OutputStream out;
+
+    public ScpHelper(InputStream in, OutputStream out, FileSystemView root) {
+        this.in = in;
+        this.out = out;
+        this.root = root;
+    }
+
+    public void receive(SshFile path, boolean recursive, boolean shouldBeDir) throws IOException {
+        if (shouldBeDir) {
+            if (!path.doesExist()) {
+                throw new SshException("Target directory " + path.toString() + " does not exists");
+            }
+            if (!path.isDirectory()) {
+                throw new SshException("Target directory " + path.toString() + " is not a directory");
+            }
+        }
+        ack();
+        for (;;)
+        {
+            String line;
+            boolean isDir = false;
+            int c = readAck(true);
+            switch (c)
+            {
+                case -1:
+                    return;
+                case 'D':
+                    isDir = true;
+                case 'C':
+                    line = ((char) c) + readLine();
+                    break;
+                case 'T':
+                    readLine();
+                    ack();
+                    continue;
+                case 'E':
+                    readLine();
+                    return;
+                default:
+                    //a real ack that has been acted upon already
+                    continue;
+            }
+
+            if (recursive && isDir)
+            {
+                receiveDir(line, path);
+            }
+            else
+            {
+                receiveFile(line, path);
+            }
+        }
+    }
+
+
+    public void receiveDir(String header, SshFile path) throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("Writing dir {}", path);
+        }
+        if (!header.startsWith("D")) {
+            throw new IOException("Expected a D message but got '" + header + "'");
+        }
+
+        String perms = 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 but got " + length);
+        }
+        SshFile file;
+        if (path.doesExist() && path.isDirectory()) {
+            file = root.getFile(path, name);
+        } else if (!path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory()) {
+            file = path;
+        } else {
+            throw new IOException("Can not write to " + path);
+        }
+        if (!(file.doesExist() && file.isDirectory()) && !file.mkdir()) {
+            throw new IOException("Could not create directory " + file);
+        }
+
+        ack();
+
+        for (;;) {
+            header = readLine();
+            if (header.startsWith("C")) {
+                receiveFile(header, file);
+            } else if (header.startsWith("D")) {
+                receiveDir(header, file);
+            } else if (header.equals("E")) {
+                ack();
+                break;
+            } else if (header.equals("T")) {
+                ack();
+                break;
+            } else {
+                throw new IOException("Unexpected message: '" + header + "'");
+            }
+        }
+
+    }
+
+    public void receiveFile(String header, SshFile path) throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("Writing file {}", path);
+        }
+        if (!header.startsWith("C")) {
+            throw new IOException("Expected a C message but got '" + header + "'");
+        }
+
+        String perms = header.substring(1, 5);
+        long length = Long.parseLong(header.substring(6, header.indexOf(' ', 6)));
+        String name = header.substring(header.indexOf(' ', 6) + 1);
+
+        SshFile file;
+        if (path.doesExist() && path.isDirectory()) {
+            file = root.getFile(path, name);
+        } else if (path.doesExist() && path.isFile()) {
+            file = path;
+        } else if (!path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory()) {
+            file = path;
+        } else {
+            throw new IOException("Can not write to " + path);
+        }
+        if (file.doesExist() && file.isDirectory()) {
+            throw new IOException("File is a directory: " + file);
+        } else if (file.doesExist() && !file.isWritable()) {
+            throw new IOException("Can not write to file: " + file);
+        }
+        OutputStream os = file.createOutputStream(0);
+        try {
+            ack();
+
+            byte[] buffer = new byte[8192];
+            while (length > 0) {
+                int len = (int) Math.min(length, buffer.length);
+                len = in.read(buffer, 0, len);
+                if (len <= 0) {
+                    throw new IOException("End of stream reached");
+                }
+                os.write(buffer, 0, len);
+                length -= len;
+            }
+        } finally {
+            os.close();
+        }
+
+        ack();
+        readAck(false);
+    }
+
+    public String readLine() throws IOException {
+        return readLine(false);
+    }
+
+    public String readLine(boolean canEof) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        for (;;) {
+            int c = in.read();
+            if (c == '\n') {
+                return baos.toString();
+            } else if (c == -1) {
+                if (!canEof) {
+                    throw new EOFException();
+                }
+                return null;
+            } else {
+                baos.write(c);
+            }
+        }
+    }
+
+    public void send(List<String> paths, boolean recursive) throws IOException {
+        for (String pattern : paths) {
+            int idx = pattern.indexOf('*');
+            if (idx >= 0) {
+                String basedir = "";
+                int lastSep = pattern.substring(0, idx).lastIndexOf('/');
+                if (lastSep >= 0) {
+                    basedir = pattern.substring(0, lastSep);
+                    pattern = pattern.substring(lastSep + 1);
+                }
+                String[] included = new DirectoryScanner(basedir, pattern).scan();
+                for (String path : included) {
+                    SshFile file = root.getFile(basedir + "/" + path);
+                    if (file.isFile()) {
+                        sendFile(file);
+                    } else if (file.isDirectory()) {
+                        if (!recursive) {
+                            out.write(ScpHelper.WARNING);
+                            out.write((path + " not a regular file\n").getBytes());
+                        } else {
+                            sendDir(file);
+                        }
+                    } else {
+                        out.write(ScpHelper.WARNING);
+                        out.write((path + " unknown file type\n").getBytes());
+                    }
+                }
+            } else {
+                String basedir = "";
+                int lastSep = pattern.lastIndexOf('/');
+                if (lastSep >= 0) {
+                    basedir = pattern.substring(0, lastSep);
+                    pattern = pattern.substring(lastSep + 1);
+                }
+                SshFile file = root.getFile(basedir + "/" + pattern);
+                if (!file.doesExist()) {
+                    throw new IOException(file + ": no such file or directory");
+                }
+                if (file.isFile()) {
+                    sendFile(file);
+                } else if (file.isDirectory()) {
+                    if (!recursive) {
+                        throw new IOException(file + " not a regular file");
+                    } else {
+                        sendDir(file);
+                    }
+                } else {
+                    throw new IOException(file + ": unknown file type");
+                }
+            }
+        }
+    }
+
+    public void sendFile(SshFile path) throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("Reading file {}", path);
+        }
+        StringBuffer buf = new StringBuffer();
+        buf.append("C");
+        buf.append("0644"); // TODO: what about perms
+        buf.append(" ");
+        buf.append(path.getSize()); // length
+        buf.append(" ");
+        buf.append(path.getName());
+        buf.append("\n");
+        out.write(buf.toString().getBytes());
+        out.flush();
+        readAck(false);
+
+        InputStream is = path.createInputStream(0);
+        try {
+            byte[] buffer = new byte[8192];
+            for (;;) {
+                int len = is.read(buffer, 0, buffer.length);
+                if (len == -1) {
+                    break;
+                }
+                out.write(buffer, 0, len);
+            }
+        } finally {
+            is.close();
+        }
+        ack();
+        readAck(false);
+    }
+
+    public void sendDir(SshFile path) throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("Reading directory {}", path);
+        }
+        StringBuffer buf = new StringBuffer();
+        buf.append("D");
+        buf.append("0755"); // what about perms
+        buf.append(" ");
+        buf.append("0"); // length
+        buf.append(" ");
+        buf.append(path.getName());
+        buf.append("\n");
+        out.write(buf.toString().getBytes());
+        out.flush();
+        readAck(false);
+
+        for (SshFile child : path.listSshFiles()) {
+            if (child.isFile()) {
+                sendFile(child);
+            } else if (child.isDirectory()) {
+                sendDir(child);
+            }
+        }
+
+        out.write("E\n".getBytes());
+        out.flush();
+        readAck(false);
+    }
+
+    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 (!canEof) {
+                    throw new EOFException();
+                }
+                break;
+            case OK:
+                break;
+            case WARNING:
+                log.warn("Received warning: " + readLine());
+                break;
+            case ERROR:
+                throw new IOException("Received nack: " + readLine());
+            default:
+                break;
+        }
+        return c;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/16fc6a80/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
index 950d569..8ecd7cd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
@@ -18,20 +18,19 @@
  */
 package org.apache.sshd.server.command;
 
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
-import org.apache.sshd.common.util.DirectoryScanner;
+import org.apache.sshd.common.file.FileSystemAware;
+import org.apache.sshd.common.file.FileSystemView;
+import org.apache.sshd.common.scp.ScpHelper;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.common.file.FileSystemAware;
-import org.apache.sshd.common.file.FileSystemView;
-import org.apache.sshd.common.file.SshFile;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,19 +44,15 @@ import org.slf4j.LoggerFactory;
 public class ScpCommand implements Command, Runnable, FileSystemAware {
 
     protected static final Logger log = LoggerFactory.getLogger(ScpCommand.class);
-    protected static final int OK = 0;
-    protected static final int WARNING = 1;
-    protected static final int ERROR = 2;
 
     protected String name;
     protected boolean optR;
     protected boolean optT;
     protected boolean optF;
-    protected boolean optV;
     protected boolean optD;
-    protected boolean optP;
+    protected boolean optP; // TODO: handle modification times
     protected FileSystemView root;
-    protected String path;
+    protected List<String> paths;
     protected InputStream in;
     protected OutputStream out;
     protected OutputStream err;
@@ -69,7 +64,7 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
         if (log.isDebugEnabled()) {
             log.debug("Executing command {}", name);
         }
-        path = ".";
+        paths = new ArrayList<String>();
         for (int i = 1; i < args.length; i++) {
             if (args[i].charAt(0) == '-') {
                 for (int j = 1; j < args[i].length(); j++) {
@@ -86,9 +81,6 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
                         case 't':
                             optT = true;
                             break;
-                        case 'v':
-                            optV = true;
-                            break;
                         case 'd':
                             optD = true;
                             break;
@@ -98,12 +90,15 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
                     }
                 }
             } else if (i == args.length - 1) {
-                path = args[args.length - 1];
+                paths.add(args[args.length - 1]);
             }
         }
         if (!optF && !optT) {
             error = new IOException("Either -f or -t option should be set");
         }
+        if (optT && paths.size() != 1) {
+            error = new IOException("One and only one path must be given with -t option");
+        }
     }
 
     public void setInputStream(InputStream in) {
@@ -122,6 +117,10 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
         this.callback = callback;
     }
 
+    public void setFileSystemView(FileSystemView view) {
+        this.root = view;
+    }
+
     public void start(Environment env) throws IOException {
         if (error != null) {
             throw error;
@@ -133,104 +132,20 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
     }
 
     public void run() {
-        int exitValue = OK;
+        int exitValue = ScpHelper.OK;
         String exitMessage = null;
-        
+        ScpHelper helper = new ScpHelper(in, out, root);
         try {
-            if (optT)
-            {
-                ack();
-                for (; ;)
-                {
-                    String line;
-                    boolean isDir = false;
-                    int c = readAck(true);
-                    switch (c)
-                    {
-                        case -1:
-                            return;
-                        case 'D':
-                            isDir = true;
-                        case 'C':
-                            line = ((char) c) + readLine();
-                            break;
-                        case 'T':
-                            readLine();
-                            ack();
-                            continue;
-                        case 'E':
-                            readLine();
-                            return;
-                        default:
-                            //a real ack that has been acted upon already
-                            continue;
-                    }
-
-                    if (optR && isDir)
-                    {
-                        writeDir(line, root.getFile(path));
-                    }
-                    else
-                    {
-                        writeFile(line, root.getFile(path));
-                    }
-                }
+            if (optT) {
+                helper.receive(root.getFile(paths.get(0)), optR, optD);
             } else if (optF) {
-                String pattern = path;
-                int idx = pattern.indexOf('*');
-                if (idx >= 0) {
-                    String basedir = "";
-                    int lastSep = pattern.substring(0, idx).lastIndexOf('/');
-                    if (lastSep >= 0) {
-                        basedir = pattern.substring(0, lastSep);
-                        pattern = pattern.substring(lastSep + 1);
-                    }
-                    String[] included = new DirectoryScanner(basedir, pattern).scan();
-                    for (String path : included) {
-                        SshFile file = root.getFile(basedir + "/" + path);
-                        if (file.isFile()) {
-                            readFile(file);
-                        } else if (file.isDirectory()) {
-                            if (!optR) {
-                                out.write(WARNING);
-                                out.write((path + " not a regular file\n").getBytes());
-                            } else {
-                                readDir(file);
-                            }
-                        } else {
-                            out.write(WARNING);
-                            out.write((path + " unknown file type\n").getBytes());
-                        }
-                    }
-                } else {
-                    String basedir = "";
-                    int lastSep = pattern.lastIndexOf('/');
-                    if (lastSep >= 0) {
-                        basedir = pattern.substring(0, lastSep);
-                        pattern = pattern.substring(lastSep + 1);
-                    }
-                    SshFile file = root.getFile(basedir + "/" + pattern);
-                    if (!file.doesExist()) {
-                        throw new IOException(file + ": no such file or directory");
-                    }
-                    if (file.isFile()) {
-                        readFile(file);
-                    } else if (file.isDirectory()) {
-                        if (!optR) {
-                            throw new IOException(file + " not a regular file");
-                        } else {
-                            readDir(file);
-                        }
-                    } else {
-                        throw new IOException(file + ": unknown file type");
-                    }
-                }
+                helper.send(paths, optR);
             } else {
                 throw new IOException("Unsupported mode");
             }
         } catch (IOException e) {
             try {
-                exitValue = ERROR;
+                exitValue = ScpHelper.ERROR;
                 exitMessage = e.getMessage() == null ? "" : e.getMessage();
                 out.write(exitValue);
                 out.write(exitMessage.getBytes());
@@ -247,207 +162,5 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
         }
     }
 
-    protected void writeDir(String header, SshFile path) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("Writing dir {}", path);
-        }
-        if (!header.startsWith("D")) {
-            throw new IOException("Expected a D message but got '" + header + "'");
-        }
-
-        String perms = 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 but got " + length);
-        }
-        SshFile file;
-        if (path.doesExist() && path.isDirectory()) {
-            file = root.getFile(path, name);
-        } else if (!path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory()) {
-            file = path;
-        } else {
-            throw new IOException("Can not write to " + path);
-        }
-        if (!(file.doesExist() && file.isDirectory()) && !file.mkdir()) {
-            throw new IOException("Could not create directory " + file);
-        }
-
-        ack();
-
-        for (;;) {
-            header = readLine();
-            if (header.startsWith("C")) {
-                writeFile(header, file);
-            } else if (header.startsWith("D")) {
-                writeDir(header, file);
-            } else if (header.equals("E")) {
-                ack();
-                break;
-            } else if (header.equals("T")) {
-                ack();
-                break;
-            } else {
-                throw new IOException("Unexpected message: '" + header + "'");
-            }
-        }
-
-    }
-
-    protected void writeFile(String header, SshFile path) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("Writing file {}", path);
-        }
-        if (!header.startsWith("C")) {
-            throw new IOException("Expected a C message but got '" + header + "'");
-        }
-
-        String perms = header.substring(1, 5);
-        long length = Long.parseLong(header.substring(6, header.indexOf(' ', 6)));
-        String name = header.substring(header.indexOf(' ', 6) + 1);
-
-        SshFile file;
-        if (path.doesExist() && path.isDirectory()) {
-            file = root.getFile(path, name);
-        } else if (path.doesExist() && path.isFile()) {
-            file = path;
-        } else if (!path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory()) {
-            file = path;
-        } else {
-            throw new IOException("Can not write to " + path);
-        }
-        if (file.doesExist() && file.isDirectory()) {
-            throw new IOException("File is a directory: " + file);
-        } else if (file.doesExist() && !file.isWritable()) {
-            throw new IOException("Can not write to file: " + file);
-        }
-        OutputStream os = file.createOutputStream(0);
-        try {
-            ack();
-
-            byte[] buffer = new byte[8192];
-            while (length > 0) {
-                int len = (int) Math.min(length, buffer.length);
-                len = in.read(buffer, 0, len);
-                if (len <= 0) {
-                    throw new IOException("End of stream reached");
-                }
-                os.write(buffer, 0, len);
-                length -= len;
-            }
-        } finally {
-            os.close();
-        }
-
-        ack();
-        readAck(false);
-    }
-
-    protected String readLine() throws IOException {
-        ByteArrayOutputStream 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);
-            }
-        }
-    }
-
-    protected void readFile(SshFile path) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("Reading file {}", path);
-        }
-        StringBuffer buf = new StringBuffer();
-        buf.append("C");
-        buf.append("0644"); // what about perms
-        buf.append(" ");
-        buf.append(path.getSize()); // length
-        buf.append(" ");
-        buf.append(path.getName());
-        buf.append("\n");
-        out.write(buf.toString().getBytes());
-        out.flush();
-        readAck(false);
-
-        InputStream is = path.createInputStream(0);
-        try {
-            byte[] buffer = new byte[8192];
-            for (;;) {
-                int len = is.read(buffer, 0, buffer.length);
-                if (len == -1) {
-                    break;
-                }
-                out.write(buffer, 0, len);
-            }
-        } finally {
-            is.close();
-        }
-        ack();
-        readAck(false);
-    }
-
-    protected void readDir(SshFile path) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("Reading directory {}", path);
-        }
-        StringBuffer buf = new StringBuffer();
-        buf.append("D");
-        buf.append("0755"); // what about perms
-        buf.append(" ");
-        buf.append("0"); // length
-        buf.append(" ");
-        buf.append(path.getName());
-        buf.append("\n");
-        out.write(buf.toString().getBytes());
-        out.flush();
-        readAck(false);
-
-        for (SshFile child : path.listSshFiles()) {
-            if (child.isFile()) {
-                readFile(child);
-            } else if (child.isDirectory()) {
-                readDir(child);
-            }
-        }
-
-        out.write("E\n".getBytes());
-        out.flush();
-        readAck(false);
-    }
-
-    protected void ack() throws IOException {
-        out.write(0);
-        out.flush();
-    }
-
-    protected int readAck(boolean canEof) throws IOException {
-        int c = in.read();
-        switch (c) {
-            case -1:
-                if (!canEof) {
-                    throw new EOFException();
-                }
-                break;
-            case OK:
-                break;
-            case WARNING:
-                log.warn("Received warning: " + readLine());
-                break;
-            case ERROR:
-                throw new IOException("Received nack: " + readLine());
-            default:
-                break;
-        }
-        return c;
-    }
-
-    public void setFileSystemView(FileSystemView view) {
-        this.root = view;
-    }
 
 }