mina-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From lgoldst...@apache.org
Subject [1/2] mina-sshd git commit: [SSHD-476] Allow direct SCP file upload/download to/from stream
Date Tue, 26 May 2015 12:08:07 GMT
Repository: mina-sshd
Updated Branches:
  refs/heads/master d7939e253 -> 9b60dcc53


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
new file mode 100644
index 0000000..3f38e01
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.scp;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpReceiveLineHandler {
+    /**
+     * @param line Received SCP input line
+     * @param isDir Does the input line refer to a directory
+     * @param time The received {@link ScpTimestamp} - may be {@code null}
+     * @throws IOException If failed to process the line
+     */
+    void process(String line, boolean isDir, ScpTimestamp time) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
new file mode 100644
index 0000000..6749a66
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpSourceStreamResolver {
+    /**
+     * @return The uploaded file name
+     * @throws IOException If failed to resolve the name
+     */
+    String getFileName() throws IOException;
+
+    /**
+     * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
+     */
+    Path getEventListenerFilePath();
+
+    /**
+     * @return The permissions to be used for uploading a file
+     * @throws IOException If failed to generate the required permissions
+     */
+    Collection<PosixFilePermission> getPermissions() throws IOException;
+
+    /**
+     * @return The {@link ScpTimestamp} to use for uploading the file
+     * if {@code null} then no need to send this information
+     * @throws IOException If failed to generate the required data
+     */
+    ScpTimestamp getTimestamp() throws IOException;
+    
+    /**
+     * @return An estimated size of the expected number of bytes to be uploaded.
+     * If non-positive then assumed to be unknown.
+     * @throws IOException If failed to generate an estimate
+     */
+    long getSize() throws IOException;
+    
+    /**
+     * @return The {@link InputStream} containing the data to be uploaded
+     * @throws IOException If failed to create the stream
+     */
+    InputStream resolveSourceStream() throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
new file mode 100644
index 0000000..f3db61f
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.scp;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpTargetStreamResolver {
+    /**
+     * Called when receiving a file in order to obtain an output stream
+     * for the incoming data
+     * @param name File name as received from remote site
+     * @param length Number of bytes expected to receive
+     * @param perms The {@link Set} of {@link PosixFilePermission} expected
+     * @return The {@link OutputStream} to write the incoming data
+     * @throws IOException If failed to create the stream
+     */
+    OutputStream resolveTargetStream(String name, long length, Set<PosixFilePermission>
perms) throws IOException;
+
+    /**
+     * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
+     */
+    Path getEventListenerFilePath();
+
+    /**
+     * Called after successful reception of the data (and after closing the stream)
+     * @param name File name as received from remote site
+     * @param preserve If {@code true} then the resolver should attempt to preserve
+     * the specified permissions and timestamp
+     * @param perms The {@link Set} of {@link PosixFilePermission} expected
+     * @param time If not {@code null} then the required timestamp(s) on the
+     * incoming data
+     * @throws IOException If failed to post-process the incoming data
+     */
+    void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission>
perms, ScpTimestamp time) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
new file mode 100644
index 0000000..eccf71a
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.scp;
+
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpTimestamp {
+    public final long lastModifiedTime;
+    public final long lastAccessTime;
+
+    public ScpTimestamp(long modTime, long accTime) {
+        lastModifiedTime = modTime;
+        lastAccessTime = accTime;
+    }
+    
+    @Override
+    public String toString() {
+        return "modified=" + new Date(lastModifiedTime)
+             + ";accessed=" + new Date(lastAccessTime)
+             ;
+    }
+
+    /**
+     * @param line The time specification - format:
+     * {@code T<mtime-sec> <mtime-micros> <atime-sec> <atime-micros>}
+     * where specified times are since UTC 
+     * @return The {@link ScpTimestamp} value with the timestamps converted to
+     * <U>milliseconds</U>
+     * @throws NumberFormatException if bad numerical values - <B>Note:</B>
+     * does not check if 1st character is 'T'.
+     */
+    public static ScpTimestamp parseTime(String line) throws NumberFormatException {
+        String[] numbers = GenericUtils.split(line.substring(1), ' ');
+        return new ScpTimestamp(TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[0])),
+                                TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2])));
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
index 9df5f4c..98c3f6e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
@@ -598,7 +598,7 @@ public abstract class AbstractSession extends CloseableUtils.AbstractInnerClosea
                     return requestResult.get();
                 }
             } catch (InterruptedException e) {
-                throw (InterruptedIOException) new InterruptedIOException().initCause(e);
+                throw (InterruptedIOException) new InterruptedIOException("Interrupted while
waiting for request result").initCause(e);
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/main/java/org/apache/sshd/common/util/io/LimitInputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/LimitInputStream.java
b/sshd-core/src/main/java/org/apache/sshd/common/util/io/LimitInputStream.java
new file mode 100644
index 0000000..b4af206
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/LimitInputStream.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.util.io;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.Channel;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Reads from another {@link InputStream} up to specified max. length
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LimitInputStream extends FilterInputStream implements Channel {
+    private final AtomicBoolean open = new AtomicBoolean(true);
+    private long remaining;
+
+    public LimitInputStream(InputStream in, long length) {
+        super(in);
+        remaining = length;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return open.get();
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (!isOpen()) {
+            throw new IOException("read() - stream is closed (remaining=" + remaining + ")");
+        }
+
+        if (remaining > 0) {
+            remaining--;
+            return super.read();
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("read(len=" + len + ") stream is closed (remaining=" +
remaining + ")");
+        }
+
+        int nb = len;
+        if (nb > remaining) {
+            nb = (int) remaining;
+        }
+        if (nb > 0) {
+            int read = super.read(b, off, nb);
+            remaining -= read;
+            return read;
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("skip(" + n + ") stream is closed (remaining=" + remaining
+ ")");
+        }
+
+        long skipped = super.skip(n);
+        remaining -= skipped;
+        return skipped;
+    }
+
+    @Override
+    public int available() throws IOException {
+        if (!isOpen()) {
+            throw new IOException("available() stream is closed (remaining=" + remaining
+ ")");
+        }
+
+        int av = super.available();
+        if (av > remaining) {
+            return (int) remaining;
+        } else {
+            return av;
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        // do not close the original input stream since it serves for ACK(s)
+        if (open.getAndSet(false)) {
+            return; // debug breakpoint
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/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 1a96157..92ba0d5 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
@@ -41,11 +41,6 @@ import org.apache.sshd.server.CommandFactory;
  */
 public class ScpCommandFactory implements CommandFactory, Cloneable, ExecutorServiceConfigurer
{
     /**
-     * Command prefix used to identify SCP commands
-     */
-    public static final String SCP_COMMAND_PREFIX = "scp";
-
-    /**
      * A useful {@link ObjectBuilder} for {@link ScpCommandFactory}
      */
     public static class Builder implements ObjectBuilder<ScpCommandFactory> {
@@ -227,7 +222,7 @@ public class ScpCommandFactory implements CommandFactory, Cloneable, ExecutorSer
      */
     @Override
     public Command createCommand(String command) {
-        if (command.startsWith(SCP_COMMAND_PREFIX)) {
+        if (command.startsWith(ScpHelper.SCP_COMMAND_PREFIX)) {
             return new ScpCommand(command, getExecutorService(), isShutdownOnExit(), getSendBufferSize(),
getReceiveBufferSize(), listenerProxy);
         }
 
@@ -236,7 +231,7 @@ public class ScpCommandFactory implements CommandFactory, Cloneable, ExecutorSer
             return factory.createCommand(command);
         }
 
-        throw new IllegalArgumentException("Unknown command, does not begin with '" + SCP_COMMAND_PREFIX
+ "': " + command);
+        throw new IllegalArgumentException("Unknown command, does not begin with '" + ScpHelper.SCP_COMMAND_PREFIX
+ "': " + command);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/test/java/org/apache/sshd/client/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ScpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ScpTest.java
index 2fb871a..0a5406b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ScpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ScpTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sshd.client;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
@@ -31,6 +32,7 @@ import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.PosixFilePermission;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -38,9 +40,11 @@ import java.util.concurrent.TimeUnit;
 import org.apache.sshd.ClientSession;
 import org.apache.sshd.SshClient;
 import org.apache.sshd.SshServer;
+import org.apache.sshd.client.scp.ScpClient;
 import org.apache.sshd.common.Session;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.root.RootedFileSystemProvider;
+import org.apache.sshd.common.scp.ScpHelper;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.server.command.ScpCommandFactory;
@@ -51,7 +55,6 @@ import org.apache.sshd.util.JSchLogger;
 import org.apache.sshd.util.SimpleUserInfo;
 import org.apache.sshd.util.Utils;
 import org.junit.After;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -141,7 +144,7 @@ public class ScpTest extends BaseTestSupport {
                     ScpClient scp = createScpClient(session);
                     Path targetPath = detectTargetFolder().toPath();
                     Path parentPath = targetPath.getParent();
-                    Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                    Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX,
getClass().getSimpleName());
                     Utils.deleteRecursive(scpRoot);
 
                     Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
@@ -186,7 +189,7 @@ public class ScpTest extends BaseTestSupport {
 
                     Path targetPath = detectTargetFolder().toPath();
                     Path parentPath = targetPath.getParent();
-                    Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                    Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX,
getClass().getSimpleName());
                     Utils.deleteRecursive(scpRoot);
 
                     Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
@@ -210,7 +213,7 @@ public class ScpTest extends BaseTestSupport {
     @Test
     public void testScpUploadZeroLengthFile() throws Exception {
         Path targetPath = detectTargetFolder().toPath();
-        Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName());
         Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
         Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
         Path zeroLocal = localDir.resolve(getCurrentTestName());
@@ -249,7 +252,7 @@ public class ScpTest extends BaseTestSupport {
     @Test
     public void testScpDownloadZeroLengthFile() throws Exception {
         Path targetPath = detectTargetFolder().toPath();
-        Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+        Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName());
         Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
         Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
         Path zeroLocal = localDir.resolve(getCurrentTestName());
@@ -299,7 +302,7 @@ public class ScpTest extends BaseTestSupport {
 
                     Path targetPath = detectTargetFolder().toPath();
                     Path parentPath = targetPath.getParent();
-                    Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                    Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX,
getClass().getSimpleName());
                     Utils.deleteRecursive(scpRoot);
 
                     Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
@@ -350,7 +353,7 @@ public class ScpTest extends BaseTestSupport {
                     ScpClient scp = createScpClient(session);
                     Path targetPath = detectTargetFolder().toPath();
                     Path parentPath = targetPath.getParent();
-                    Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                    Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX,
getClass().getSimpleName());
                     Utils.deleteRecursive(scpRoot);
 
                     Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
@@ -432,7 +435,7 @@ public class ScpTest extends BaseTestSupport {
                 ScpClient scp = createScpClient(session);
                 Path targetPath = detectTargetFolder().toPath();
                 Path parentPath = targetPath.getParent();
-                Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName());
                 Utils.deleteRecursive(scpRoot);
 
                 Path localDir = scpRoot.resolve("local");
@@ -472,7 +475,7 @@ public class ScpTest extends BaseTestSupport {
                 ScpClient scp = createScpClient(session);
                 Path targetPath = detectTargetFolder().toPath();
                 Path parentPath = targetPath.getParent();
-                Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName());
                 Utils.deleteRecursive(scpRoot);
 
                 Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
@@ -510,7 +513,7 @@ public class ScpTest extends BaseTestSupport {
                 ScpClient scp = createScpClient(session);
                 Path targetPath = detectTargetFolder().toPath();
                 Path parentPath = targetPath.getParent();
-                Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName());
                 Utils.deleteRecursive(scpRoot);
 
                 Path localDir = scpRoot.resolve("local");
@@ -547,13 +550,9 @@ public class ScpTest extends BaseTestSupport {
 
     @Test
     public void testScpNativePreserveAttributes() throws Exception {
-        // Ignore this test if running a Windows system
-        Assume.assumeFalse("Skip test for Windows", OsUtils.isWin32());
-
         try (SshClient client = SshClient.setUpDefaultClient()) {
             client.start();
 
-
             try (ClientSession session = client.connect(getCurrentTestName(), "localhost",
port).await().getSession()) {
                 session.addPasswordIdentity(getCurrentTestName());
                 session.auth().verify(5L, TimeUnit.SECONDS);
@@ -561,12 +560,13 @@ public class ScpTest extends BaseTestSupport {
                 ScpClient scp = createScpClient(session);
                 Path targetPath = detectTargetFolder().toPath();
                 Path parentPath = targetPath.getParent();
-                Path scpRoot = Utils.resolve(targetPath, "scp", getClass().getSimpleName());
+                Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName());
                 Utils.deleteRecursive(scpRoot);
 
                 Path localDir = scpRoot.resolve("local");
                 Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
-                long lastMod = Files.getLastModifiedTime(localSubDir).toMillis() - TimeUnit.DAYS.toMillis(1);
+                // convert everything to seconds since this is the SCP timestamps granularity
+                long lastMod = TimeUnit.MILLISECONDS.toSeconds(Files.getLastModifiedTime(localSubDir).toMillis()
- TimeUnit.DAYS.toMillis(1));
                 Path local1 = localDir.resolve(getCurrentTestName() + "-1.txt");
                 byte[] data = writeFile(local1, getCurrentTestName() + "\n");
                 File lclFile1 = local1.toFile();
@@ -587,29 +587,77 @@ public class ScpTest extends BaseTestSupport {
                 assertFileLength(remote1, data.length, 5000);
                 
                 File remFile1 = remote1.toFile();
-                assertEquals("Mismatched uploaded last-modified time for " + remFile1, lastMod,
remFile1.lastModified());
+                assertLastModifiedTimeEquals(remFile1, lastMod);
 
                 Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
                 Path remoteSub2 = remoteSubDir.resolve(localSub2.getFileName());
                 assertFileLength(remoteSub2, data.length, 5000);
 
                 File remSubFile2 = remoteSub2.toFile();
-                assertEquals("Mismatched uploaded last-modified time for " + remSubFile2,
lastMod, remSubFile2.lastModified());
+                assertLastModifiedTimeEquals(remSubFile2, lastMod);
 
                 Utils.deleteRecursive(localDir);
                 Files.createDirectories(localDir);
 
                 scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
                 assertFileLength(local1, data.length, 5000);
-                assertEquals("Mismatched downloaded last-modified time for " + lclFile1,
lastMod, lclFile1.lastModified());
+                assertLastModifiedTimeEquals(lclFile1, lastMod);
                 assertFileLength(localSub2, data.length, 5000);
-                assertEquals("Mismatched downloaded last-modified time for " + lclSubFile2,
lastMod, lclSubFile2.lastModified());
+                assertLastModifiedTimeEquals(lclSubFile2, lastMod);
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
+    public void testStreamsUploadAndDownload() throws Exception {
+        try (SshClient client = SshClient.setUpDefaultClient()) {
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), "localhost",
port).await().getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                ScpClient scp = createScpClient(session);
+                Path targetPath = detectTargetFolder().toPath();
+                Path parentPath = targetPath.getParent();
+                Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName());
+                Utils.deleteRecursive(scpRoot);
+
+                Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+                Path remoteFile = remoteDir.resolve(getCurrentTestName() + ".txt");
+                String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+                byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes();
+                scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
+
+                byte[] uploaded = Files.readAllBytes(remoteFile);
+                assertArrayEquals("Mismatched uploaded data", data, uploaded);
+
+                byte[] downloaded = scp.downloadBytes(remotePath);
+                assertArrayEquals("Mismatched downloaded data", uploaded, downloaded);
             } finally {
                 client.stop();
             }
         }
     }
 
+    // see http://stackoverflow.com/questions/2717936/file-createnewfile-creates-files-with-last-modified-time-before-actual-creatio
+    // See https://msdn.microsoft.com/en-us/library/ms724290(VS.85).aspx
+    // The NTFS file system delays updates to the last access time for a file by up to 1
hour after the last access
+    private static void assertLastModifiedTimeEquals(File file, long expectedSeconds) {
+        long actualSeconds = TimeUnit.MILLISECONDS.toSeconds(file.lastModified());
+        if (OsUtils.isWin32()) {
+            if (expectedSeconds != actualSeconds) {
+                System.err.append("Mismatched last modified time for ").append(file.getAbsolutePath())
+                          .append(" - expected=").append(String.valueOf(expectedSeconds))
+                          .append(", actual=").println(actualSeconds);
+            }
+        } else {
+            assertEquals("Mismatched last modified time for " + file.getAbsolutePath(), expectedSeconds,
actualSeconds);
+        }
+    }
+
     private static byte[] writeFile(Path path, String data) throws IOException {
         try(OutputStream fos = Files.newOutputStream(path)) {
             byte[]  bytes = data.getBytes();
@@ -643,7 +691,7 @@ public class ScpTest extends BaseTestSupport {
             sendFile(unixDir, fileName, data);
             assertFileLength(target, data.length(), 5000);
     
-            sendFileError("target", "scp", data);
+            sendFileError("target", ScpHelper.SCP_COMMAND_PREFIX, data);
     
             readFileError(unixDir);
     
@@ -653,7 +701,7 @@ public class ScpTest extends BaseTestSupport {
             target.delete();
             root.delete();
     
-            sendDir("target", "scp", fileName, data);
+            sendDir("target", ScpHelper.SCP_COMMAND_PREFIX, fileName, data);
             assertFileLength(target, data.length(), 5000);
         } finally {
             session.disconnect();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/test/java/org/apache/sshd/common/util/io/LimitInputStreamTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/util/io/LimitInputStreamTest.java
b/sshd-core/src/test/java/org/apache/sshd/common/util/io/LimitInputStreamTest.java
new file mode 100644
index 0000000..c873611
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/util/io/LimitInputStreamTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.util.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.sshd.util.BaseTestSupport;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LimitInputStreamTest extends BaseTestSupport {
+    public LimitInputStreamTest() {
+        super();
+    }
+
+    @Test
+    public void testReadLimit() throws IOException {
+        Path targetPath = detectTargetFolder().toPath();
+        Path rootFolder = assertHierarchyTargetFolderExists(targetPath.resolve(getClass().getSimpleName()));
+        Path inputFile = rootFolder.resolve(getCurrentTestName() + ".bin"); 
+        byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes();
+        Files.write(inputFile, data);
+
+        try(InputStream in = Files.newInputStream(inputFile)) {
+            int maxLen = data.length / 2;
+            byte[] expected = new byte[maxLen];
+            System.arraycopy(data, 0, expected, 0, expected.length);
+
+            byte[] actual = new byte[expected.length];
+            try(LimitInputStream limited = new LimitInputStream(in, expected.length)) {
+                assertTrue("Limited stream not marked as open", limited.isOpen());
+                assertEquals("Mismatched initial available data size", expected.length, limited.available());
+
+                int readLen = limited.read(actual);
+                assertEquals("Incomplete actual data read", actual.length, readLen);
+                assertArrayEquals("Mismatched read data", expected, actual);
+                assertEquals("Mismatched remaining available data size", 0, limited.available());
+                
+                readLen = limited.read();
+                assertTrue("Unexpected success to read one more byte: " + readLen, readLen
< 0);
+
+                readLen = limited.read(actual);
+                assertTrue("Unexpected success to read extra buffer: " + readLen, readLen
< 0);
+                
+                limited.close();
+                assertFalse("Limited stream still marked as open", limited.isOpen());
+                
+                try {
+                    readLen = limited.read();
+                    fail("Unexpected one byte read success after close");
+                } catch(IOException e) {
+                    // expected
+                }
+
+                try {
+                    readLen = limited.read(actual);
+                    fail("Unexpected buffer read success after close: " + readLen);
+                } catch(IOException e) {
+                    // expected
+                }
+
+                try {
+                    readLen = limited.read(actual);
+                    fail("Unexpected buffer read success after close: " + readLen);
+                } catch(IOException e) {
+                    // expected
+                }
+
+                try {
+                    readLen = (int) limited.skip(Byte.SIZE);
+                    fail("Unexpected skip success after close: " + readLen);
+                } catch(IOException e) {
+                    // expected
+                }
+
+                try {
+                    readLen = limited.available();
+                    fail("Unexpected available success after close: " + readLen);
+                } catch(IOException e) {
+                    // expected
+                }
+            }
+            
+            // make sure underlying stream not closed
+            int readLen = in.read(actual);
+            assertEquals("Incomplete extra data read", Math.min(actual.length, data.length
- expected.length), readLen);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/9b60dcc5/sshd-core/src/test/java/org/apache/sshd/util/Utils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/Utils.java b/sshd-core/src/test/java/org/apache/sshd/util/Utils.java
index 5f53f3e..fd42e38 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/Utils.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/Utils.java
@@ -116,7 +116,15 @@ public class Utils {
             }
         }
 
-        file.delete();
+        // seems that if a file is not writable it cannot be deleted
+        if (!file.canWrite()) {
+            file.setWritable(true, false);
+        }
+        
+        if (!file.delete()) {
+            System.err.append("Failed to delete ").println(file.getAbsolutePath());
+        }
+
         return file;
     }
     
@@ -142,6 +150,10 @@ public class Utils {
         }
         
         try {
+            // seems that if a file is not writable it cannot be deleted
+            if (!Files.isWritable(path)) {
+                path.toFile().setWritable(true, false);
+            }
             Files.delete(path);
         } catch(IOException e) {
             // same logic as deleteRecursive(File) which does not check if deletion succeeded


Mime
View raw message