[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 <[email protected]> Authored: Thu Jul 25 23:49:52 2013 +0200 Committer: Guillaume Nodet <[email protected]> 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();
