You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/11/18 09:33:02 UTC

[4/5] mina-sshd git commit: Added SftpCommandMain upload/download marker progress indication (default=true)

Added SftpCommandMain upload/download marker progress indication (default=true)


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

Branch: refs/heads/master
Commit: bd40cd0c70866ec4543b61cbf87c57296296019b
Parents: 1a8924d
Author: Lyor Goldstein <lg...@apache.org>
Authored: Sun Nov 18 11:12:28 2018 +0200
Committer: Lyor Goldstein <lg...@apache.org>
Committed: Sun Nov 18 11:32:53 2018 +0200

----------------------------------------------------------------------
 CHANGES.md                                      |  14 ++
 .../apache/sshd/cli/client/SftpCommandMain.java | 201 ++++++++++++++-----
 .../SftpFileTransferProgressOutputStream.java   | 129 ++++++++++++
 3 files changed, 297 insertions(+), 47 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/bd40cd0c/CHANGES.md
----------------------------------------------------------------------
diff --git a/CHANGES.md b/CHANGES.md
index e79d215..60f941b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -88,3 +88,17 @@ in order to provide key file(s) location information
 
 * [SSHD-866](https://issues.apache.org/jira/browse/SSHD-866) - Counting empty challenges separately when enforcing
 max. attempts during `keyboard-interactive` authentication
+
+* `SftpCommandMain` shows by default `get/put` command progress using the hash sign (`#`) marker. The marker
+can be enabled/disabled via the `progress` command:
+
+```
+    > progress
+
+    ... reponse is whether it is 'on' or 'off'
+
+    > progress on/off
+
+    ... set the progress marker indicator  ...
+
+```

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/bd40cd0c/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
index 99427ff..694d937 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
@@ -41,6 +41,7 @@ import java.util.ServiceLoader;
 import java.util.TreeMap;
 import java.util.logging.Level;
 
+import org.apache.sshd.cli.client.helper.SftpFileTransferProgressOutputStream;
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.subsystem.sftp.SftpClient;
@@ -89,6 +90,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
     private final Map<String, SftpCommandExecutor> commandsMap;
     private String cwdRemote;
     private String cwdLocal;
+    private boolean showProgress = true;
 
     public SftpCommandMain(SftpClient client) {
         this.client = Objects.requireNonNull(client, "No client");
@@ -113,6 +115,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
                 new StatVfsCommandExecutor(),
                 new GetCommandExecutor(),
                 new PutCommandExecutor(),
+                new ProgressCommandExecutor(),
                 new HelpCommandExecutor()
         )) {
             String name = e.getName();
@@ -205,9 +208,11 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
     }
 
-    protected <A extends Appendable> A appendFileAttributes(A stdout, SftpClient sftp, String path, Attributes attrs) throws IOException {
-        stdout.append('\t').append(Long.toString(attrs.getSize()))
-              .append('\t').append(SftpFileSystemProvider.getRWXPermissions(attrs.getPermissions()));
+    protected <A extends Appendable> A appendFileAttributes(
+            A stdout, SftpClient sftp, String path, Attributes attrs)
+                throws IOException {
+        stdout.append("    ").append(Long.toString(attrs.getSize()))
+              .append("    ").append(SftpFileSystemProvider.getRWXPermissions(attrs.getPermissions()));
         if (attrs.isSymbolicLink()) {
             String linkValue = sftp.readLink(path);
             stdout.append(" => ")
@@ -234,6 +239,14 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         cwdLocal = path;
     }
 
+    public boolean isShowProgress() {
+        return showProgress;
+    }
+
+    public void setShowProgress(boolean showProgress) {
+        this.showProgress = showProgress;
+    }
+
     @Override
     public boolean isOpen() {
         return client.isOpen();
@@ -249,7 +262,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
     //////////////////////////////////////////////////////////////////////////
 
     public static <A extends Appendable> A appendInfoValue(A sb, CharSequence name, Object value) throws IOException {
-        sb.append('\t').append(name).append(": ").append(Objects.toString(value));
+        sb.append("    ").append(name).append(": ").append(Objects.toString(value));
         return sb;
     }
 
@@ -309,7 +322,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
                 setupLogging(level, stdout, stderr, logStream);
             }
 
-            ClientSession session = (logStream == null) ? null : setupClientSession(SFTP_PORT_OPTION, stdin, stdout, stderr, args);
+            ClientSession session = (logStream == null)
+                ? null
+                : setupClientSession(SFTP_PORT_OPTION, stdin, stdout, stderr, args);
             if (session == null) {
                 System.err.println("usage: sftp [-v[v][v]] [-E logoutput] [-i identity] [-io nio2|mina|netty]"
                         + " [-l login] [" + SFTP_PORT_OPTION + " port] [-o option=value]"
@@ -347,7 +362,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
             stdout.println("Exiting");
             return true;
@@ -365,10 +382,12 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
-            stdout.append('\t').append("Remote: ").println(getCurrentRemoteDirectory());
-            stdout.append('\t').append("Local: ").println(getCurrentLocalDirectory());
+            stdout.append("    ").append("Remote: ").println(getCurrentRemoteDirectory());
+            stdout.append("    ").append("Local: ").println(getCurrentLocalDirectory());
             return false;
         }
     }
@@ -384,7 +403,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
             SftpClient sftp = getClient();
             ClientSession session = sftp.getSession();
@@ -414,7 +435,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
             SftpClient sftp = getClient();
             ClientSession session = sftp.getSession();
@@ -442,11 +465,13 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
             SftpClient sftp = getClient();
             Session session = sftp.getSession();
-            stdout.append('\t').println(session.getServerVersion());
+            stdout.append("    ").println(session.getServerVersion());
 
             Map<String, byte[]> extensions = sftp.getServerExtensions();
             Map<String, ?> parsed = ParserUtils.parse(extensions);
@@ -457,7 +482,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
             extensions.forEach((name, value) -> {
                 Object info = parsed.get(name);
 
-                stdout.append('\t').append(name).append(": ");
+                stdout.append("    ").append(name).append(": ");
                 if (info == null) {
                     stdout.println(BufferUtils.toHex(value));
                 } else {
@@ -480,10 +505,12 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
             SftpClient sftp = getClient();
-            stdout.append('\t').println(sftp.getVersion());
+            stdout.append("    ").println(sftp.getVersion());
             return false;
         }
     }
@@ -499,7 +526,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
 
             String newPath = resolveRemotePath(args);
@@ -520,7 +549,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             if (GenericUtils.isEmpty(args)) {
                 setCurrentLocalDirectory(System.getProperty("user.home"));
             } else {
@@ -545,7 +576,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
 
             String path = resolveRemotePath(args);
@@ -566,7 +599,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             String[] comps = GenericUtils.split(args, ' ');
             int numComps = GenericUtils.length(comps);
             String pathArg = (numComps <= 0) ? null : GenericUtils.trimToEmpty(comps[numComps - 1]);
@@ -584,7 +619,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
             for (SftpClient.DirEntry entry : sftp.readDir(path)) {
                 String fileName = entry.getFilename();
                 SftpClient.Attributes attrs = entry.getAttributes();
-                appendFileAttributes(stdout.append('\t').append(fileName), sftp, path + "/" + fileName, attrs).println();
+                appendFileAttributes(stdout.append("    ").append(fileName), sftp, path + "/" + fileName, attrs).println();
                 if (showLongName) {
                     stdout.append("\t\tlong-name: ").println(entry.getLongFilename());
                 }
@@ -605,7 +640,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             String[] comps = GenericUtils.split(args, ' ');
             int numArgs = GenericUtils.length(comps);
             ValidateUtils.checkTrue(numArgs >= 1, "No arguments");
@@ -643,14 +680,16 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
             } else {
                 sftp.remove(path);
                 if (verbose) {
-                    stdout.append('\t').append("Removed ").println(path);
+                    stdout.append("    ").append("Removed ").println(path);
                 }
             }
 
             return false;
         }
 
-        private void removeRecursive(SftpClient sftp, String path, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
+        private void removeRecursive(
+                SftpClient sftp, String path, Attributes attrs, PrintStream stdout, boolean verbose)
+                    throws IOException {
             if (attrs.isDirectory()) {
                 for (DirEntry entry : sftp.readDir(path)) {
                     String name = entry.getFilename();
@@ -666,13 +705,13 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
                 sftp.remove(path);
             } else {
                 if (verbose) {
-                    stdout.append('\t').append("Skip special file ").println(path);
+                    stdout.append("    ").append("Skip special file ").println(path);
                     return;
                 }
             }
 
             if (verbose) {
-                stdout.append('\t').append("Removed ").println(path);
+                stdout.append("    ").append("Removed ").println(path);
             }
         }
     }
@@ -688,7 +727,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
 
             String path = resolveRemotePath(args);
@@ -709,7 +750,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             String[] comps = GenericUtils.split(args, ' ');
             ValidateUtils.checkTrue(GenericUtils.length(comps) == 2, "Invalid number of arguments: %s", args);
 
@@ -732,7 +775,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             String[] comps = GenericUtils.split(args, ' ');
             int numArgs = GenericUtils.length(comps);
             ValidateUtils.checkTrue(numArgs <= 1, "Invalid number of arguments: %s", args);
@@ -741,7 +786,8 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
             OpenSSHStatPathExtension ext = sftp.getExtension(OpenSSHStatPathExtension.class);
             ValidateUtils.checkTrue(ext.isSupported(), "Extension not supported by server: %s", ext.getName());
 
-            String remPath = resolveRemotePath((numArgs >= 1) ? GenericUtils.trimToEmpty(comps[0]) :  GenericUtils.trimToEmpty(args));
+            String remPath = resolveRemotePath(
+                (numArgs >= 1) ? GenericUtils.trimToEmpty(comps[0]) :  GenericUtils.trimToEmpty(args));
             OpenSSHStatExtensionInfo info = ext.stat(remPath);
             Field[] fields = info.getClass().getFields();
             for (Field f : fields) {
@@ -752,7 +798,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
                 }
 
                 Object value = f.get(info);
-                stdout.append('\t').append(name).append(": ").println(value);
+                stdout.append("    ").append(name).append(": ").println(value);
             }
 
             return false;
@@ -770,7 +816,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             String[] comps = GenericUtils.split(args, ' ');
             ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args);
 
@@ -800,7 +848,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
             String path = GenericUtils.trimToEmpty(resolveRemotePath(args));
             SftpClient client = getClient();
             String linkData = client.readLink(path);
-            stdout.append('\t').println(linkData);
+            stdout.append("    ").println(linkData);
             return false;
         }
     }
@@ -817,10 +865,12 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
 
         @Override
         @SuppressWarnings("synthetic-access")
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
             for (String cmd : commandsMap.keySet()) {
-                stdout.append('\t').println(cmd);
+                stdout.append("    ").println(cmd);
             }
             return false;
         }
@@ -846,7 +896,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
             createDirectories(sftp, remotePath.substring(0, pos));
         }
 
-        protected void transferFile(SftpClient sftp, Path localPath, String remotePath, boolean upload, PrintStream stdout, boolean verbose) throws IOException {
+        protected void transferFile(
+                SftpClient sftp, Path localPath, String remotePath, boolean upload, PrintStream stdout, boolean verbose)
+                    throws IOException {
             // Create the file's hierarchy
             if (upload) {
                 int pos = remotePath.lastIndexOf('/');
@@ -856,19 +908,33 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
                 Files.createDirectories(localPath.getParent());
             }
 
+            boolean withProgress = isShowProgress();
+            long copySize;
             try (InputStream input = upload ? Files.newInputStream(localPath) : sftp.read(remotePath);
-                 OutputStream output = upload ? sftp.write(remotePath) : Files.newOutputStream(localPath)) {
-                IoUtils.copy(input, output, SftpClient.IO_BUFFER_SIZE);
+                 OutputStream target = upload ? sftp.write(remotePath) : Files.newOutputStream(localPath);
+                 OutputStream output = withProgress ? new SftpFileTransferProgressOutputStream(target, stdout) : target) {
+                if (withProgress) {
+                    stdout.println();
+                }
+
+                copySize = IoUtils.copy(input, output, SftpClient.IO_BUFFER_SIZE);
+
+                if (withProgress) {
+                    stdout.println();
+                }
             }
 
             if (verbose) {
-                stdout.append('\t')
-                      .append("Copied ").append(upload ? localPath.toString() : remotePath)
-                      .append(" to ").println(upload ? remotePath : localPath.toString());
+                stdout.append("    ")
+                  .append("Copied ").append(Long.toString(copySize)).append(" bytes")
+                  .append(" from ").append(upload ? localPath.toString() : remotePath)
+                  .append(" to ").println(upload ? remotePath : localPath.toString());
             }
         }
 
-        protected void transferRemoteDir(SftpClient sftp, Path localPath, String remotePath, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
+        protected void transferRemoteDir(
+                SftpClient sftp, Path localPath, String remotePath, Attributes attrs, PrintStream stdout, boolean verbose)
+                    throws IOException {
             if (attrs.isDirectory()) {
                 for (DirEntry entry : sftp.readDir(remotePath)) {
                     String name = entry.getFilename();
@@ -882,12 +948,14 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
                 transferFile(sftp, localPath, remotePath, false, stdout, verbose);
             } else {
                 if (verbose) {
-                    stdout.append('\t').append("Skip remote special file ").println(remotePath);
+                    stdout.append("    ").append("Skip remote special file ").println(remotePath);
                 }
             }
         }
 
-        protected void transferLocalDir(SftpClient sftp, Path localPath, String remotePath, PrintStream stdout, boolean verbose) throws IOException {
+        protected void transferLocalDir(
+                SftpClient sftp, Path localPath, String remotePath, PrintStream stdout, boolean verbose)
+                    throws IOException {
             if (Files.isDirectory(localPath)) {
                 try (DirectoryStream<Path> ds = Files.newDirectoryStream(localPath)) {
                     for (Path entry : ds) {
@@ -899,7 +967,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
                 transferFile(sftp, localPath, remotePath, true, stdout, verbose);
             } else {
                 if (verbose) {
-                    stdout.append('\t').append("Skip local special file ").println(localPath);
+                    stdout.append("    ").append("Skip local special file ").println(localPath);
                 }
             }
         }
@@ -980,7 +1048,9 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             executeCommand(args, false, stdout);
             return false;
         }
@@ -997,9 +1067,46 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         }
 
         @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
             executeCommand(args, true, stdout);
             return false;
         }
     }
+
+    private class ProgressCommandExecutor implements SftpCommandExecutor {
+        ProgressCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "progress";
+        }
+
+        @Override
+        public boolean executeCommand(
+                String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                    throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            if (numArgs <= 0) {
+                stdout.append("    ").append(getName()).append(' ').println(isShowProgress() ? "on" : "off");
+            } else {
+                ValidateUtils.checkTrue(numArgs == 1, "Invalid arguments count: %d", numArgs);
+
+                String argVal = comps[0];
+                if ("on".equalsIgnoreCase(argVal)) {
+                    setShowProgress(true);
+                } else if ("off".equalsIgnoreCase(argVal)) {
+                    setShowProgress(false);
+                } else {
+                    throw new IllegalArgumentException("Unknown value: " + argVal);
+                }
+            }
+
+            return false;
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/bd40cd0c/sshd-cli/src/main/java/org/apache/sshd/cli/client/helper/SftpFileTransferProgressOutputStream.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/helper/SftpFileTransferProgressOutputStream.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/helper/SftpFileTransferProgressOutputStream.java
new file mode 100644
index 0000000..26330bc
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/helper/SftpFileTransferProgressOutputStream.java
@@ -0,0 +1,129 @@
+/*
+ * 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.cli.client.helper;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
+import java.util.Objects;
+
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpFileTransferProgressOutputStream extends FilterOutputStream {
+    public static final char DEFAULT_PROGRESS_CHAR = '#';
+    public static final int DEFAULT_MARKS_PER_LINE = 72;
+    public static final int DEFAULT_MARKER_SIZE = IoUtils.DEFAULT_COPY_SIZE;
+
+    private final int markerSize;
+    private final char markerChar;
+    private final int markersPerLine;
+    private final Appendable stdout;
+    private final byte[] workBuf = {0};
+    private long byteCount;
+    private long lastMarkOffset;
+    private int curMarkersInLine;
+
+    public SftpFileTransferProgressOutputStream(OutputStream out, Appendable stdout) {
+        this(out, DEFAULT_MARKER_SIZE, DEFAULT_PROGRESS_CHAR, DEFAULT_MARKS_PER_LINE, stdout);
+    }
+
+    public SftpFileTransferProgressOutputStream(
+            OutputStream out, int markerSize, char markerChar, int markersPerLine, Appendable stdout) {
+        super(Objects.requireNonNull(out, "No target stream"));
+
+        ValidateUtils.checkTrue(markerSize > 0, "Invalid marker size: %d", markerSize);
+        this.markerSize = markerSize;
+
+        if ((markerChar <= ' ') || (markerChar > 0x7E)) {
+            throw new IllegalArgumentException("Non-printable marker character: 0x" + Integer.toHexString(markerChar));
+        }
+        this.markerChar = markerChar;
+
+        ValidateUtils.checkTrue(markersPerLine > 0, "Invalid markers per line: %d", markersPerLine);
+
+        this.markersPerLine = markersPerLine;
+        this.stdout = Objects.requireNonNull(stdout, "No progress report target");
+    }
+
+    public int getMarkerSize() {
+        return markerSize;
+    }
+
+    public char getMarkerChar() {
+        return markerChar;
+    }
+
+    public int getMarkersPerLine() {
+        return markersPerLine;
+    }
+
+    public Appendable getStdout() {
+        return stdout;
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        workBuf[0] = (byte) (b & 0xFF);
+        write(workBuf, 0, 1);
+    }
+
+    @Override
+    public void write(byte[] b) throws IOException {
+        write(b, 0, b.length);
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        if ((len < 0) || (off < 0)) {
+            throw new StreamCorruptedException("Invalid offset (" + off + ")/length(" + len + ")");
+        }
+        this.out.write(b, off, len);
+
+        byteCount += len;
+
+        long reportDiff = byteCount - lastMarkOffset;
+        int reportSize = getMarkerSize();
+        long markersCount = reportDiff / reportSize;
+        appendMarkers((int) markersCount);
+        lastMarkOffset += markersCount * reportSize;
+    }
+
+    protected void appendMarkers(int markersCount) throws IOException {
+        if (markersCount <= 0) {
+            return;
+        }
+
+        Appendable target = getStdout();
+        char marker = getMarkerChar();
+        for (int index = 1, limit = getMarkersPerLine(); index <= markersCount; index++) {
+            target.append(marker);
+            curMarkersInLine++;
+            if (curMarkersInLine >= limit) {
+                target.append(System.lineSeparator());
+                curMarkersInLine = 0;
+            }
+        }
+    }
+}