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 2015/05/26 13:28:07 UTC

mina-sshd git commit: [SSHD-472] Create a Closeable SFTP Handle

Repository: mina-sshd
Updated Branches:
  refs/heads/master 8009a89f9 -> d7939e253


[SSHD-472] Create a Closeable SFTP Handle


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

Branch: refs/heads/master
Commit: d7939e2530d009f62ee6595f5f26c4c4ac58fe25
Parents: 8009a89
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Tue May 26 14:27:56 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Tue May 26 14:27:56 2015 +0300

----------------------------------------------------------------------
 .../java/org/apache/sshd/ClientSession.java     |   2 +-
 .../java/org/apache/sshd/client/SftpClient.java | 275 ------------------
 .../sshd/client/session/ClientSessionImpl.java  |   2 +-
 .../sshd/client/sftp/AbstractSftpClient.java    |  93 ++++++
 .../client/sftp/DefaultCloseableHandle.java     |  55 ++++
 .../sshd/client/sftp/DefaultSftpClient.java     | 232 ++++++++-------
 .../org/apache/sshd/client/sftp/SftpClient.java | 289 +++++++++++++++++++
 .../sshd/client/sftp/SftpFileChannel.java       |   1 -
 .../apache/sshd/client/sftp/SftpFileSystem.java |  14 +-
 .../client/sftp/SftpFileSystemProvider.java     |   6 +-
 .../apache/sshd/common/sftp/SftpConstants.java  |   2 +-
 .../sshd/server/sftp/SftpSubsystemFactory.java  |   3 +-
 .../test/java/org/apache/sshd/ClientTest.java   |   3 +-
 .../client/sftp/DefaultCloseableHandleTest.java |  87 ++++++
 .../sshd/client/sftp/SftpFileSystemTest.java    |   5 +-
 .../org/apache/sshd/client/sftp/SftpTest.java   | 111 +++----
 16 files changed, 704 insertions(+), 476 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
index fca6b4a..822e072 100644
--- a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
@@ -25,13 +25,13 @@ import java.util.Map;
 
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.ScpClient;
-import org.apache.sshd.client.SftpClient;
 import org.apache.sshd.client.UserInteraction;
 import org.apache.sshd.client.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ChannelShell;
 import org.apache.sshd.client.channel.ChannelSubsystem;
 import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.client.sftp.SftpClient;
 import org.apache.sshd.common.Session;
 import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.future.CloseFuture;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
deleted file mode 100644
index 3b8840c..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * 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.client;
-
-import static org.apache.sshd.common.sftp.SftpConstants.S_IFDIR;
-import static org.apache.sshd.common.sftp.SftpConstants.S_IFLNK;
-import static org.apache.sshd.common.sftp.SftpConstants.S_IFMT;
-import static org.apache.sshd.common.sftp.SftpConstants.S_IFREG;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.attribute.FileTime;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * @author <a href="http://mina.apache.org">Apache MINA Project</a>
- */
-public interface SftpClient extends Closeable {
-
-    enum OpenMode {
-        Read,
-        Write,
-        Append,
-        Create,
-        Truncate,
-        Exclusive
-    }
-
-    enum CopyMode {
-        Atomic,
-        Overwrite
-    }
-
-    enum Attribute {
-        Size,
-        UidGid,
-        Perms,
-        AcModTime,
-        OwnerGroup,
-        AccessTime,
-        ModifyTime,
-        CreateTime,
-    }
-
-    public static class Handle {
-        public final String id;
-        public Handle(String id) {
-            this.id = id;
-        }
-        
-        @Override
-        public String toString() {
-            return id;
-        }
-    }
-
-    public static class Attributes {
-        public final Set<Attribute> flags = EnumSet.noneOf(Attribute.class);
-        public long size;
-        public byte type;
-        public int uid;
-        public int gid;
-        public int perms;
-        public int atime;
-        public int ctime;
-        public int mtime;
-        public String owner;
-        public String group;
-        public FileTime accessTime;
-        public FileTime createTime;
-        public FileTime modifyTime;
-
-        @Override
-        public String toString() {
-            return "type=" + type
-                 + ";size=" + size
-                 + ";uid=" + uid
-                 + ";gid=" + gid
-                 + ";perms=0x" + Integer.toHexString(perms)
-                 + ";flags=" + flags
-                 + ";owner=" + owner
-                 + ";group=" + group
-                 + ";aTime=(" + atime + ")[" + accessTime + "]"
-                 + ";cTime=(" + ctime + ")[" + createTime + "]"
-                 + ";mTime=(" + mtime + ")[" + modifyTime + "]"
-                 ;
-        }
-
-        public Attributes size(long size) {
-            flags.add(Attribute.Size);
-            this.size = size;
-            return this;
-        }
-        public Attributes owner(String owner) {
-            flags.add(Attribute.OwnerGroup);
-            this.owner = owner;
-            if (group == null) {
-                group = "GROUP@";
-            }
-            return this;
-        }
-        public Attributes group(String group) {
-            flags.add(Attribute.OwnerGroup);
-            this.group = group;
-            if (owner == null) {
-                owner = "OWNER@";
-            }
-            return this;
-        }
-        public Attributes owner(int uid, int gid) {
-            flags.add(Attribute.UidGid);
-            this.uid = uid;
-            this.gid = gid;
-            return this;
-        }
-        public Attributes perms(int perms) {
-            flags.add(Attribute.Perms);
-            this.perms = perms;
-            return this;
-        }
-        public Attributes atime(int atime) {
-            flags.add(Attribute.AccessTime);
-            this.atime = atime;
-            this.accessTime = FileTime.from(atime, TimeUnit.SECONDS);
-            return this;
-        }
-        public Attributes ctime(int ctime) {
-            flags.add(Attribute.CreateTime);
-            this.ctime = ctime;
-            this.createTime = FileTime.from(atime, TimeUnit.SECONDS);
-            return this;
-        }
-        public Attributes mtime(int mtime) {
-            flags.add(Attribute.ModifyTime);
-            this.mtime = mtime;
-            this.modifyTime = FileTime.from(atime, TimeUnit.SECONDS);
-            return this;
-        }
-        public Attributes time(int atime, int mtime) {
-            flags.add(Attribute.AcModTime);
-            this.atime = atime;
-            this.mtime = mtime;
-            return this;
-        }
-        public Attributes accessTime(FileTime atime) {
-            flags.add(Attribute.AccessTime);
-            this.atime = (int) atime.to(TimeUnit.SECONDS);
-            this.accessTime = atime;
-            return this;
-        }
-        public Attributes createTime(FileTime ctime) {
-            flags.add(Attribute.CreateTime);
-            this.ctime = (int) ctime.to(TimeUnit.SECONDS);
-            this.createTime = ctime;
-            return this;
-        }
-        public Attributes modifyTime(FileTime mtime) {
-            flags.add(Attribute.ModifyTime);
-            this.mtime = (int) mtime.to(TimeUnit.SECONDS);
-            this.modifyTime = mtime;
-            return this;
-        }
-        public boolean isRegularFile() {
-            return (perms & S_IFMT) == S_IFREG;
-        }
-        public boolean isDirectory() {
-            return (perms & S_IFMT) == S_IFDIR;
-        }
-        public boolean isSymbolicLink() {
-            return (perms & S_IFMT) == S_IFLNK;
-        }
-        public boolean isOther() {
-            return !isRegularFile() && !isDirectory() && !isSymbolicLink();
-        }
-    }
-
-    public static class DirEntry {
-        public String filename;
-        public String longFilename;
-        public Attributes attributes;
-        public DirEntry(String filename, String longFilename, Attributes attributes) {
-            this.filename = filename;
-            this.longFilename = longFilename;
-            this.attributes = attributes;
-        }
-    }
-
-    int getVersion();
-
-    boolean isClosing();
-
-    //
-    // Low level API
-    //
-
-    Handle open(String path, Collection<OpenMode> options) throws IOException;
-
-    void close(Handle handle) throws IOException;
-
-    void remove(String path) throws IOException;
-
-    void rename(String oldPath, String newPath) throws IOException;
-
-    void rename(String oldPath, String newPath, CopyMode... options) throws IOException;
-
-    int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException;
-
-    void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException;
-
-    void mkdir(String path) throws IOException;
-
-    void rmdir(String path) throws IOException;
-
-    Handle openDir(String path) throws IOException;
-
-    DirEntry[] readDir(Handle handle) throws IOException;
-
-    String canonicalPath(String canonical) throws IOException;
-
-    Attributes stat(String path) throws IOException;
-
-    Attributes lstat(String path) throws IOException;
-
-    Attributes stat(Handle handle) throws IOException;
-
-    void setStat(String path, Attributes attributes) throws IOException;
-
-    void setStat(Handle handle, Attributes attributes) throws IOException;
-
-    String readLink(String path) throws IOException;
-
-    void symLink(String linkPath, String targetPath) throws IOException;
-
-    void link(String linkPath, String targetPath, boolean symbolic) throws IOException;
-
-    void lock(Handle handle, long offset, long length, int mask) throws IOException;
-
-    void unlock(Handle handle, long offset, long length) throws IOException;
-
-    //
-    // High level API
-    //
-
-    Iterable<DirEntry> readDir(String path) throws IOException;
-
-    InputStream read(String path) throws IOException;
-
-    InputStream read(String path, Collection<OpenMode> mode) throws IOException;
-
-    OutputStream write(String path) throws IOException;
-
-    OutputStream write(String path, Collection<OpenMode> mode) throws IOException;
-
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index 6761a88..c9a1656 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -32,7 +32,6 @@ import org.apache.sshd.ClientSession;
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.ScpClient;
 import org.apache.sshd.client.ServerKeyVerifier;
-import org.apache.sshd.client.SftpClient;
 import org.apache.sshd.client.UserInteraction;
 import org.apache.sshd.client.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.channel.ChannelExec;
@@ -42,6 +41,7 @@ import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.future.DefaultAuthFuture;
 import org.apache.sshd.client.scp.DefaultScpClient;
 import org.apache.sshd.client.sftp.DefaultSftpClient;
+import org.apache.sshd.client.sftp.SftpClient;
 import org.apache.sshd.client.sftp.SftpFileSystem;
 import org.apache.sshd.client.sftp.SftpFileSystemProvider;
 import org.apache.sshd.common.NamedResource;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/sftp/AbstractSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/AbstractSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/AbstractSftpClient.java
new file mode 100644
index 0000000..b1533c1
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/AbstractSftpClient.java
@@ -0,0 +1,93 @@
+/*
+ * 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.client.sftp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.EnumSet;
+
+import org.apache.sshd.common.util.AbstractLoggingBean;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractSftpClient extends AbstractLoggingBean implements SftpClient {
+    protected AbstractSftpClient() {
+        super();
+    }
+    
+    @Override
+    public CloseableHandle open(String path) throws IOException {
+        return open(path, Collections.<OpenMode>emptySet());
+    }
+    
+    @Override
+    public CloseableHandle open(String path, OpenMode ... options) throws IOException {
+        return open(path, GenericUtils.of(options));
+    }
+
+    @Override
+    public void rename(String oldPath, String newPath) throws IOException {
+        rename(oldPath, newPath, Collections.<CopyMode>emptySet());
+    }
+    
+    @Override
+    public void rename(String oldPath, String newPath, CopyMode ... options) throws IOException {
+        rename(oldPath, newPath, GenericUtils.of(options));
+    }
+
+    @Override
+    public InputStream read(final String path) throws IOException {
+        return read(path, EnumSet.of(OpenMode.Read));
+    }
+
+    @Override
+    public InputStream read(String path, OpenMode ... mode) throws IOException {
+        return read(path, GenericUtils.of(mode));
+    }
+
+    @Override
+    public int read(Handle handle, long fileOffset, byte[] dst) throws IOException {
+        return read(handle, fileOffset, dst, 0, dst.length);
+    }
+
+    @Override
+    public OutputStream write(final String path) throws IOException {
+        return write(path, EnumSet.of(OpenMode.Write, OpenMode.Create, OpenMode.Truncate));
+    }
+
+    @Override
+    public OutputStream write(String path, OpenMode ... mode) throws IOException {
+        return write(path, GenericUtils.of(mode));
+    }
+
+    @Override
+    public void write(Handle handle, long fileOffset, byte[] src) throws IOException {
+        write(handle, fileOffset, src, 0, src.length);
+    }
+
+    @Override
+    public void symLink(String linkPath, String targetPath) throws IOException {
+        link(linkPath, targetPath, true);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultCloseableHandle.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultCloseableHandle.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultCloseableHandle.java
new file mode 100644
index 0000000..f86c47e
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultCloseableHandle.java
@@ -0,0 +1,55 @@
+/*
+ * 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.client.sftp;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sshd.client.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultCloseableHandle extends CloseableHandle {
+    private final AtomicBoolean open = new AtomicBoolean(true);
+    private final SftpClient client;
+
+    public DefaultCloseableHandle(SftpClient client, String id) {
+        super(id);
+        this.client = ValidateUtils.checkNotNull(client, "No client for id=%s", id);
+    }
+
+    public final SftpClient getSftpClient() {
+        return client;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return open.get();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (open.getAndSet(false)) {
+            client.close(this);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
index 19e37b6..3ee81c2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
@@ -97,7 +97,6 @@ import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.attribute.FileTime;
 import java.util.Collection;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
@@ -105,20 +104,23 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.ClientSession;
-import org.apache.sshd.client.SftpClient;
 import org.apache.sshd.client.SftpException;
 import org.apache.sshd.client.channel.ChannelSubsystem;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.util.AbstractLoggingBean;
+import org.apache.sshd.common.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.InputStreamWithChannel;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseOutputStream;
 import org.apache.sshd.common.util.io.OutputStreamWithChannel;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient {
+public class DefaultSftpClient extends AbstractSftpClient {
     private final ClientSession clientSession;
     private final ChannelSubsystem channel;
     private final Map<Integer, Buffer> messages;
@@ -130,7 +132,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
 
     public DefaultSftpClient(ClientSession clientSession) throws IOException {
         this.clientSession = clientSession;
-        this.channel = clientSession.createSubsystemChannel("sftp");
+        this.channel = clientSession.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
         this.messages = new HashMap<>();
         try {
             this.channel.setOut(new OutputStream() {
@@ -146,7 +148,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             this.channel.setErr(new ByteArrayOutputStream());
             this.channel.open().await();
         } catch (InterruptedException e) {
-            throw (IOException) new InterruptedIOException().initCause(e);
+            throw (IOException) new InterruptedIOException("Interrupted while await channel open").initCause(e);
         }
         this.channel.onClose(new Runnable() {
             @SuppressWarnings("synthetic-access")
@@ -173,7 +175,9 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
 
     @Override
     public void close() throws IOException {
-        this.channel.close(false);
+        if (this.channel.isOpen()) {
+            this.channel.close(false);
+        }
     }
 
     /**
@@ -238,15 +242,17 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
         }
     }
 
-
     protected int send(int cmd, Buffer buffer) throws IOException {
         int id = cmdId.incrementAndGet();
-        DataOutputStream dos = new DataOutputStream(channel.getInvertedIn());
-        dos.writeInt(5 + buffer.available());
-        dos.writeByte(cmd);
-        dos.writeInt(id);
-        dos.write(buffer.array(), buffer.rpos(), buffer.available());
-        dos.flush();
+        
+        try(DataOutputStream dos = new DataOutputStream(new NoCloseOutputStream(channel.getInvertedIn()))) {
+            dos.writeInt(5 + buffer.available());
+            dos.writeByte(cmd);
+            dos.writeInt(id);
+            dos.write(buffer.array(), buffer.rpos(), buffer.available());
+            dos.flush();
+        }
+
         return id;
     }
 
@@ -270,32 +276,36 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
     }
 
     protected Buffer read() throws IOException {
-        DataInputStream dis = new DataInputStream(channel.getInvertedOut());
-        int length = dis.readInt();
-        if (length < 5) {
-            throw new IllegalArgumentException("Bad length: " + length);
-        }
-        Buffer buffer = new ByteArrayBuffer(length + 4);
-        buffer.putInt(length);
-        int nb = length;
-        while (nb > 0) {
-            int l = dis.read(buffer.array(), buffer.wpos(), nb);
-            if (l < 0) {
-                throw new IllegalArgumentException("Premature EOF while read " + length + " bytes - remaining=" + nb);
-            }
-            buffer.wpos(buffer.wpos() + l);
-            nb -= l;
+        try(DataInputStream dis=new DataInputStream(new NoCloseInputStream(channel.getInvertedOut()))) {
+            int length = dis.readInt();
+            if (length < 5) {
+                throw new IllegalArgumentException("Bad length: " + length);
+            }
+            Buffer buffer = new ByteArrayBuffer(length + 4);
+            buffer.putInt(length);
+            int nb = length;
+            while (nb > 0) {
+                int readLen = dis.read(buffer.array(), buffer.wpos(), nb);
+                if (readLen < 0) {
+                    throw new IllegalArgumentException("Premature EOF while read " + length + " bytes - remaining=" + nb);
+                }
+                buffer.wpos(buffer.wpos() + readLen);
+                nb -= readLen;
+            }
+
+            return buffer;
         }
-        return buffer;
     }
 
     protected void init() throws IOException {
         // Init packet
-        DataOutputStream dos = new DataOutputStream(channel.getInvertedIn());
-        dos.writeInt(5);
-        dos.writeByte(SSH_FXP_INIT);
-        dos.writeInt(SFTP_V6);
-        dos.flush();
+        try(DataOutputStream dos = new DataOutputStream(new NoCloseOutputStream(channel.getInvertedIn()))) {
+            dos.writeInt(5);
+            dos.writeByte(SSH_FXP_INIT);
+            dos.writeInt(SFTP_V6);
+            dos.flush();
+        }
+
         Buffer buffer;
         synchronized (messages) {
             while (messages.isEmpty()) {
@@ -355,7 +365,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
         }
     }
 
-    protected Handle checkHandle(Buffer buffer) throws IOException {
+    protected String checkHandle(Buffer buffer) throws IOException {
         int length = buffer.getInt();
         int type = buffer.getByte();
         int id = buffer.getInt();
@@ -368,8 +378,8 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             }
             throw new SftpException(substatus, msg);
         } else if (type == SSH_FXP_HANDLE) {
-            String handle = buffer.getString();
-            return new Handle(handle);
+            String handle = ValidateUtils.checkNotNullAndNotEmpty(buffer.getString(), "Null/empty handle in buffer", GenericUtils.EMPTY_OBJECT_ARRAY);
+            return handle;
         } else {
             throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
         }
@@ -508,7 +518,6 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
         return FileTime.from(millis, TimeUnit.MILLISECONDS);
     }
 
-
     protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
         if (version == SFTP_V3) {
             int flags = 0;
@@ -615,8 +624,8 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
     }
 
     @Override
-    public Handle open(String path, Collection<OpenMode> options) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+    public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
         if (version == SFTP_V3) {
             int mode = 0;
@@ -673,56 +682,56 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             buffer.putInt(mode);
         }
         writeAttributes(buffer, new Attributes());
-        return checkHandle(receive(send(SSH_FXP_OPEN, buffer)));
+        return new DefaultCloseableHandle(this, checkHandle(receive(send(SSH_FXP_OPEN, buffer))));
     }
 
     @Override
     public void close(Handle handle) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
         buffer.putString(handle.id);
         checkStatus(receive(send(SSH_FXP_CLOSE, buffer)));
     }
 
     @Override
     public void remove(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
         checkStatus(receive(send(SSH_FXP_REMOVE, buffer)));
     }
 
     @Override
-    public void rename(String oldPath, String newPath) throws IOException {
-        rename(oldPath, newPath, new CopyMode[0]);
-    }
-
-    @Override
-    public void rename(String oldPath, String newPath, CopyMode... options) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+    public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */);
         buffer.putString(oldPath);
         buffer.putString(newPath);
+        
+        int numOptions = GenericUtils.size(options);
         if (version >= SFTP_V5) {
             int opts = 0;
-            for (CopyMode opt : options) {
-                switch (opt) {
-                    case Atomic:
-                        opts |= SSH_FXP_RENAME_ATOMIC;
-                        break;
-                    case Overwrite:
-                        opts |= SSH_FXP_RENAME_OVERWRITE;
-                        break;
-                    default:    // do nothing
+            if (numOptions > 0) {
+                for (CopyMode opt : options) {
+                    switch (opt) {
+                        case Atomic:
+                            opts |= SSH_FXP_RENAME_ATOMIC;
+                            break;
+                        case Overwrite:
+                            opts |= SSH_FXP_RENAME_OVERWRITE;
+                            break;
+                        default:    // do nothing
+                    }
                 }
             }
             buffer.putInt(opts);
-        } else if (options.length > 0) {
-            throw new UnsupportedOperationException("copy options can not be used with this SFTP version");
+        } else if (numOptions > 0) {
+            throw new UnsupportedOperationException("rename(" + oldPath + " => " + newPath + ")"
+                                                  + " - copy options can not be used with this SFTP version: " + options);
         }
         checkStatus(receive(send(SSH_FXP_RENAME, buffer)));
     }
 
     @Override
     public int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
         buffer.putString(handle.id);
         buffer.putLong(fileOffset);
         buffer.putInt(len);
@@ -758,13 +767,18 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
     @Override
     public void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException {
         // do some bounds checking first
-        if (fileOffset < 0 || srcoff < 0 || len < 0) {
-            throw new IllegalArgumentException("please ensure all parameters are non-negative values");
+        if ((fileOffset < 0) || (srcoff < 0) || (len < 0)) {
+            throw new IllegalArgumentException("write(" + handle + ") please ensure all parameters "
+                                             + " are non-negative values: file-offset=" + fileOffset
+                                             + ", src-offset=" + srcoff + ", len=" + len);
         }
-        if (srcoff + len > src.length) {
-            throw new IllegalArgumentException("cannot read bytes " + srcoff + " to " + (srcoff + len) + " when array is only of length " + src.length);
+        if ((srcoff + len) > src.length) {
+            throw new IllegalArgumentException("write(" + handle + ")"
+                                             + " cannot read bytes " + srcoff + " to " + (srcoff + len)
+                                             + " when array is only of length " + src.length);
         }
-        Buffer buffer = new ByteArrayBuffer();
+
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + len + Long.SIZE /* some extra fields */);
         buffer.putString(handle.id);
         buffer.putLong(fileOffset);
         buffer.putBytes(src, srcoff, len);
@@ -773,7 +787,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
 
     @Override
     public void mkdir(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+        Buffer buffer = new ByteArrayBuffer(path.length() +  Long.SIZE /* some extra fields */);
         buffer.putString(path, StandardCharsets.UTF_8);
         buffer.putInt(0);
         if (version != SFTP_V3) {
@@ -784,21 +798,21 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
 
     @Override
     public void rmdir(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+        Buffer buffer = new ByteArrayBuffer(path.length() +  Long.SIZE /* some extra fields */);
         buffer.putString(path);
         checkStatus(receive(send(SSH_FXP_RMDIR, buffer)));
     }
 
     @Override
-    public Handle openDir(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+    public CloseableHandle openDir(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
-        return checkHandle(receive(send(SSH_FXP_OPENDIR, buffer)));
+        return new DefaultCloseableHandle(this, checkHandle(receive(send(SSH_FXP_OPENDIR, buffer))));
     }
 
     @Override
     public DirEntry[] readDir(Handle handle) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
         buffer.putString(handle.id);
         return checkDir(receive(send(SSH_FXP_READDIR, buffer)));
     }
@@ -892,28 +906,22 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
 
     @Override
     public String readLink(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
         buffer.putString(path);
         return checkOneName(receive(send(SSH_FXP_READLINK, buffer)));
     }
 
     @Override
-    public void symLink(String linkPath, String targetPath) throws IOException {
-        link(linkPath, targetPath, true);
-    }
-
-    @Override
     public void link(String linkPath, String targetPath, boolean symbolic) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */);
         if (version < SFTP_V6) {
             if (!symbolic) {
                 throw new UnsupportedOperationException("Hard links are not supported in sftp v" + version);
             }
-            Buffer buffer = new ByteArrayBuffer();
             buffer.putString(targetPath);
             buffer.putString(linkPath);
             checkStatus(receive(send(SSH_FXP_SYMLINK, buffer)));
         } else {
-            Buffer buffer = new ByteArrayBuffer();
             buffer.putString(targetPath);
             buffer.putString(linkPath);
             buffer.putBoolean(symbolic);
@@ -946,17 +954,20 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             @Override
             public Iterator<DirEntry> iterator() {
                 return new Iterator<DirEntry>() {
-                    Handle handle;
-                    DirEntry[] entries;
-                    int index;
+                    private CloseableHandle handle;
+                    private DirEntry[] entries;
+                    private int index;
+
                     {
                         open();
                         load();
                     }
+
                     @Override
                     public boolean hasNext() {
-                        return entries != null && index < entries.length;
+                        return (entries != null) && (index < entries.length);
                     }
+
                     @Override
                     public DirEntry next() {
                         DirEntry entry = entries[index++];
@@ -965,30 +976,45 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
                         }
                         return entry;
                     }
+
+                    @SuppressWarnings("synthetic-access")
                     private void open() {
                         try {
                             handle = openDir(path);
+                            if (log.isDebugEnabled()) {
+                                log.debug("readDir(" + path + ") handle=" + handle);
+                            }
                         } catch (IOException e) {
+                            if (log.isDebugEnabled()) {
+                                log.debug("readDir(" + path + ") failed (" + e.getClass().getSimpleName() + ") to open dir: " + e.getMessage());
+                            }
                             throw new RuntimeException(e);
                         }
                     }
+
+                    @SuppressWarnings("synthetic-access")
                     private void load() {
                         try {
                             entries = readDir(handle);
                             index = 0;
                             if (entries == null) {
-                                close(handle);
+                                handle.close();
                             }
                         } catch (IOException e) {
                             entries = null;
                             try {
-                                close(handle);
+                                handle.close();
                             } catch (IOException t) {
-                                // Ignore
+                                if (log.isTraceEnabled()) {
+                                    log.trace(t.getClass().getSimpleName() + " while close handle=" + handle
+                                            + " due to " + e.getClass().getSimpleName() + " [" + e.getMessage() + "]"
+                                            + ": " + t.getMessage());
+                                }
                             }
                             throw new RuntimeException(e);
                         }
                     }
+
                     @Override
                     public void remove() {
                         throw new UnsupportedOperationException("readDir(" + path + ") Iterator#remove() N/A");
@@ -999,23 +1025,18 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
     }
 
     @Override
-    public InputStream read(final String path) throws IOException {
-        return read(path, EnumSet.of(OpenMode.Read));
-    }
-
-    @Override
     public InputStream read(final String path, final Collection<OpenMode> mode) throws IOException {
         return new InputStreamWithChannel() {
             private byte[] bb = new byte[1];
             private byte[] buffer = new byte[32 * 1024];
             private int index;
             private int available;
-            private Handle handle = DefaultSftpClient.this.open(path, mode);
+            private CloseableHandle handle = DefaultSftpClient.this.open(path, mode);
             private long offset;
 
             @Override
             public boolean isOpen() {
-                return handle != null;
+                return (handle != null) && handle.isOpen();
             }
 
             @Override
@@ -1031,7 +1052,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             @Override
             public int read(byte[] b, int off, int len) throws IOException {
                 if (!isOpen()) {
-                    throw new IOException("Stream closed");
+                    throw new IOException("read(" + path + ") stream closed");
                 }
 
                 int idx = off;
@@ -1064,7 +1085,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             public void close() throws IOException {
                 if (isOpen()) {
                     try {
-                        DefaultSftpClient.this.close(handle);
+                        handle.close();
                     } finally {
                         handle = null;
                     }
@@ -1074,22 +1095,17 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
     }
 
     @Override
-    public OutputStream write(final String path) throws IOException {
-        return write(path, EnumSet.of(OpenMode.Write, OpenMode.Create, OpenMode.Truncate));
-    }
-
-    @Override
     public OutputStream write(final String path, final Collection<OpenMode> mode) throws IOException {
         return new OutputStreamWithChannel() {
             private byte[] bb = new byte[1];
             private byte[] buffer = new byte[32 * 1024];
             private int index;
-            private Handle handle = DefaultSftpClient.this.open(path, mode);
+            private CloseableHandle handle = DefaultSftpClient.this.open(path, mode);
             private long offset;
 
             @Override
             public boolean isOpen() {
-                return handle != null;
+                return (handle != null) && handle.isOpen();
             }
 
             @Override
@@ -1101,7 +1117,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             @Override
             public void write(byte[] b, int off, int len) throws IOException {
                 if (!isOpen()) {
-                    throw new IOException("write(len=" + len + ") Stream is closed");
+                    throw new IOException("write(" + path + ")[len=" + len + "] stream is closed");
                 }
 
                 do {
@@ -1119,7 +1135,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
             @Override
             public void flush() throws IOException {
                 if (!isOpen()) {
-                    throw new IOException("flush() Stream is closed");
+                    throw new IOException("flush(" + path + ") stream is closed");
                 }
 
                 DefaultSftpClient.this.write(handle, offset, buffer, 0, index);
@@ -1136,7 +1152,7 @@ public class DefaultSftpClient extends AbstractLoggingBean implements SftpClient
                                 flush();
                             }
                         } finally {
-                            DefaultSftpClient.this.close(handle);
+                            handle.close();
                         }
                     } finally {
                         handle = null;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpClient.java
new file mode 100644
index 0000000..75483b7
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpClient.java
@@ -0,0 +1,289 @@
+/*
+ * 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.client.sftp;
+
+import static org.apache.sshd.common.sftp.SftpConstants.S_IFDIR;
+import static org.apache.sshd.common.sftp.SftpConstants.S_IFLNK;
+import static org.apache.sshd.common.sftp.SftpConstants.S_IFMT;
+import static org.apache.sshd.common.sftp.SftpConstants.S_IFREG;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.Channel;
+import java.nio.file.attribute.FileTime;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="http://mina.apache.org">Apache MINA Project</a>
+ */
+public interface SftpClient extends Closeable {
+
+    enum OpenMode {
+        Read,
+        Write,
+        Append,
+        Create,
+        Truncate,
+        Exclusive
+    }
+
+    enum CopyMode {
+        Atomic,
+        Overwrite
+    }
+
+    enum Attribute {
+        Size,
+        UidGid,
+        Perms,
+        AcModTime,
+        OwnerGroup,
+        AccessTime,
+        ModifyTime,
+        CreateTime,
+    }
+
+    public static class Handle {
+        public final String id;
+        public Handle(String id) {
+            this.id = ValidateUtils.checkNotNullAndNotEmpty(id, "No handle ID", GenericUtils.EMPTY_OBJECT_ARRAY);
+        }
+        
+        @Override
+        public String toString() {
+            return id;
+        }
+    }
+
+    public static abstract class CloseableHandle extends Handle implements Channel, Closeable {
+        protected CloseableHandle(String id) {
+            super(id);
+        }
+    }
+
+    public static class Attributes {
+        public final Set<Attribute> flags = EnumSet.noneOf(Attribute.class);
+        public long size;
+        public byte type;
+        public int uid;
+        public int gid;
+        public int perms;
+        public int atime;
+        public int ctime;
+        public int mtime;
+        public String owner;
+        public String group;
+        public FileTime accessTime;
+        public FileTime createTime;
+        public FileTime modifyTime;
+
+        @Override
+        public String toString() {
+            return "type=" + type
+                 + ";size=" + size
+                 + ";uid=" + uid
+                 + ";gid=" + gid
+                 + ";perms=0x" + Integer.toHexString(perms)
+                 + ";flags=" + flags
+                 + ";owner=" + owner
+                 + ";group=" + group
+                 + ";aTime=(" + atime + ")[" + accessTime + "]"
+                 + ";cTime=(" + ctime + ")[" + createTime + "]"
+                 + ";mTime=(" + mtime + ")[" + modifyTime + "]"
+                 ;
+        }
+
+        public Attributes size(long size) {
+            flags.add(Attribute.Size);
+            this.size = size;
+            return this;
+        }
+        public Attributes owner(String owner) {
+            flags.add(Attribute.OwnerGroup);
+            this.owner = owner;
+            if (GenericUtils.isEmpty(group)) {
+                group = "GROUP@";
+            }
+            return this;
+        }
+        public Attributes group(String group) {
+            flags.add(Attribute.OwnerGroup);
+            this.group = group;
+            if (GenericUtils.isEmpty(owner)) {
+                owner = "OWNER@";
+            }
+            return this;
+        }
+        public Attributes owner(int uid, int gid) {
+            flags.add(Attribute.UidGid);
+            this.uid = uid;
+            this.gid = gid;
+            return this;
+        }
+        public Attributes perms(int perms) {
+            flags.add(Attribute.Perms);
+            this.perms = perms;
+            return this;
+        }
+        public Attributes atime(int atime) {
+            flags.add(Attribute.AccessTime);
+            this.atime = atime;
+            this.accessTime = FileTime.from(atime, TimeUnit.SECONDS);
+            return this;
+        }
+        public Attributes ctime(int ctime) {
+            flags.add(Attribute.CreateTime);
+            this.ctime = ctime;
+            this.createTime = FileTime.from(atime, TimeUnit.SECONDS);
+            return this;
+        }
+        public Attributes mtime(int mtime) {
+            flags.add(Attribute.ModifyTime);
+            this.mtime = mtime;
+            this.modifyTime = FileTime.from(atime, TimeUnit.SECONDS);
+            return this;
+        }
+        public Attributes time(int atime, int mtime) {
+            flags.add(Attribute.AcModTime);
+            this.atime = atime;
+            this.mtime = mtime;
+            return this;
+        }
+        public Attributes accessTime(FileTime atime) {
+            flags.add(Attribute.AccessTime);
+            this.atime = (int) atime.to(TimeUnit.SECONDS);
+            this.accessTime = atime;
+            return this;
+        }
+        public Attributes createTime(FileTime ctime) {
+            flags.add(Attribute.CreateTime);
+            this.ctime = (int) ctime.to(TimeUnit.SECONDS);
+            this.createTime = ctime;
+            return this;
+        }
+        public Attributes modifyTime(FileTime mtime) {
+            flags.add(Attribute.ModifyTime);
+            this.mtime = (int) mtime.to(TimeUnit.SECONDS);
+            this.modifyTime = mtime;
+            return this;
+        }
+        public boolean isRegularFile() {
+            return (perms & S_IFMT) == S_IFREG;
+        }
+        public boolean isDirectory() {
+            return (perms & S_IFMT) == S_IFDIR;
+        }
+        public boolean isSymbolicLink() {
+            return (perms & S_IFMT) == S_IFLNK;
+        }
+        public boolean isOther() {
+            return !isRegularFile() && !isDirectory() && !isSymbolicLink();
+        }
+    }
+
+    public static class DirEntry {
+        public String filename;
+        public String longFilename;
+        public Attributes attributes;
+        public DirEntry(String filename, String longFilename, Attributes attributes) {
+            this.filename = filename;
+            this.longFilename = longFilename;
+            this.attributes = attributes;
+        }
+    }
+
+    int getVersion();
+
+    boolean isClosing();
+
+    //
+    // Low level API
+    //
+
+    CloseableHandle open(String path) throws IOException;
+    CloseableHandle open(String path, OpenMode ... options) throws IOException;
+    CloseableHandle open(String path, Collection<OpenMode> options) throws IOException;
+
+    void close(Handle handle) throws IOException;
+
+    void remove(String path) throws IOException;
+
+    void rename(String oldPath, String newPath) throws IOException;
+    void rename(String oldPath, String newPath, CopyMode... options) throws IOException;
+    void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException;
+
+    int read(Handle handle, long fileOffset, byte[] dst) throws IOException;
+    int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException;
+
+    void write(Handle handle, long fileOffset, byte[] src) throws IOException;
+    void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException;
+
+    void mkdir(String path) throws IOException;
+
+    void rmdir(String path) throws IOException;
+
+    CloseableHandle openDir(String path) throws IOException;
+
+    DirEntry[] readDir(Handle handle) throws IOException;
+
+    String canonicalPath(String canonical) throws IOException;
+
+    Attributes stat(String path) throws IOException;
+
+    Attributes lstat(String path) throws IOException;
+
+    Attributes stat(Handle handle) throws IOException;
+
+    void setStat(String path, Attributes attributes) throws IOException;
+
+    void setStat(Handle handle, Attributes attributes) throws IOException;
+
+    String readLink(String path) throws IOException;
+
+    void symLink(String linkPath, String targetPath) throws IOException;
+
+    void link(String linkPath, String targetPath, boolean symbolic) throws IOException;
+
+    void lock(Handle handle, long offset, long length, int mask) throws IOException;
+
+    void unlock(Handle handle, long offset, long length) throws IOException;
+
+    //
+    // High level API
+    //
+
+    Iterable<DirEntry> readDir(String path) throws IOException;
+
+    InputStream read(String path) throws IOException;
+    InputStream read(String path, OpenMode ... mode) throws IOException;
+    InputStream read(String path, Collection<OpenMode> mode) throws IOException;
+
+    OutputStream write(String path) throws IOException;
+    OutputStream write(String path, OpenMode ... mode) throws IOException;
+    OutputStream write(String path, Collection<OpenMode> mode) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileChannel.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileChannel.java
index dc61005..3d9244c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileChannel.java
@@ -37,7 +37,6 @@ import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import org.apache.sshd.client.SftpClient;
 import org.apache.sshd.client.SftpException;
 
 public class SftpFileChannel extends FileChannel {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java
index 574d282..dfc88f4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java
@@ -34,7 +34,6 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.ClientSession;
-import org.apache.sshd.client.SftpClient;
 import org.apache.sshd.common.file.util.BaseFileSystem;
 import org.apache.sshd.common.file.util.ImmutableList;
 
@@ -110,7 +109,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         return defaultDir;
     }
 
-    private class Wrapper implements SftpClient {
+    private class Wrapper extends AbstractSftpClient {
 
         private final SftpClient delegate;
         private final AtomicInteger count = new AtomicInteger(1);
@@ -145,7 +144,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         }
 
         @Override
-        public Handle open(String path, Collection<OpenMode> options) throws IOException {
+        public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
             return delegate.open(path, options);
         }
 
@@ -160,12 +159,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         }
 
         @Override
-        public void rename(String oldPath, String newPath) throws IOException {
-            delegate.rename(oldPath, newPath);
-        }
-
-        @Override
-        public void rename(String oldPath, String newPath, CopyMode... options) throws IOException {
+        public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException {
             delegate.rename(oldPath, newPath, options);
         }
 
@@ -190,7 +184,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         }
 
         @Override
-        public Handle openDir(String path) throws IOException {
+        public CloseableHandle openDir(String path) throws IOException {
             return delegate.openDir(path);
         }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java
index 4e3e9b8..fc53d04 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java
@@ -75,14 +75,14 @@ import java.util.concurrent.TimeUnit;
 import org.apache.sshd.ClientSession;
 import org.apache.sshd.SshBuilder;
 import org.apache.sshd.SshClient;
-import org.apache.sshd.client.SftpClient;
-import org.apache.sshd.client.SftpClient.Attributes;
+import org.apache.sshd.client.sftp.SftpClient.Attributes;
 import org.apache.sshd.client.SftpException;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.config.SshConfigFileReader;
 import org.apache.sshd.common.sftp.SftpConstants;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.IoUtils;
+import org.apache.sshd.server.sftp.SftpSubsystemFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -107,7 +107,7 @@ public class SftpFileSystemProvider extends FileSystemProvider {
 
     @Override
     public String getScheme() {
-        return "sftp";
+        return SftpConstants.SFTP_SUBSYSTEM_NAME;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/common/sftp/SftpConstants.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/sftp/SftpConstants.java b/sshd-core/src/main/java/org/apache/sshd/common/sftp/SftpConstants.java
index 80d0667..dd54bf3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/sftp/SftpConstants.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/sftp/SftpConstants.java
@@ -22,6 +22,7 @@ package org.apache.sshd.common.sftp;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class SftpConstants {
+    public static String SFTP_SUBSYSTEM_NAME = "sftp";
 
     public static final int SSH_FXP_INIT =             1;
     public static final int SSH_FXP_VERSION =          2;
@@ -219,5 +220,4 @@ public class SftpConstants {
     public static int SFTP_V4 = 4;
     public static int SFTP_V5 = 5;
     public static int SFTP_V6 = 6;
-
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystemFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystemFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystemFactory.java
index e3a59c6..04d0239 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystemFactory.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystemFactory.java
@@ -22,6 +22,7 @@ package org.apache.sshd.server.sftp;
 import java.util.concurrent.ExecutorService;
 
 import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.sftp.SftpConstants;
 import org.apache.sshd.common.util.ObjectBuilder;
 import org.apache.sshd.common.util.threads.ExecutorServiceConfigurer;
 import org.apache.sshd.server.Command;
@@ -30,7 +31,7 @@ import org.apache.sshd.server.Command;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class SftpSubsystemFactory implements NamedFactory<Command>, Cloneable, ExecutorServiceConfigurer {
-    public static final String NAME = "sftp";
+    public static final String NAME = SftpConstants.SFTP_SUBSYSTEM_NAME;
     public static final UnsupportedAttributePolicy DEFAULT_POLICY = UnsupportedAttributePolicy.Warn;
 
     public static class Builder implements ObjectBuilder<SftpSubsystemFactory> {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/ClientTest.java b/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
index ba0e313..121de96 100644
--- a/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
@@ -66,6 +66,7 @@ import org.apache.sshd.common.io.nio2.Nio2Session;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.AbstractSession;
 import org.apache.sshd.common.session.ConnectionService;
+import org.apache.sshd.common.sftp.SftpConstants;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
@@ -904,7 +905,7 @@ public class ClientTest extends BaseTestSupport {
             session.auth().verify(5L, TimeUnit.SECONDS);
             session.switchToNoneCipher().await();
     
-            try(ClientChannel channel = session.createSubsystemChannel("sftp")) {
+            try(ClientChannel channel = session.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME)) {
                 channel.open().verify(5L, TimeUnit.SECONDS);
             }
         } finally {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/test/java/org/apache/sshd/client/sftp/DefaultCloseableHandleTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/sftp/DefaultCloseableHandleTest.java b/sshd-core/src/test/java/org/apache/sshd/client/sftp/DefaultCloseableHandleTest.java
new file mode 100644
index 0000000..43f6913
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/sftp/DefaultCloseableHandleTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.client.sftp;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sshd.client.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.client.sftp.SftpClient.Handle;
+import org.apache.sshd.util.BaseTestSupport;
+import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultCloseableHandleTest extends BaseTestSupport {
+    public DefaultCloseableHandleTest() {
+        super();
+    }
+
+    @Test
+    public void testChannelBehavior() throws IOException {
+        final String id = getCurrentTestName();
+        SftpClient client = Mockito.mock(SftpClient.class);
+        Mockito.doAnswer(new Answer<Void>() {
+                @Override
+                public Void answer(InvocationOnMock invocation) throws Throwable {
+                    Object[] args = invocation.getArguments();
+                    Handle handle = (Handle) args[0];
+                    assertEquals("Mismatched closing handle", id, handle.id);
+                    return null;
+                }
+            }).when(client).close(Matchers.any(Handle.class));
+
+        CloseableHandle handle = new DefaultCloseableHandle(client, id);
+        try {
+            assertTrue("Handle not initially open", handle.isOpen());
+        } finally {
+            handle.close();
+        }
+        assertFalse("Handle not marked as closed", handle.isOpen());
+        // make sure close was called
+        Mockito.verify(client).close(handle);
+    }
+
+    @Test
+    public void testCloseIdempotent() throws IOException {
+        SftpClient client = Mockito.mock(SftpClient.class);
+        final AtomicBoolean closeCalled = new AtomicBoolean(false);
+        Mockito.doAnswer(new Answer<Void>() {
+                @Override
+                public Void answer(InvocationOnMock invocation) throws Throwable {
+                    Object[] args = invocation.getArguments();
+                    assertFalse("Close already called on handle=" + args[0], closeCalled.getAndSet(true));
+                    return null;
+                }
+            }).when(client).close(Matchers.any(Handle.class));
+
+        CloseableHandle handle = new DefaultCloseableHandle(client, getCurrentTestName());
+        for (int index=0; index < Byte.SIZE; index++) {
+            handle.close();
+        }
+        
+        assertTrue("Close method not called", closeCalled.get());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpFileSystemTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpFileSystemTest.java b/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpFileSystemTest.java
index 3187f63..1acb676 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpFileSystemTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpFileSystemTest.java
@@ -46,6 +46,7 @@ import org.apache.sshd.common.NamedFactory;
 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.sftp.SftpConstants;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.command.ScpCommandFactory;
@@ -99,7 +100,7 @@ public class SftpFileSystemTest extends BaseTestSupport {
     @Test
     public void testFileSystem() throws IOException {
         Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
         Utils.deleteRecursive(lclSftp);
 
         try(FileSystem fs = FileSystems.newFileSystem(URI.create("sftp://x:x@localhost:" + port + "/"), Collections.<String,Object>emptyMap())) {
@@ -208,7 +209,7 @@ public class SftpFileSystemTest extends BaseTestSupport {
     @Test
     public void testAttributes() throws IOException {
         Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
         Utils.deleteRecursive(lclSftp);
 
         try (FileSystem fs = FileSystems.newFileSystem(URI.create("sftp://x:x@localhost:" + port + "/"), null)) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d7939e25/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpTest.java
index 9e9207e..fe9e917 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/sftp/SftpTest.java
@@ -43,11 +43,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.SftpClient;
 import org.apache.sshd.common.NamedFactory;
 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.sftp.SftpConstants;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.server.Command;
@@ -136,7 +136,7 @@ public class SftpTest extends BaseTestSupport {
 
                 Path targetPath = detectTargetFolder().toPath();
                 Path parentPath = targetPath.getParent();
-                Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+                Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
                 Path clientFolder = lclSftp.resolve("client");
                 Path testFile = clientFolder.resolve(getCurrentTestName() + ".txt");
                 String file = Utils.resolveRelativeRemotePath(parentPath, testFile);
@@ -148,34 +148,27 @@ public class SftpTest extends BaseTestSupport {
                 javaFile.setReadable(false, false);
         
                 try (SftpClient sftp = session.createSftpClient()) {
-                    SftpClient.Handle h;
-            
                     boolean	isWindows = OsUtils.isWin32();
             
-                    try {
-                        h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Read));
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Read))) {
                         // NOTE: on Windows files are always readable
                         // see https://svn.apache.org/repos/asf/harmony/enhanced/java/branches/java6/classlib/modules/luni/src/test/api/windows/org/apache/harmony/luni/tests/java/io/WinFileTest.java
                         assertTrue("Empty read should have failed on " + file, isWindows);
-                        sftp.close(h);
                     } catch (IOException e) {
                         if (isWindows) {
                             throw e;
                         }
                     }
             
-                    try {
-                        h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write));
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write))) {
                         fail("Empty write should have failed on " + file);
                     } catch (IOException e) {
                         // ok
                     }
     
-                    try {
-                        h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Truncate));
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Truncate))) {
                         // NOTE: on Windows files are always readable
                         assertTrue("Empty truncate should have failed on " + file, isWindows);
-                        sftp.close(h);
                     } catch (IOException e) {
                         // ok
                     }
@@ -187,40 +180,28 @@ public class SftpTest extends BaseTestSupport {
             
                     javaFile.setWritable(true, false);
             
-                    h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Truncate, SftpClient.OpenMode.Write));
-                    sftp.close(h);
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Truncate, SftpClient.OpenMode.Write))) {
+                        // OK should succeed
+                        assertTrue("Handle not marked as open for file=" + file, h.isOpen());
+                    }
             
                     byte[] d = "0123456789\n".getBytes();
-                    h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write));
-                    try {
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write))) {
                         sftp.write(h, 0, d, 0, d.length);
                         sftp.write(h, d.length, d, 0, d.length);
-                    } finally {
-                        sftp.close(h);
                     }
 
-                    h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write));
-                    try {
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write))) {
                         sftp.write(h, d.length * 2, d, 0, d.length);
-                    } finally {
-                        sftp.close(h);
                     }
 
-                    h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write));
-                    try {
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write))) {
                         sftp.write(h, 3, "-".getBytes(), 0, 1);
-                    } finally {
-                        sftp.close(h);
                     }
 
-                    try {
-                        h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Read));
-                        try {
-                            // NOTE: on Windows files are always readable
-                            assertTrue("Data read should have failed on " + file, isWindows);
-                        } finally {
-                            sftp.close(h);
-                        }
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Read))) {
+                        // NOTE: on Windows files are always readable
+                        assertTrue("Data read should have failed on " + file, isWindows);
                     } catch (IOException e) {
                         if (isWindows) {
                             throw e;
@@ -230,12 +211,9 @@ public class SftpTest extends BaseTestSupport {
                     javaFile.setReadable(true, false);
             
                     byte[] buf = new byte[3];
-                    h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Read));
-                    try {
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Read))) {
                         int l = sftp.read(h, 2l, buf, 0, 3);
                         assertEquals("Mismatched read data", "2-4", new String(buf, 0, l));
-                    } finally {
-                        sftp.close(h);
                     }
                 }
             } finally {
@@ -254,7 +232,7 @@ public class SftpTest extends BaseTestSupport {
                 session.auth().verify(5L, TimeUnit.SECONDS);
 
                 Path targetPath = detectTargetFolder().toPath();
-                Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+                Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
                 Utils.deleteRecursive(lclSftp);
                 Files.createDirectories(lclSftp);
 
@@ -266,24 +244,20 @@ public class SftpTest extends BaseTestSupport {
                 try (SftpClient sftp = session.createSftpClient()) {
                     sftp.mkdir(dir);
             
-                    SftpClient.Handle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create));
-                    byte[] d = "0123456789\n".getBytes();
-                    sftp.write(h, 0, d, 0, d.length);
-                    sftp.write(h, d.length, d, 0, d.length);
-            
-                    SftpClient.Attributes attrs = sftp.stat(h);
-                    assertNotNull("No handle attributes", attrs);
-            
-                    sftp.close(h);
+                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) {
+                        byte[] d = "0123456789\n".getBytes();
+                        sftp.write(h, 0, d, 0, d.length);
+                        sftp.write(h, d.length, d, 0, d.length);
+                
+                        SftpClient.Attributes attrs = sftp.stat(h);
+                        assertNotNull("No handle attributes", attrs);
+                    }            
             
-                    h = sftp.openDir(dir);
-                    try {
+                    try(SftpClient.CloseableHandle h = sftp.openDir(dir)) {
                         SftpClient.DirEntry[] dirEntries = sftp.readDir(h);
                         assertNotNull("No dir entries", dirEntries);
                         assertEquals("Mismatced number of dir entries", 1, dirEntries.length);
                         assertNull("Unexpected entry read", sftp.readDir(h));
-                    } finally {
-                        sftp.close(h);
                     }
             
                     sftp.remove(file);
@@ -341,7 +315,7 @@ public class SftpTest extends BaseTestSupport {
                 session.auth().verify(5L, TimeUnit.SECONDS);
         
                 Path targetPath = detectTargetFolder().toPath();
-                Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+                Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
                 Utils.deleteRecursive(lclSftp);
                 Files.createDirectories(lclSftp);
 
@@ -381,8 +355,7 @@ public class SftpTest extends BaseTestSupport {
         // generate random file and upload it
         String randomData = randomString(5);
         byte[] randomBytes = randomData.getBytes();
-        SftpClient.Handle handle = sftp.open(filePath, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create));
-        try {
+        try(SftpClient.CloseableHandle handle = sftp.open(filePath, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) {
             try {
                 sftp.write(handle, -1, randomBytes, 0, 0);
                 fail("should not have been able to write file with invalid file offset for " + filePath);
@@ -413,9 +386,6 @@ public class SftpTest extends BaseTestSupport {
             } catch (IllegalArgumentException e) {
                 // expected
             }
-        } finally {
-            // cleanup
-            sftp.close(handle);
         }
 
         sftp.remove(filePath);
@@ -426,11 +396,8 @@ public class SftpTest extends BaseTestSupport {
         // generate random file and upload it
         String remotePath = remoteDir + "/" + filename;
         String randomData = randomString(size);
-        SftpClient.Handle handle = sftp.open(remotePath, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create));
-        try {
+        try(SftpClient.CloseableHandle handle = sftp.open(remotePath, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) {
             sftp.write(handle, 0, randomData.getBytes(), 0, randomData.length());
-        } finally {
-            sftp.close(handle);
         }
 
         // verify results
@@ -448,7 +415,7 @@ public class SftpTest extends BaseTestSupport {
         String d = getCurrentTestName() + "\n";
 
         Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
         Utils.deleteRecursive(lclSftp);
         Files.createDirectories(lclSftp);
 
@@ -475,7 +442,7 @@ public class SftpTest extends BaseTestSupport {
     @Test
     public void testReadWriteWithOffset() throws Exception {
         Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
         Utils.deleteRecursive(lclSftp);
         Files.createDirectories(lclSftp);
 
@@ -485,7 +452,7 @@ public class SftpTest extends BaseTestSupport {
         String extraData = "@" + getClass().getSimpleName();
         int appendOffset = -5;
 
-        ChannelSftp c = (ChannelSftp) session.openChannel("sftp");
+        ChannelSftp c = (ChannelSftp) session.openChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
         c.connect();
         try {
             c.put(new ByteArrayInputStream(data.getBytes()), remotePath);
@@ -509,7 +476,7 @@ public class SftpTest extends BaseTestSupport {
 
     @Test
     public void testReadDir() throws Exception {
-        ChannelSftp c = (ChannelSftp) session.openChannel("sftp");
+        ChannelSftp c = (ChannelSftp) session.openChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
         c.connect();
         try {
             URI url = getClass().getClassLoader().getResource(SshClient.class.getName().replace('.', '/') + ".class").toURI();
@@ -527,7 +494,7 @@ public class SftpTest extends BaseTestSupport {
 
     @Test
     public void testRealPath() throws Exception {
-        ChannelSftp c = (ChannelSftp) session.openChannel("sftp");
+        ChannelSftp c = (ChannelSftp) session.openChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
         c.connect();
 
         try {
@@ -559,7 +526,7 @@ public class SftpTest extends BaseTestSupport {
                 session.auth().verify(5L, TimeUnit.SECONDS);
         
                 Path targetPath = detectTargetFolder().toPath();
-                Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+                Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
                 Utils.deleteRecursive(lclSftp);
                 Files.createDirectories(lclSftp);
 
@@ -609,7 +576,7 @@ public class SftpTest extends BaseTestSupport {
         Assume.assumeTrue("Skip non-Unix O/S", OsUtils.isUNIX());
 
         Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, "sftp", getClass().getSimpleName());
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName());
         Utils.deleteRecursive(lclSftp);
         Files.createDirectories(lclSftp);
 
@@ -620,7 +587,7 @@ public class SftpTest extends BaseTestSupport {
         String remLinkPath = Utils.resolveRelativeRemotePath(parentPath, linkPath);
 
         String data = getCurrentTestName();
-        ChannelSftp c = (ChannelSftp) session.openChannel("sftp");
+        ChannelSftp c = (ChannelSftp) session.openChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
         c.connect();
         try {
             c.put(new ByteArrayInputStream(data.getBytes()), remSrcPath);
@@ -642,7 +609,7 @@ public class SftpTest extends BaseTestSupport {
     }
 
     protected String readFile(String path) throws Exception {
-        ChannelSftp c = (ChannelSftp) session.openChannel("sftp");
+        ChannelSftp c = (ChannelSftp) session.openChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
         c.connect();
         
         try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -661,7 +628,7 @@ public class SftpTest extends BaseTestSupport {
     }
 
     protected void sendFile(String path, String data) throws Exception {
-        ChannelSftp c = (ChannelSftp) session.openChannel("sftp");
+        ChannelSftp c = (ChannelSftp) session.openChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
         c.connect();
         try {
             c.put(new ByteArrayInputStream(data.getBytes()), path);