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/26 10:37:27 UTC

[2/3] git commit: [SSHD-90] Fix some problems with SCP client Support for attributes preservation on copy

[SSHD-90] Fix some problems with SCP client
Support for attributes preservation on copy

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

Branch: refs/heads/master
Commit: 725bcd9f2fc7c0027eaec7c30e439b375f088254
Parents: f31ac81
Author: Guillaume Nodet <gn...@apache.org>
Authored: Thu Jul 25 23:49:52 2013 +0200
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Thu Jul 25 23:49:52 2013 +0200

----------------------------------------------------------------------
 .../java/org/apache/sshd/client/ScpClient.java  |  18 +-
 .../client/channel/AbstractClientChannel.java   |   4 +
 .../sshd/client/scp/DefaultScpClient.java       | 105 +++++---
 .../common/channel/ChannelPipedInputStream.java |   2 +-
 .../org/apache/sshd/common/scp/ScpHelper.java   | 189 +++++++++++--
 .../apache/sshd/server/command/ScpCommand.java  |  25 +-
 .../sshd/server/command/ScpCommandFactory.java  |  32 +--
 .../src/test/java/org/apache/sshd/ScpTest.java  | 263 +++++++++++++++++++
 8 files changed, 518 insertions(+), 120 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/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
index d2bdaa8..381a210 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
@@ -24,18 +24,18 @@ import java.io.IOException;
  */
 public interface ScpClient {
 
-    void download(String remote, String local) throws IOException;
+    enum Option {
+        Recursive,
+        PreserveAttributes,
+        TargetIsDirectory
+    }
 
-    void download(String remote, String local, boolean recursive) throws IOException;
+    void download(String remote, String local, Option... options) throws IOException;
 
-    void download(String[] remote, String local) throws Exception;
+    void download(String[] remote, String local, Option... options) throws Exception;
 
-    void download(String[] remote, String local, boolean recursive) throws Exception;
+    void upload(String local, String remote, Option... options) throws IOException;
 
-    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;
+    void upload(String[] local, String remote, Option... options) throws IOException;
 
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/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 8d04dba..b786c39 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
@@ -227,6 +227,10 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
     }
 
     protected void doWriteData(byte[] data, int off, int len) throws IOException {
+        // If we're already closing, ignore incoming data
+        if (closing.get()) {
+            return;
+        }
         if (out != null) {
             out.write(data, off, len);
             out.flush();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/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 1b590c2..911eb5c 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
@@ -20,7 +20,11 @@ package org.apache.sshd.client.scp;
 
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 import org.apache.sshd.ClientSession;
 import org.apache.sshd.client.ScpClient;
@@ -42,39 +46,44 @@ public class DefaultScpClient implements ScpClient {
         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, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        download(remote, local, Arrays.asList(options));
     }
 
-    public void download(String[] remote, String local, boolean recursive) throws IOException {
-        download(remote, local, recursive, true);
+    public void download(String[] remote, String local, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        List<Option> opts = options(options);
+        if (remote.length > 1) {
+            opts.add(Option.TargetIsDirectory);
+        }
+        for (String r : remote) {
+            download(r, local, opts);
+        }
     }
 
-    private void download(String[] remote, String local, boolean recursive, boolean shouldBeDir) throws IOException {
+    protected void download(String remote, String local, Collection<Option> options) throws IOException {
         local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
         remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+
         StringBuilder sb = new StringBuilder("scp");
-        if (recursive) {
+        if (options.contains(Option.Recursive)) {
             sb.append(" -r");
         }
-        sb.append(" -f");
-        for (String r : remote) {
-            r = checkNotNullAndNotEmpty(r, "Invalid argument remote: {}");
-            sb.append(" ").append(r);
+        if (options.contains(Option.PreserveAttributes)) {
+            sb.append(" -p");
         }
+        sb.append(" -f");
+        sb.append(" --");
+        sb.append(" ");
+        sb.append(remote);
 
         FileSystemFactory factory = clientSession.getFactoryManager().getFileSystemFactory();
         FileSystemView fs = factory.createFileSystemView(clientSession);
         SshFile target = fs.getFile(local);
-        if (shouldBeDir) {
+        if (options.contains(Option.TargetIsDirectory)) {
             if (!target.doesExist()) {
                 throw new SshException("Target directory " + target.toString() + " does not exists");
             }
@@ -92,42 +101,47 @@ public class DefaultScpClient implements ScpClient {
 
         ScpHelper helper = new ScpHelper(channel.getInvertedOut(), channel.getInvertedIn(), fs);
 
-        helper.receive(target, recursive, shouldBeDir);
+        helper.receive(target,
+                       options.contains(Option.Recursive),
+                       options.contains(Option.TargetIsDirectory),
+                       options.contains(Option.PreserveAttributes));
 
         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, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        upload(new String[] { local }, remote, options(options));
     }
 
-    public void upload(String[] local, String remote, boolean recursive) throws IOException {
-        upload(local, remote, false, true);
+    public void upload(String[] local, String remote, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        List<Option> opts = options(options);
+        if (local.length > 1) {
+            opts.add(Option.TargetIsDirectory);
+        }
+        upload(local, remote, opts);
     }
 
-    private void upload(String[] local, String remote, boolean recursive, boolean shouldBeDir) throws IOException {
+    protected void upload(String[] local, String remote, Collection<Option> options) throws IOException {
         local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
         remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
         StringBuilder sb = new StringBuilder("scp");
-        if (recursive) {
+        if (options.contains(Option.Recursive)) {
             sb.append(" -r");
         }
-        if (shouldBeDir) {
+        if (options.contains(Option.TargetIsDirectory)) {
             sb.append(" -d");
         }
-        sb.append(" -t");
-        for (String r : local) {
-            r = checkNotNullAndNotEmpty(r, "Invalid argument remote: {}");
-            sb.append(" ").append(r);
+        if (options.contains(Option.PreserveAttributes)) {
+            sb.append(" -p");
         }
+        sb.append(" -t");
+        sb.append(" --");
+        sb.append(" ");
+        sb.append(remote);
         ChannelExec channel = clientSession.createExecChannel(sb.toString());
         try {
             channel.open().await();
@@ -138,13 +152,22 @@ public class DefaultScpClient implements ScpClient {
         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);
+        helper.send(Arrays.asList(local),
+                    options.contains(Option.Recursive),
+                    options.contains(Option.PreserveAttributes));
 
         channel.close(false);
     }
 
+    private List<Option> options(Option... options) {
+        List<Option> opts = new ArrayList<Option>();
+        if (options != null) {
+            opts.addAll(Arrays.asList(options));
+        }
+        return opts;
+    }
+
     private <T> T checkNotNull(T t, String message) {
         if (t == null) {
             throw new IllegalStateException(String.format(message, t));

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
index e68d074..1d7b489 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
@@ -96,7 +96,7 @@ public class ChannelPipedInputStream extends InputStream {
         lock.lock();
         try {
             for (;;) {
-                if (closed) {
+                if (closed && !writerClosed) {
                     throw new IOException("Pipe closed");
                 }
                 if (buffer.available() > 0) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/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 17b55c1..b9198f3 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
@@ -23,7 +23,10 @@ import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.file.FileSystemView;
@@ -43,6 +46,16 @@ public class ScpHelper {
     public static final int WARNING = 1;
     public static final int ERROR = 2;
 
+    public static final int S_IRUSR =  0000400;
+    public static final int S_IWUSR =  0000200;
+    public static final int S_IXUSR =  0000100;
+    public static final int S_IRGRP =  0000040;
+    public static final int S_IWGRP =  0000020;
+    public static final int S_IXGRP =  0000010;
+    public static final int S_IROTH =  0000004;
+    public static final int S_IWOTH =  0000002;
+    public static final int S_IXOTH =  0000001;
+
     protected final FileSystemView root;
     protected final InputStream in;
     protected final OutputStream out;
@@ -53,7 +66,7 @@ public class ScpHelper {
         this.root = root;
     }
 
-    public void receive(SshFile path, boolean recursive, boolean shouldBeDir) throws IOException {
+    public void receive(SshFile path, boolean recursive, boolean shouldBeDir, boolean preserve) throws IOException {
         if (shouldBeDir) {
             if (!path.doesExist()) {
                 throw new SshException("Target directory " + path.toString() + " does not exists");
@@ -63,6 +76,7 @@ public class ScpHelper {
             }
         }
         ack();
+        long[] time = null;
         for (;;)
         {
             String line;
@@ -76,13 +90,17 @@ public class ScpHelper {
                     isDir = true;
                 case 'C':
                     line = ((char) c) + readLine();
+                    log.debug("Received header: " + line);
                     break;
                 case 'T':
-                    readLine();
+                    line = ((char) c) + readLine();
+                    log.debug("Received header: " + line);
+                    time = parseTime(line);
                     ack();
                     continue;
                 case 'E':
-                    readLine();
+                    line = ((char) c) + readLine();
+                    log.debug("Received header: " + line);
                     return;
                 default:
                     //a real ack that has been acted upon already
@@ -91,19 +109,21 @@ public class ScpHelper {
 
             if (recursive && isDir)
             {
-                receiveDir(line, path);
+                receiveDir(line, path, time, preserve);
+                time = null;
             }
             else
             {
-                receiveFile(line, path);
+                receiveFile(line, path, time, preserve);
+                time = null;
             }
         }
     }
 
 
-    public void receiveDir(String header, SshFile path) throws IOException {
+    public void receiveDir(String header, SshFile path, long[] time, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Writing dir {}", path);
+            log.debug("Receiving directory {}", path);
         }
         if (!header.startsWith("D")) {
             throw new IOException("Expected a D message but got '" + header + "'");
@@ -128,20 +148,34 @@ public class ScpHelper {
             throw new IOException("Could not create directory " + file);
         }
 
+        if (preserve) {
+            Map<SshFile.Attribute, Object> attrs = new HashMap<SshFile.Attribute, Object>();
+            attrs.put(SshFile.Attribute.Permissions, fromOctalPerms(perms));
+            if (time != null) {
+                attrs.put(SshFile.Attribute.LastModifiedTime, time[0]);
+                attrs.put(SshFile.Attribute.LastAccessTime, time[1]);
+            }
+            file.setAttributes(attrs);
+        }
+
         ack();
 
+        time = null;
         for (;;) {
             header = readLine();
+            log.debug("Received header: " + header);
             if (header.startsWith("C")) {
-                receiveFile(header, file);
+                receiveFile(header, file, time, preserve);
+                time = null;
             } else if (header.startsWith("D")) {
-                receiveDir(header, file);
+                receiveDir(header, file, time, preserve);
+                time = null;
             } else if (header.equals("E")) {
                 ack();
                 break;
-            } else if (header.equals("T")) {
+            } else if (header.startsWith("T")) {
+                time = parseTime(header);
                 ack();
-                break;
             } else {
                 throw new IOException("Unexpected message: '" + header + "'");
             }
@@ -149,9 +183,9 @@ public class ScpHelper {
 
     }
 
-    public void receiveFile(String header, SshFile path) throws IOException {
+    public void receiveFile(String header, SshFile path, long[] time, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Writing file {}", path);
+            log.debug("Receiving file {}", path);
         }
         if (!header.startsWith("C")) {
             throw new IOException("Expected a C message but got '" + header + "'");
@@ -194,6 +228,16 @@ public class ScpHelper {
             os.close();
         }
 
+        if (preserve) {
+            Map<SshFile.Attribute, Object> attrs = new HashMap<SshFile.Attribute, Object>();
+            attrs.put(SshFile.Attribute.Permissions, fromOctalPerms(perms));
+            if (time != null) {
+                attrs.put(SshFile.Attribute.LastModifiedTime, time[0]);
+                attrs.put(SshFile.Attribute.LastAccessTime, time[1]);
+            }
+            file.setAttributes(attrs);
+        }
+
         ack();
         readAck(false);
     }
@@ -219,7 +263,7 @@ public class ScpHelper {
         }
     }
 
-    public void send(List<String> paths, boolean recursive) throws IOException {
+    public void send(List<String> paths, boolean recursive, boolean preserve) throws IOException {
         for (String pattern : paths) {
             int idx = pattern.indexOf('*');
             if (idx >= 0) {
@@ -233,13 +277,13 @@ public class ScpHelper {
                 for (String path : included) {
                     SshFile file = root.getFile(basedir + "/" + path);
                     if (file.isFile()) {
-                        sendFile(file);
+                        sendFile(file, preserve);
                     } else if (file.isDirectory()) {
                         if (!recursive) {
                             out.write(ScpHelper.WARNING);
                             out.write((path + " not a regular file\n").getBytes());
                         } else {
-                            sendDir(file);
+                            sendDir(file, preserve);
                         }
                     } else {
                         out.write(ScpHelper.WARNING);
@@ -258,12 +302,12 @@ public class ScpHelper {
                     throw new IOException(file + ": no such file or directory");
                 }
                 if (file.isFile()) {
-                    sendFile(file);
+                    sendFile(file, preserve);
                 } else if (file.isDirectory()) {
                     if (!recursive) {
                         throw new IOException(file + " not a regular file");
                     } else {
-                        sendDir(file);
+                        sendDir(file, preserve);
                     }
                 } else {
                     throw new IOException(file + ": unknown file type");
@@ -272,15 +316,33 @@ public class ScpHelper {
         }
     }
 
-    public void sendFile(SshFile path) throws IOException {
+    public void sendFile(SshFile path, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Reading file {}", path);
+            log.debug("Sending file {}", path);
+        }
+
+        Map<SshFile.Attribute,Object> attrs =  path.getAttributes(true);
+        if (preserve) {
+            StringBuffer buf = new StringBuffer();
+            buf.append("T");
+            buf.append(attrs.get(SshFile.Attribute.LastModifiedTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append(" ");
+            buf.append(attrs.get(SshFile.Attribute.LastAccessTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append("\n");
+            out.write(buf.toString().getBytes());
+            out.flush();
+            readAck(false);
         }
+
         StringBuffer buf = new StringBuffer();
         buf.append("C");
-        buf.append("0644"); // TODO: what about perms
+        buf.append(preserve ? toOctalPerms((EnumSet<SshFile.Permission>) attrs.get(SshFile.Attribute.Permissions)) : "0644");
         buf.append(" ");
-        buf.append(path.getSize()); // length
+        buf.append(attrs.get(SshFile.Attribute.Size)); // length
         buf.append(" ");
         buf.append(path.getName());
         buf.append("\n");
@@ -305,13 +367,30 @@ public class ScpHelper {
         readAck(false);
     }
 
-    public void sendDir(SshFile path) throws IOException {
+    public void sendDir(SshFile path, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Reading directory {}", path);
+            log.debug("Sending directory {}", path);
+        }
+        Map<SshFile.Attribute,Object> attrs =  path.getAttributes(true);
+        if (preserve) {
+            StringBuffer buf = new StringBuffer();
+            buf.append("T");
+            buf.append(attrs.get(SshFile.Attribute.LastModifiedTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append(" ");
+            buf.append(attrs.get(SshFile.Attribute.LastAccessTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append("\n");
+            out.write(buf.toString().getBytes());
+            out.flush();
+            readAck(false);
         }
+
         StringBuffer buf = new StringBuffer();
         buf.append("D");
-        buf.append("0755"); // what about perms
+        buf.append(preserve ? toOctalPerms((EnumSet<SshFile.Permission>) attrs.get(SshFile.Attribute.Permissions)) : "0755");
         buf.append(" ");
         buf.append("0"); // length
         buf.append(" ");
@@ -323,9 +402,9 @@ public class ScpHelper {
 
         for (SshFile child : path.listSshFiles()) {
             if (child.isFile()) {
-                sendFile(child);
+                sendFile(child, preserve);
             } else if (child.isDirectory()) {
-                sendDir(child);
+                sendDir(child, preserve);
             }
         }
 
@@ -334,6 +413,62 @@ public class ScpHelper {
         readAck(false);
     }
 
+    private long[] parseTime(String line) {
+        String[] numbers = line.substring(1).split(" ");
+        return new long[] { Long.parseLong(numbers[0]), Long.parseLong(numbers[2]) };
+    }
+
+    public static String toOctalPerms(EnumSet<SshFile.Permission> perms) {
+        int pf = 0;
+        for (SshFile.Permission p : perms) {
+            switch (p) {
+                case UserRead:      pf |= S_IRUSR; break;
+                case UserWrite:     pf |= S_IWUSR; break;
+                case UserExecute:   pf |= S_IXUSR; break;
+                case GroupRead:     pf |= S_IRGRP; break;
+                case GroupWrite:    pf |= S_IWGRP; break;
+                case GroupExecute:  pf |= S_IXGRP; break;
+                case OthersRead:    pf |= S_IROTH; break;
+                case OthersWrite:   pf |= S_IWOTH; break;
+                case OthersExecute: pf |= S_IXOTH; break;
+            }
+        }
+        return String.format("%04o", pf);
+    }
+
+    public static EnumSet<SshFile.Permission> fromOctalPerms(String str) {
+        int perms = Integer.parseInt(str, 8);
+        EnumSet<SshFile.Permission> p = EnumSet.noneOf(SshFile.Permission.class);
+        if ((perms & S_IRUSR) != 0) {
+            p.add(SshFile.Permission.UserRead);
+        }
+        if ((perms & S_IWUSR) != 0) {
+            p.add(SshFile.Permission.UserWrite);
+        }
+        if ((perms & S_IXUSR) != 0) {
+            p.add(SshFile.Permission.UserExecute);
+        }
+        if ((perms & S_IRGRP) != 0) {
+            p.add(SshFile.Permission.GroupRead);
+        }
+        if ((perms & S_IWGRP) != 0) {
+            p.add(SshFile.Permission.GroupWrite);
+        }
+        if ((perms & S_IXGRP) != 0) {
+            p.add(SshFile.Permission.GroupExecute);
+        }
+        if ((perms & S_IROTH) != 0) {
+            p.add(SshFile.Permission.OthersRead);
+        }
+        if ((perms & S_IWOTH) != 0) {
+            p.add(SshFile.Permission.OthersWrite);
+        }
+        if ((perms & S_IXOTH) != 0) {
+            p.add(SshFile.Permission.OthersExecute);
+        }
+        return p;
+    }
+
     public void ack() throws IOException {
         out.write(0);
         out.flush();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/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 8ecd7cd..d67715c 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
@@ -23,6 +23,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.sshd.common.file.FileSystemAware;
@@ -52,19 +53,17 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
     protected boolean optD;
     protected boolean optP; // TODO: handle modification times
     protected FileSystemView root;
-    protected List<String> paths;
+    protected String path;
     protected InputStream in;
     protected OutputStream out;
     protected OutputStream err;
     protected ExitCallback callback;
     protected IOException error;
 
-    public ScpCommand(String[] args) {
-        name = Arrays.asList(args).toString();
-        if (log.isDebugEnabled()) {
-            log.debug("Executing command {}", name);
-        }
-        paths = new ArrayList<String>();
+    public ScpCommand(String command) {
+        this.name = command;
+        log.debug("Executing command {}", command);
+        String[] args = command.split(" ");
         for (int i = 1; i < args.length; i++) {
             if (args[i].charAt(0) == '-') {
                 for (int j = 1; j < args[i].length(); j++) {
@@ -89,16 +88,14 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
 //                            return;
                     }
                 }
-            } else if (i == args.length - 1) {
-                paths.add(args[args.length - 1]);
+            } else {
+                path = command.substring(command.indexOf(args[i-1]) + args[i-1].length() + 1);
+                break;
             }
         }
         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) {
@@ -137,9 +134,9 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
         ScpHelper helper = new ScpHelper(in, out, root);
         try {
             if (optT) {
-                helper.receive(root.getFile(paths.get(0)), optR, optD);
+                helper.receive(root.getFile(path), optR, optD, optP);
             } else if (optF) {
-                helper.send(paths, optR);
+                helper.send(Collections.singletonList(path), optR, optP);
             } else {
                 throw new IOException("Unsupported mode");
             }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
index 61bded0..51eee6c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
@@ -55,7 +55,10 @@ public class ScpCommandFactory implements CommandFactory {
      */
     public Command createCommand(String command) {
         try {
-            return new ScpCommand(splitCommandString(command));
+            if (!command.startsWith("scp")) {
+                throw new IllegalArgumentException("Unknown command, does not begin with 'scp'");
+            }
+            return new ScpCommand(command);
         } catch (IllegalArgumentException iae) {
             if (delegate != null) {
                 return delegate.createCommand(command);
@@ -64,31 +67,4 @@ public class ScpCommandFactory implements CommandFactory {
         }
     }
 
-    private String[] splitCommandString(String command) {
-        if (!command.trim().startsWith("scp")) {
-            throw new IllegalArgumentException("Unknown command, does not begin with 'scp'");
-        }
-
-        String[] args = command.split(" ");
-        List<String> parts = new ArrayList<String>();
-        parts.add(args[0]);
-        for (int i = 1; i < args.length; i++) {
-            if (!args[i].trim().startsWith("-")) {
-                parts.add(concatenateWithSpace(args, i));
-                break;
-            } else {
-                parts.add(args[i]);
-            }
-        }
-        return parts.toArray(new String[parts.size()]);
-    }
-
-    private String concatenateWithSpace(String[] args, int from) {
-        StringBuilder sb = new StringBuilder();
-
-        for (int i = from; i < args.length; i++) {
-            sb.append(args[i] + " ");
-        }
-        return sb.toString().trim();
-    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/ScpTest.java b/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
index 835b931..19572b6 100644
--- a/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
@@ -20,11 +20,13 @@ package org.apache.sshd;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.ServerSocket;
 import java.util.Properties;
+import java.util.concurrent.TimeUnit;
 
 import ch.ethz.ssh2.Connection;
 import ch.ethz.ssh2.SCPClient;
@@ -33,6 +35,7 @@ import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.Logger;
 import com.jcraft.jsch.UserInfo;
+import org.apache.sshd.client.ScpClient;
 import org.apache.sshd.server.command.ScpCommandFactory;
 import org.apache.sshd.util.BogusPasswordAuthenticator;
 import org.apache.sshd.util.EchoShellFactory;
@@ -45,6 +48,7 @@ import org.junit.Test;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Test for SCP support.
@@ -62,6 +66,7 @@ public class ScpTest {
         ServerSocket s = new ServerSocket(0);
         port = s.getLocalPort();
         s.close();
+//        port = 8102;
 
         sshd = SshServer.setUpDefaultServer();
         sshd.setPort(port);
@@ -123,6 +128,264 @@ public class ScpTest {
     }
 
     @Test
+    public void testScpNativeOnSingleFile() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        assertTrue(root.exists());
+
+
+        writeFile(new File("target/scp/local/out.txt"), data);
+        try {
+            scp.upload("target/scp/local/out.txt", "target/scp/remote/out.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // ok
+        }
+        new File(root, "remote").mkdirs();
+        scp.upload("target/scp/local/out.txt", "target/scp/remote/out.txt");
+        assertFileLength(new File("target/scp/remote/out.txt"), data.length(), 5000);
+
+        scp.download("target/scp/remote/out.txt", "target/scp/local/out2.txt");
+        assertFileLength(new File("target/scp/local/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnMultipleFiles() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/out2.txt"), data);
+        try {
+            scp.upload(new String[] { "target/scp/local/out1.txt", "target/scp/local/out2.txt" }, "target/scp/remote/out.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        writeFile(new File("target/scp/remote/out.txt"), data);
+        try {
+            scp.upload(new String[] { "target/scp/local/out1.txt", "target/scp/local/out2.txt" }, "target/scp/remote/out.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        new File(root, "remote/dir").mkdirs();
+        scp.upload(new String[] { "target/scp/local/out1.txt", "target/scp/local/out2.txt" }, "target/scp/remote/dir");
+        assertFileLength(new File("target/scp/remote/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+
+        try {
+            scp.download(new String[] { "target/scp/remote/dir/out1.txt", "target/scp/remote/dir/out2.txt" }, "target/scp/local/out1.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        try {
+            scp.download(new String[] { "target/scp/remote/dir/out1.txt", "target/scp/remote/dir/out2.txt" }, "target/scp/local/dir");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        new File(root, "local/dir").mkdirs();
+        scp.download(new String[] { "target/scp/remote/dir/out1.txt", "target/scp/remote/dir/out2.txt" }, "target/scp/local/dir");
+        assertFileLength(new File("target/scp/local/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnRecursiveDirs() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        new File("target/scp/local/dir").mkdirs();
+        writeFile(new File("target/scp/local/dir/out1.txt"), data);
+        writeFile(new File("target/scp/local/dir/out2.txt"), data);
+        scp.upload("target/scp/local/dir", "target/scp/remote/", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/remote/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+
+        Utils.deleteRecursive(new File("target/scp/local/dir"));
+        scp.download("target/scp/remote/dir", "target/scp/local", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/local/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnDirWithPattern() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/out2.txt"), data);
+        scp.upload("target/scp/local/*", "target/scp/remote/");
+        assertFileLength(new File("target/scp/remote/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/out2.txt"), data.length(), 5000);
+
+        new File("target/scp/local/out1.txt").delete();
+        new File("target/scp/local/out2.txt").delete();
+        scp.download("target/scp/remote/*", "target/scp/local");
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnMixedDirAndFiles() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        new File("target/scp/local/dir").mkdirs();
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/dir/out2.txt"), data);
+        scp.upload("target/scp/local/*", "target/scp/remote/", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/remote/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+
+        Utils.deleteRecursive(new File("target/scp/local/out1.txt"));
+        Utils.deleteRecursive(new File("target/scp/local/dir"));
+        scp.download("target/scp/remote/*", "target/scp/local");
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertFalse(new File("target/scp/local/dir/out2.txt").exists());
+
+        Utils.deleteRecursive(new File("target/scp/local/out1.txt"));
+        scp.download("target/scp/remote/*", "target/scp/local", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativePreserveAttributes() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        new File("target/scp/local/dir").mkdirs();
+        long lastMod = new File("target/scp/local/dir").lastModified() - TimeUnit.DAYS.toMillis(1);
+
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/dir/out2.txt"), data);
+        new File("target/scp/local/out1.txt").setLastModified(lastMod);
+        new File("target/scp/local/out1.txt").setExecutable(true, true);
+        new File("target/scp/local/out1.txt").setWritable(false, false);
+        new File("target/scp/local/dir/out2.txt").setLastModified(lastMod);
+        scp.upload("target/scp/local/*", "target/scp/remote/", ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+        assertFileLength(new File("target/scp/remote/out1.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/remote/out1.txt").lastModified());
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/remote/dir/out2.txt").lastModified());
+
+        Utils.deleteRecursive(new File("target/scp/local"));
+        new File("target/scp/local").mkdirs();
+        scp.download("target/scp/remote/*", "target/scp/local", ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/local/out1.txt").lastModified());
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/local/dir/out2.txt").lastModified());
+
+        session.close(false);
+        client.stop();
+    }
+
+    private void writeFile(File file, String data) throws IOException {
+        FileOutputStream fos = new FileOutputStream(file);
+        try {
+            fos.write(data.getBytes());
+        } finally {
+            fos.close();
+        }
+    }
+
+    @Test
     public void testScp() throws Exception {
         session = getJschSession();