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 2017/12/28 09:06:46 UTC

[1/3] mina-sshd git commit: [SSHD-790] Allow users to register a custom SFTP client factory

Repository: mina-sshd
Updated Branches:
  refs/heads/master c5b163f2b -> 154287462


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClient.java
new file mode 100644
index 0000000..4118ff6
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClient.java
@@ -0,0 +1,462 @@
+/*
+ * 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.subsystem.sftp.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
+import java.net.SocketTimeoutException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.client.channel.ChannelSubsystem;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+import org.apache.sshd.common.subsystem.sftp.extensions.VersionsParser.Versions;
+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.BufferUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultSftpClient extends AbstractSftpClient {
+    private final ClientSession clientSession;
+    private final ChannelSubsystem channel;
+    private final Map<Integer, Buffer> messages = new HashMap<>();
+    private final AtomicInteger cmdId = new AtomicInteger(100);
+    private final Buffer receiveBuffer = new ByteArrayBuffer();
+    private final byte[] workBuf = new byte[Integer.BYTES];
+    private final AtomicInteger versionHolder = new AtomicInteger(0);
+    private final AtomicBoolean closing = new AtomicBoolean(false);
+    private final NavigableMap<String, byte[]> extensions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+    private final NavigableMap<String, byte[]> exposedExtensions = Collections.unmodifiableNavigableMap(extensions);
+    private Charset nameDecodingCharset = DEFAULT_NAME_DECODING_CHARSET;
+
+    public DefaultSftpClient(ClientSession clientSession) throws IOException {
+        this.nameDecodingCharset = PropertyResolverUtils.getCharset(clientSession, NAME_DECODING_CHARSET, DEFAULT_NAME_DECODING_CHARSET);
+        this.clientSession = Objects.requireNonNull(clientSession, "No client session");
+        this.channel = clientSession.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
+        this.channel.setOut(new OutputStream() {
+            private final byte[] singleByte = new byte[1];
+            @Override
+            public void write(int b) throws IOException {
+                synchronized (singleByte) {
+                    singleByte[0] = (byte) b;
+                    write(singleByte);
+                }
+            }
+
+            @Override
+            public void write(byte[] b, int off, int len) throws IOException {
+                data(b, off, len);
+            }
+        });
+        this.channel.setErr(new ByteArrayOutputStream(Byte.MAX_VALUE));
+
+        long initializationTimeout = clientSession.getLongProperty(SFTP_CHANNEL_OPEN_TIMEOUT, DEFAULT_CHANNEL_OPEN_TIMEOUT);
+        this.channel.open().verify(initializationTimeout);
+        this.channel.onClose(() -> {
+            synchronized (messages) {
+                closing.set(true);
+                messages.notifyAll();
+            }
+
+            if (versionHolder.get() <= 0) {
+                log.warn("onClose({}) closed before version negotiated", channel);
+            }
+        });
+
+        try {
+            init(initializationTimeout);
+        } catch (IOException | RuntimeException e) {
+            this.channel.close(true);
+            throw e;
+        }
+    }
+
+    @Override
+    public int getVersion() {
+        return versionHolder.get();
+    }
+
+    @Override
+    public ClientSession getClientSession() {
+        return clientSession;
+    }
+
+    @Override
+    public ClientChannel getClientChannel() {
+        return channel;
+    }
+
+    @Override
+    public NavigableMap<String, byte[]> getServerExtensions() {
+        return exposedExtensions;
+    }
+
+    @Override
+    public Charset getNameDecodingCharset() {
+        return nameDecodingCharset;
+    }
+
+    @Override
+    public void setNameDecodingCharset(Charset nameDecodingCharset) {
+        this.nameDecodingCharset = Objects.requireNonNull(nameDecodingCharset, "No charset provided");
+    }
+
+    @Override
+    public boolean isClosing() {
+        return closing.get();
+    }
+
+    @Override
+    public boolean isOpen() {
+        return this.channel.isOpen();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (isOpen()) {
+            this.channel.close(false);
+        }
+    }
+
+    /**
+     * Receive binary data
+     * @param buf   The buffer for the incoming data
+     * @param start Offset in buffer to place the data
+     * @param len   Available space in buffer for the data
+     * @return Actual size of received data
+     * @throws IOException If failed to receive incoming data
+     */
+    protected int data(byte[] buf, int start, int len) throws IOException {
+        Buffer incoming = new ByteArrayBuffer(buf, start, len);
+        // If we already have partial data, we need to append it to the buffer and use it
+        if (receiveBuffer.available() > 0) {
+            receiveBuffer.putBuffer(incoming);
+            incoming = receiveBuffer;
+        }
+
+        // Process commands
+        int rpos = incoming.rpos();
+        for (int count = 1; receive(incoming); count++) {
+            if (log.isTraceEnabled()) {
+                log.trace("data({}) Processed {} data messages", getClientChannel(), count);
+            }
+        }
+
+        int read = incoming.rpos() - rpos;
+        // Compact and add remaining data
+        receiveBuffer.compact();
+        if (receiveBuffer != incoming && incoming.available() > 0) {
+            receiveBuffer.putBuffer(incoming);
+        }
+
+        return read;
+    }
+
+    /**
+     * Read SFTP packets from buffer
+     *
+     * @param incoming The received {@link Buffer}
+     * @return {@code true} if data from incoming buffer was processed
+     * @throws IOException if failed to process the buffer
+     * @see #process(Buffer)
+     */
+    protected boolean receive(Buffer incoming) throws IOException {
+        int rpos = incoming.rpos();
+        int wpos = incoming.wpos();
+        ClientSession session = getClientSession();
+        session.resetIdleTimeout();
+
+        if ((wpos - rpos) > 4) {
+            int length = incoming.getInt();
+            if (length < 5) {
+                throw new IOException("Illegal sftp packet length: " + length);
+            }
+            if ((wpos - rpos) >= (length + 4)) {
+                incoming.rpos(rpos);
+                incoming.wpos(rpos + 4 + length);
+                process(incoming);
+                incoming.rpos(rpos + 4 + length);
+                incoming.wpos(wpos);
+                return true;
+            }
+        }
+        incoming.rpos(rpos);
+        return false;
+    }
+
+    /**
+     * Process an SFTP packet
+     *
+     * @param incoming The received {@link Buffer}
+     * @throws IOException if failed to process the buffer
+     */
+    protected void process(Buffer incoming) throws IOException {
+        // create a copy of the buffer in case it is being re-used
+        Buffer buffer = new ByteArrayBuffer(incoming.available() + Long.SIZE, false);
+        buffer.putBuffer(incoming);
+
+        int rpos = buffer.rpos();
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        Integer id = buffer.getInt();
+        buffer.rpos(rpos);
+
+        if (log.isTraceEnabled()) {
+            log.trace("process({}) id={}, type={}, len={}",
+                      getClientChannel(), id, SftpConstants.getCommandMessageName(type), length);
+        }
+
+        synchronized (messages) {
+            messages.put(id, buffer);
+            messages.notifyAll();
+        }
+    }
+
+    @Override
+    public int send(int cmd, Buffer buffer) throws IOException {
+        int id = cmdId.incrementAndGet();
+        int len = buffer.available();
+        if (log.isTraceEnabled()) {
+            log.trace("send({}) cmd={}, len={}, id={}",
+                      getClientChannel(), SftpConstants.getCommandMessageName(cmd), len, id);
+        }
+
+        OutputStream dos = channel.getInvertedIn();
+        BufferUtils.writeInt(dos, 1 /* cmd */ + Integer.BYTES /* id */ + len, workBuf);
+        dos.write(cmd & 0xFF);
+        BufferUtils.writeInt(dos, id, workBuf);
+        dos.write(buffer.array(), buffer.rpos(), len);
+        dos.flush();
+        return id;
+    }
+
+    @Override
+    public Buffer receive(int id) throws IOException {
+        Integer reqId = id;
+        synchronized (messages) {
+            for (int count = 1;; count++) {
+                if (isClosing() || (!isOpen())) {
+                    throw new SshException("Channel is being closed");
+                }
+
+                Buffer buffer = messages.remove(reqId);
+                if (buffer != null) {
+                    return buffer;
+                }
+
+                try {
+                    messages.wait();
+                } catch (InterruptedException e) {
+                    throw (IOException) new InterruptedIOException("Interrupted while waiting for messages at iteration #" + count).initCause(e);
+                }
+            }
+        }
+    }
+
+    protected Buffer read() throws IOException {
+        InputStream dis = channel.getInvertedOut();
+        int length = BufferUtils.readInt(dis, workBuf);
+        // must have at least command + length
+        if (length < (1 + Integer.BYTES)) {
+            throw new IllegalArgumentException("Bad length: " + length);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(length + Integer.BYTES, false);
+        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;
+    }
+
+    protected void init(long initializationTimeout) throws IOException {
+        ValidateUtils.checkTrue(initializationTimeout > 0L, "Invalid initialization timeout: %d", initializationTimeout);
+
+        // Send init packet
+        OutputStream dos = channel.getInvertedIn();
+        BufferUtils.writeInt(dos, 5 /* total length */, workBuf);
+        dos.write(SftpConstants.SSH_FXP_INIT);
+        BufferUtils.writeInt(dos, SftpConstants.SFTP_V6, workBuf);
+        dos.flush();
+
+        Buffer buffer;
+        Integer reqId;
+        synchronized (messages) {
+            /*
+             * We need to use a timeout since if the remote server does not support
+             * SFTP, we will not know it immediately. This is due to the fact that the
+             * request for the subsystem does not contain a reply as to its success or
+             * failure. Thus, the SFTP channel is created by the client, but there is
+             * no one on the other side to reply - thus the need for the timeout
+             */
+            for (long remainingTimeout = initializationTimeout; (remainingTimeout > 0L) && messages.isEmpty() && (!isClosing()) && isOpen();) {
+                try {
+                    long sleepStart = System.nanoTime();
+                    messages.wait(remainingTimeout);
+                    long sleepEnd = System.nanoTime();
+                    long sleepDuration = sleepEnd - sleepStart;
+                    long sleepMillis = TimeUnit.NANOSECONDS.toMillis(sleepDuration);
+                    if (sleepMillis < 1L) {
+                        remainingTimeout--;
+                    } else {
+                        remainingTimeout -= sleepMillis;
+                    }
+                } catch (InterruptedException e) {
+                    throw (IOException) new InterruptedIOException("Interrupted init()").initCause(e);
+                }
+            }
+
+            if (isClosing() || (!isOpen())) {
+                throw new EOFException("Closing while await init message");
+            }
+
+            if (messages.isEmpty()) {
+                throw new SocketTimeoutException("No incoming initialization response received within " + initializationTimeout + " msec.");
+            }
+
+            Collection<Integer> ids = messages.keySet();
+            Iterator<Integer> iter = ids.iterator();
+            reqId = iter.next();
+            buffer = messages.remove(reqId);
+        }
+
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (log.isTraceEnabled()) {
+            log.trace("init({}) id={} type={} len={}",
+                      getClientChannel(), id, SftpConstants.getCommandMessageName(type), length);
+        }
+
+        if (type == SftpConstants.SSH_FXP_VERSION) {
+            if (id < SftpConstants.SFTP_V3) {
+                throw new SshException("Unsupported sftp version " + id);
+            }
+            versionHolder.set(id);
+
+            if (log.isTraceEnabled()) {
+                log.trace("init({}) version={}", getClientChannel(), versionHolder);
+            }
+
+            while (buffer.available() > 0) {
+                String name = buffer.getString();
+                byte[] data = buffer.getBytes();
+                if (log.isTraceEnabled()) {
+                    log.trace("init({}) added extension=", getClientChannel(), name);
+                }
+                extensions.put(name, data);
+            }
+        } else if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("init({})[id={}] - status: {} [{}] {}",
+                          getClientChannel(), id, SftpConstants.getStatusName(substatus), lang, msg);
+            }
+
+            throwStatusException(SftpConstants.SSH_FXP_INIT, id, substatus, msg, lang);
+        } else {
+            handleUnexpectedPacket(SftpConstants.SSH_FXP_INIT, SftpConstants.SSH_FXP_VERSION, id, type, length, buffer);
+        }
+    }
+
+    /**
+     * @param selector The {@link SftpVersionSelector} to use - ignored if {@code null}
+     * @return The selected version (may be same as current)
+     * @throws IOException If failed to negotiate
+     */
+    public int negotiateVersion(SftpVersionSelector selector) throws IOException {
+        int current = getVersion();
+        if (selector == null) {
+            return current;
+        }
+
+        Set<Integer> available = GenericUtils.asSortedSet(Collections.singleton(current));
+        Map<String, ?> parsed = getParsedServerExtensions();
+        Collection<String> extensions = ParserUtils.supportedExtensions(parsed);
+        if ((GenericUtils.size(extensions) > 0) && extensions.contains(SftpConstants.EXT_VERSION_SELECT)) {
+            Versions vers = GenericUtils.isEmpty(parsed) ? null : (Versions) parsed.get(SftpConstants.EXT_VERSIONS);
+            Collection<String> reported = (vers == null) ? null : vers.getVersions();
+            if (GenericUtils.size(reported) > 0) {
+                for (String v : reported) {
+                    if (!available.add(Integer.valueOf(v))) {
+                        continue;   // debug breakpoint
+                    }
+                }
+            }
+        }
+
+        int selected = selector.selectVersion(getClientSession(), current, new ArrayList<>(available));
+        if (log.isDebugEnabled()) {
+            log.debug("negotiateVersion({}) current={} {} -> {}", getClientChannel(), current, available, selected);
+        }
+
+        if (selected == current) {
+            return current;
+        }
+
+        if (!available.contains(selected)) {
+            throw new StreamCorruptedException("Selected version (" + selected + ") not part of available: " + available);
+        }
+
+        String verVal = String.valueOf(selected);
+        Buffer buffer = new ByteArrayBuffer(Integer.BYTES + SftpConstants.EXT_VERSION_SELECT.length()     // extension name
+                + Integer.BYTES + verVal.length() + Byte.SIZE, false);
+        buffer.putString(SftpConstants.EXT_VERSION_SELECT);
+        buffer.putString(verVal);
+        checkCommandStatus(SftpConstants.SSH_FXP_EXTENDED, buffer);
+        versionHolder.set(selected);
+        return selected;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java
new file mode 100644
index 0000000..c6702f8
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java
@@ -0,0 +1,81 @@
+/*
+ * 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.subsystem.sftp.impl;
+
+import java.io.IOException;
+
+import org.apache.sshd.client.ClientFactoryManager;
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
+import org.apache.sshd.client.subsystem.sftp.SftpFileSystem;
+import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
+import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultSftpClientFactory extends AbstractLoggingBean implements SftpClientFactory {
+    public static final DefaultSftpClientFactory INSTANCE = new DefaultSftpClientFactory();
+
+    public DefaultSftpClientFactory() {
+        super();
+    }
+
+    @Override
+    public SftpClient createSftpClient(ClientSession session, SftpVersionSelector selector) throws IOException {
+        DefaultSftpClient client = createDefaultSftpClient(session, selector);
+        try {
+            client.negotiateVersion(selector);
+        } catch (IOException | RuntimeException e) {
+            if (log.isDebugEnabled()) {
+                log.debug("createSftpClient({}) failed ({}) to negotiate version: {}",
+                          session, e.getClass().getSimpleName(), e.getMessage());
+            }
+            if (log.isTraceEnabled()) {
+                log.trace("createSftpClient(" + session + ") version negotiation failure details", e);
+            }
+
+            client.close();
+            throw e;
+        }
+
+        return client;
+    }
+
+    protected DefaultSftpClient createDefaultSftpClient(ClientSession session, SftpVersionSelector selector) throws IOException {
+        return new DefaultSftpClient(session);
+    }
+
+    @Override
+    public SftpFileSystem createSftpFileSystem(
+            ClientSession session, SftpVersionSelector selector, int readBufferSize, int writeBufferSize)
+                throws IOException {
+        ClientFactoryManager manager = session.getFactoryManager();
+        SftpFileSystemProvider provider = new SftpFileSystemProvider((SshClient) manager, selector);
+        SftpFileSystem fs = provider.newFileSystem(session);
+        fs.setReadBufferSize(readBufferSize);
+        fs.setWriteBufferSize(writeBufferSize);
+        return fs;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java
index eee30c5..35c325b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java
@@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
 import org.apache.sshd.client.subsystem.sftp.SftpClient.Handle;
+import org.apache.sshd.client.subsystem.sftp.impl.DefaultCloseableHandle;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.junit.FixMethodOrder;
 import org.junit.Test;


[2/3] mina-sshd git commit: [SSHD-790] Allow users to register a custom SFTP client factory

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
deleted file mode 100644
index 0788b67..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
+++ /dev/null
@@ -1,461 +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.subsystem.sftp;
-
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InterruptedIOException;
-import java.io.OutputStream;
-import java.io.StreamCorruptedException;
-import java.net.SocketTimeoutException;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.NavigableMap;
-import java.util.Objects;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.apache.sshd.client.channel.ChannelSubsystem;
-import org.apache.sshd.client.channel.ClientChannel;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
-import org.apache.sshd.common.subsystem.sftp.extensions.VersionsParser.Versions;
-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.BufferUtils;
-import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class DefaultSftpClient extends AbstractSftpClient {
-    private final ClientSession clientSession;
-    private final ChannelSubsystem channel;
-    private final Map<Integer, Buffer> messages = new HashMap<>();
-    private final AtomicInteger cmdId = new AtomicInteger(100);
-    private final Buffer receiveBuffer = new ByteArrayBuffer();
-    private final byte[] workBuf = new byte[Integer.BYTES];
-    private final AtomicInteger versionHolder = new AtomicInteger(0);
-    private final AtomicBoolean closing = new AtomicBoolean(false);
-    private final NavigableMap<String, byte[]> extensions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-    private final NavigableMap<String, byte[]> exposedExtensions = Collections.unmodifiableNavigableMap(extensions);
-    private Charset nameDecodingCharset = DEFAULT_NAME_DECODING_CHARSET;
-
-    public DefaultSftpClient(ClientSession clientSession) throws IOException {
-        this.nameDecodingCharset = PropertyResolverUtils.getCharset(clientSession, NAME_DECODING_CHARSET, DEFAULT_NAME_DECODING_CHARSET);
-        this.clientSession = Objects.requireNonNull(clientSession, "No client session");
-        this.channel = clientSession.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME);
-        this.channel.setOut(new OutputStream() {
-            private final byte[] singleByte = new byte[1];
-            @Override
-            public void write(int b) throws IOException {
-                synchronized (singleByte) {
-                    singleByte[0] = (byte) b;
-                    write(singleByte);
-                }
-            }
-
-            @Override
-            public void write(byte[] b, int off, int len) throws IOException {
-                data(b, off, len);
-            }
-        });
-        this.channel.setErr(new ByteArrayOutputStream(Byte.MAX_VALUE));
-
-        long initializationTimeout = clientSession.getLongProperty(SFTP_CHANNEL_OPEN_TIMEOUT, DEFAULT_CHANNEL_OPEN_TIMEOUT);
-        this.channel.open().verify(initializationTimeout);
-        this.channel.onClose(() -> {
-            synchronized (messages) {
-                closing.set(true);
-                messages.notifyAll();
-            }
-
-            if (versionHolder.get() <= 0) {
-                log.warn("onClose({}) closed before version negotiated", channel);
-            }
-        });
-
-        try {
-            init(initializationTimeout);
-        } catch (IOException | RuntimeException e) {
-            this.channel.close(true);
-            throw e;
-        }
-    }
-
-    @Override
-    public int getVersion() {
-        return versionHolder.get();
-    }
-
-    @Override
-    public ClientSession getClientSession() {
-        return clientSession;
-    }
-
-    @Override
-    public ClientChannel getClientChannel() {
-        return channel;
-    }
-
-    @Override
-    public NavigableMap<String, byte[]> getServerExtensions() {
-        return exposedExtensions;
-    }
-
-    @Override
-    public Charset getNameDecodingCharset() {
-        return nameDecodingCharset;
-    }
-
-    @Override
-    public void setNameDecodingCharset(Charset nameDecodingCharset) {
-        this.nameDecodingCharset = Objects.requireNonNull(nameDecodingCharset, "No charset provided");
-    }
-
-    @Override
-    public boolean isClosing() {
-        return closing.get();
-    }
-
-    @Override
-    public boolean isOpen() {
-        return this.channel.isOpen();
-    }
-
-    @Override
-    public void close() throws IOException {
-        if (isOpen()) {
-            this.channel.close(false);
-        }
-    }
-
-    /**
-     * Receive binary data
-     * @param buf   The buffer for the incoming data
-     * @param start Offset in buffer to place the data
-     * @param len   Available space in buffer for the data
-     * @return Actual size of received data
-     * @throws IOException If failed to receive incoming data
-     */
-    protected int data(byte[] buf, int start, int len) throws IOException {
-        Buffer incoming = new ByteArrayBuffer(buf, start, len);
-        // If we already have partial data, we need to append it to the buffer and use it
-        if (receiveBuffer.available() > 0) {
-            receiveBuffer.putBuffer(incoming);
-            incoming = receiveBuffer;
-        }
-
-        // Process commands
-        int rpos = incoming.rpos();
-        for (int count = 1; receive(incoming); count++) {
-            if (log.isTraceEnabled()) {
-                log.trace("data({}) Processed {} data messages", getClientChannel(), count);
-            }
-        }
-
-        int read = incoming.rpos() - rpos;
-        // Compact and add remaining data
-        receiveBuffer.compact();
-        if (receiveBuffer != incoming && incoming.available() > 0) {
-            receiveBuffer.putBuffer(incoming);
-        }
-
-        return read;
-    }
-
-    /**
-     * Read SFTP packets from buffer
-     *
-     * @param incoming The received {@link Buffer}
-     * @return {@code true} if data from incoming buffer was processed
-     * @throws IOException if failed to process the buffer
-     * @see #process(Buffer)
-     */
-    protected boolean receive(Buffer incoming) throws IOException {
-        int rpos = incoming.rpos();
-        int wpos = incoming.wpos();
-        ClientSession session = getClientSession();
-        session.resetIdleTimeout();
-
-        if ((wpos - rpos) > 4) {
-            int length = incoming.getInt();
-            if (length < 5) {
-                throw new IOException("Illegal sftp packet length: " + length);
-            }
-            if ((wpos - rpos) >= (length + 4)) {
-                incoming.rpos(rpos);
-                incoming.wpos(rpos + 4 + length);
-                process(incoming);
-                incoming.rpos(rpos + 4 + length);
-                incoming.wpos(wpos);
-                return true;
-            }
-        }
-        incoming.rpos(rpos);
-        return false;
-    }
-
-    /**
-     * Process an SFTP packet
-     *
-     * @param incoming The received {@link Buffer}
-     * @throws IOException if failed to process the buffer
-     */
-    protected void process(Buffer incoming) throws IOException {
-        // create a copy of the buffer in case it is being re-used
-        Buffer buffer = new ByteArrayBuffer(incoming.available() + Long.SIZE, false);
-        buffer.putBuffer(incoming);
-
-        int rpos = buffer.rpos();
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        Integer id = buffer.getInt();
-        buffer.rpos(rpos);
-
-        if (log.isTraceEnabled()) {
-            log.trace("process({}) id={}, type={}, len={}",
-                      getClientChannel(), id, SftpConstants.getCommandMessageName(type), length);
-        }
-
-        synchronized (messages) {
-            messages.put(id, buffer);
-            messages.notifyAll();
-        }
-    }
-
-    @Override
-    public int send(int cmd, Buffer buffer) throws IOException {
-        int id = cmdId.incrementAndGet();
-        int len = buffer.available();
-        if (log.isTraceEnabled()) {
-            log.trace("send({}) cmd={}, len={}, id={}",
-                      getClientChannel(), SftpConstants.getCommandMessageName(cmd), len, id);
-        }
-
-        OutputStream dos = channel.getInvertedIn();
-        BufferUtils.writeInt(dos, 1 /* cmd */ + Integer.BYTES /* id */ + len, workBuf);
-        dos.write(cmd & 0xFF);
-        BufferUtils.writeInt(dos, id, workBuf);
-        dos.write(buffer.array(), buffer.rpos(), len);
-        dos.flush();
-        return id;
-    }
-
-    @Override
-    public Buffer receive(int id) throws IOException {
-        Integer reqId = id;
-        synchronized (messages) {
-            for (int count = 1;; count++) {
-                if (isClosing() || (!isOpen())) {
-                    throw new SshException("Channel is being closed");
-                }
-
-                Buffer buffer = messages.remove(reqId);
-                if (buffer != null) {
-                    return buffer;
-                }
-
-                try {
-                    messages.wait();
-                } catch (InterruptedException e) {
-                    throw (IOException) new InterruptedIOException("Interrupted while waiting for messages at iteration #" + count).initCause(e);
-                }
-            }
-        }
-    }
-
-    protected Buffer read() throws IOException {
-        InputStream dis = channel.getInvertedOut();
-        int length = BufferUtils.readInt(dis, workBuf);
-        // must have at least command + length
-        if (length < (1 + Integer.BYTES)) {
-            throw new IllegalArgumentException("Bad length: " + length);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(length + Integer.BYTES, false);
-        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;
-    }
-
-    protected void init(long initializationTimeout) throws IOException {
-        ValidateUtils.checkTrue(initializationTimeout > 0L, "Invalid initialization timeout: %d", initializationTimeout);
-
-        // Send init packet
-        OutputStream dos = channel.getInvertedIn();
-        BufferUtils.writeInt(dos, 5 /* total length */, workBuf);
-        dos.write(SftpConstants.SSH_FXP_INIT);
-        BufferUtils.writeInt(dos, SftpConstants.SFTP_V6, workBuf);
-        dos.flush();
-
-        Buffer buffer;
-        Integer reqId;
-        synchronized (messages) {
-            /*
-             * We need to use a timeout since if the remote server does not support
-             * SFTP, we will not know it immediately. This is due to the fact that the
-             * request for the subsystem does not contain a reply as to its success or
-             * failure. Thus, the SFTP channel is created by the client, but there is
-             * no one on the other side to reply - thus the need for the timeout
-             */
-            for (long remainingTimeout = initializationTimeout; (remainingTimeout > 0L) && messages.isEmpty() && (!isClosing()) && isOpen();) {
-                try {
-                    long sleepStart = System.nanoTime();
-                    messages.wait(remainingTimeout);
-                    long sleepEnd = System.nanoTime();
-                    long sleepDuration = sleepEnd - sleepStart;
-                    long sleepMillis = TimeUnit.NANOSECONDS.toMillis(sleepDuration);
-                    if (sleepMillis < 1L) {
-                        remainingTimeout--;
-                    } else {
-                        remainingTimeout -= sleepMillis;
-                    }
-                } catch (InterruptedException e) {
-                    throw (IOException) new InterruptedIOException("Interrupted init()").initCause(e);
-                }
-            }
-
-            if (isClosing() || (!isOpen())) {
-                throw new EOFException("Closing while await init message");
-            }
-
-            if (messages.isEmpty()) {
-                throw new SocketTimeoutException("No incoming initialization response received within " + initializationTimeout + " msec.");
-            }
-
-            Collection<Integer> ids = messages.keySet();
-            Iterator<Integer> iter = ids.iterator();
-            reqId = iter.next();
-            buffer = messages.remove(reqId);
-        }
-
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (log.isTraceEnabled()) {
-            log.trace("init({}) id={} type={} len={}",
-                      getClientChannel(), id, SftpConstants.getCommandMessageName(type), length);
-        }
-
-        if (type == SftpConstants.SSH_FXP_VERSION) {
-            if (id < SftpConstants.SFTP_V3) {
-                throw new SshException("Unsupported sftp version " + id);
-            }
-            versionHolder.set(id);
-
-            if (log.isTraceEnabled()) {
-                log.trace("init({}) version={}", getClientChannel(), versionHolder);
-            }
-
-            while (buffer.available() > 0) {
-                String name = buffer.getString();
-                byte[] data = buffer.getBytes();
-                if (log.isTraceEnabled()) {
-                    log.trace("init({}) added extension=", getClientChannel(), name);
-                }
-                extensions.put(name, data);
-            }
-        } else if (type == SftpConstants.SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("init({})[id={}] - status: {} [{}] {}",
-                          getClientChannel(), id, SftpConstants.getStatusName(substatus), lang, msg);
-            }
-
-            throwStatusException(SftpConstants.SSH_FXP_INIT, id, substatus, msg, lang);
-        } else {
-            handleUnexpectedPacket(SftpConstants.SSH_FXP_INIT, SftpConstants.SSH_FXP_VERSION, id, type, length, buffer);
-        }
-    }
-
-    /**
-     * @param selector The {@link SftpVersionSelector} to use - ignored if {@code null}
-     * @return The selected version (may be same as current)
-     * @throws IOException If failed to negotiate
-     */
-    public int negotiateVersion(SftpVersionSelector selector) throws IOException {
-        int current = getVersion();
-        if (selector == null) {
-            return current;
-        }
-
-        Set<Integer> available = GenericUtils.asSortedSet(Collections.singleton(current));
-        Map<String, ?> parsed = getParsedServerExtensions();
-        Collection<String> extensions = ParserUtils.supportedExtensions(parsed);
-        if ((GenericUtils.size(extensions) > 0) && extensions.contains(SftpConstants.EXT_VERSION_SELECT)) {
-            Versions vers = GenericUtils.isEmpty(parsed) ? null : (Versions) parsed.get(SftpConstants.EXT_VERSIONS);
-            Collection<String> reported = (vers == null) ? null : vers.getVersions();
-            if (GenericUtils.size(reported) > 0) {
-                for (String v : reported) {
-                    if (!available.add(Integer.valueOf(v))) {
-                        continue;   // debug breakpoint
-                    }
-                }
-            }
-        }
-
-        int selected = selector.selectVersion(getClientSession(), current, new ArrayList<>(available));
-        if (log.isDebugEnabled()) {
-            log.debug("negotiateVersion({}) current={} {} -> {}", getClientChannel(), current, available, selected);
-        }
-
-        if (selected == current) {
-            return current;
-        }
-
-        if (!available.contains(selected)) {
-            throw new StreamCorruptedException("Selected version (" + selected + ") not part of available: " + available);
-        }
-
-        String verVal = String.valueOf(selected);
-        Buffer buffer = new ByteArrayBuffer(Integer.BYTES + SftpConstants.EXT_VERSION_SELECT.length()     // extension name
-                + Integer.BYTES + verVal.length() + Byte.SIZE, false);
-        buffer.putString(SftpConstants.EXT_VERSION_SELECT);
-        buffer.putString(verVal);
-        checkCommandStatus(SftpConstants.SSH_FXP_EXTENDED, buffer);
-        versionHolder.set(selected);
-        return selected;
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpAclFileAttributeView.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpAclFileAttributeView.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpAclFileAttributeView.java
index f3ca4f3..7cada6e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpAclFileAttributeView.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpAclFileAttributeView.java
@@ -28,6 +28,8 @@ import java.nio.file.attribute.PosixFileAttributes;
 import java.nio.file.attribute.UserPrincipal;
 import java.util.List;
 
+import org.apache.sshd.client.subsystem.sftp.impl.AbstractSftpFileAttributeView;
+
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
index c835e35..c860cb4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
@@ -496,7 +496,7 @@ public interface SftpClient extends SubsystemClient {
         private final String longFilename;
         private final Attributes attributes;
 
-        DirEntry(String filename, String longFilename, Attributes attributes) {
+        public DirEntry(String filename, String longFilename, Attributes attributes) {
             this.filename = filename;
             this.longFilename = longFilename;
             this.attributes = attributes;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientCreator.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientCreator.java
index 8e41729..a282b87 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientCreator.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientCreator.java
@@ -61,15 +61,25 @@ public interface SftpClientCreator {
      */
     SftpClient createSftpClient(SftpVersionSelector selector) throws IOException;
 
-    FileSystem createSftpFileSystem() throws IOException;
+    default FileSystem createSftpFileSystem() throws IOException {
+        return createSftpFileSystem(SftpVersionSelector.CURRENT);
+    }
 
-    FileSystem createSftpFileSystem(int version) throws IOException;
+    default FileSystem createSftpFileSystem(int version) throws IOException {
+        return createSftpFileSystem(SftpVersionSelector.fixedVersionSelector(version));
+    }
 
-    FileSystem createSftpFileSystem(SftpVersionSelector selector) throws IOException;
+    default FileSystem createSftpFileSystem(SftpVersionSelector selector) throws IOException {
+        return createSftpFileSystem(selector, SftpClient.DEFAULT_READ_BUFFER_SIZE, SftpClient.DEFAULT_WRITE_BUFFER_SIZE);
+    }
 
-    FileSystem createSftpFileSystem(int readBufferSize, int writeBufferSize) throws IOException;
+    default FileSystem createSftpFileSystem(int version, int readBufferSize, int writeBufferSize) throws IOException {
+        return createSftpFileSystem(SftpVersionSelector.fixedVersionSelector(version), readBufferSize, writeBufferSize);
+    }
 
-    FileSystem createSftpFileSystem(int version, int readBufferSize, int writeBufferSize) throws IOException;
+    default FileSystem createSftpFileSystem(int readBufferSize, int writeBufferSize) throws IOException {
+        return createSftpFileSystem(SftpVersionSelector.CURRENT, readBufferSize, writeBufferSize);
+    }
 
     FileSystem createSftpFileSystem(SftpVersionSelector selector, int readBufferSize, int writeBufferSize) throws IOException;
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java
new file mode 100644
index 0000000..15e321f
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java
@@ -0,0 +1,51 @@
+/*
+ * 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.subsystem.sftp;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpClientFactory {
+    /**
+     * @param session The {@link ClientSession} to which the SFTP client should be attached
+     * @param selector The {@link SftpVersionSelector} to use in order to negotiate the SFTP version
+     * @return The created {@link SftpClient} instance
+     * @throws IOException If failed to create the client
+     */
+    SftpClient createSftpClient(ClientSession session, SftpVersionSelector selector) throws IOException;
+
+    /**
+     * @param session The {@link ClientSession} to which the SFTP client backing the file system should be attached
+     * @param selector The {@link SftpVersionSelector} to use in order to negotiate the SFTP version
+     * @param readBufferSize Default I/O read buffer size
+     * @param writeBufferSize Default I/O write buffer size
+     * @return The created {@link FileSystem} instance
+     * @throws IOException If failed to create the instance
+     */
+    FileSystem createSftpFileSystem(
+        ClientSession session, SftpVersionSelector selector, int readBufferSize, int writeBufferSize)
+            throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactoryManager.java
new file mode 100644
index 0000000..02bc5f6
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactoryManager.java
@@ -0,0 +1,37 @@
+/*
+ * 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.subsystem.sftp;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpClientFactoryManager {
+    /**
+     * @return The (never {@code null}) {@link SftpClientFactory} instance
+     */
+    SftpClientFactory getSftpClientFactory();
+
+    /**
+     * @param sftpClientFactory The {@link SftpClientFactory} instance to use - if {@code null}
+     * then an internal default will be used
+     */
+    void setSftpClientFactory(SftpClientFactory sftpClientFactory);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
index f028a5b..1ca0283 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
@@ -42,6 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.ClientSessionHolder;
+import org.apache.sshd.client.subsystem.sftp.impl.AbstractSftpClient;
 import org.apache.sshd.common.file.util.BaseFileSystem;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.util.GenericUtils;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
index 5b2f49d..116282a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
@@ -831,7 +831,7 @@ public class SftpFileSystemProvider extends FileSystemProvider {
         return map;
     }
 
-    protected SftpClient.Attributes readRemoteAttributes(SftpPath path, LinkOption... options) throws IOException {
+    public SftpClient.Attributes readRemoteAttributes(SftpPath path, LinkOption... options) throws IOException {
         SftpFileSystem fs = path.getFileSystem();
         try (SftpClient client = fs.getClient()) {
             try {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributeView.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributeView.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributeView.java
index 13d2119..1fb614c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributeView.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpPosixFileAttributeView.java
@@ -29,6 +29,7 @@ import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.UserPrincipal;
 import java.util.Set;
 
+import org.apache.sshd.client.subsystem.sftp.impl.AbstractSftpFileAttributeView;
 import org.apache.sshd.common.util.GenericUtils;
 
 /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpClient.java
new file mode 100644
index 0000000..a1d2014
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpClient.java
@@ -0,0 +1,1134 @@
+/*
+ * 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.subsystem.sftp.impl;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.subsystem.AbstractSubsystemClient;
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtensionFactory;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.subsystem.sftp.SftpHelper;
+import org.apache.sshd.common.subsystem.sftp.SftpUniversalOwnerAndGroup;
+import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+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;
+
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractSftpClient extends AbstractSubsystemClient implements SftpClient, RawSftpClient {
+    private final Attributes fileOpenAttributes = new Attributes();
+    private final AtomicReference<Map<String, Object>> parsedExtensionsHolder = new AtomicReference<>(null);
+
+    protected AbstractSftpClient() {
+        fileOpenAttributes.setType(SftpConstants.SSH_FILEXFER_TYPE_REGULAR);
+    }
+
+    @Override
+    public Channel getChannel() {
+        return getClientChannel();
+    }
+
+    @Override
+    public <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType) {
+        Object instance = getExtension(BuiltinSftpClientExtensions.fromType(extensionType));
+        if (instance == null) {
+            return null;
+        } else {
+            return extensionType.cast(instance);
+        }
+    }
+
+    @Override
+    public SftpClientExtension getExtension(String extensionName) {
+        return getExtension(BuiltinSftpClientExtensions.fromName(extensionName));
+    }
+
+    protected SftpClientExtension getExtension(SftpClientExtensionFactory factory) {
+        if (factory == null) {
+            return null;
+        }
+
+        Map<String, byte[]> extensions = getServerExtensions();
+        Map<String, Object> parsed = getParsedServerExtensions(extensions);
+        return factory.create(this, this, extensions, parsed);
+    }
+
+    protected Map<String, Object> getParsedServerExtensions() {
+        return getParsedServerExtensions(getServerExtensions());
+    }
+
+    protected Map<String, Object> getParsedServerExtensions(Map<String, byte[]> extensions) {
+        Map<String, Object> parsed = parsedExtensionsHolder.get();
+        if (parsed == null) {
+            parsed = ParserUtils.parse(extensions);
+            if (parsed == null) {
+                parsed = Collections.emptyMap();
+            }
+            parsedExtensionsHolder.set(parsed);
+        }
+
+        return parsed;
+    }
+
+    /**
+     * @param cmd The command that was sent whose response contains the name to be decoded
+     * @param buf The {@link Buffer} containing the encoded name
+     * @return The decoded referenced name
+     */
+    protected String getReferencedName(int cmd, Buffer buf) {
+        Charset cs = getNameDecodingCharset();
+        return buf.getString(cs);
+    }
+
+    /**
+     * Sends the specified command, waits for the response and then invokes {@link #checkResponseStatus(int, Buffer)}
+     * @param cmd The command to send
+     * @param request The request {@link Buffer}
+     * @throws IOException If failed to send, receive or check the returned status
+     * @see #send(int, Buffer)
+     * @see #receive(int)
+     * @see #checkResponseStatus(int, Buffer)
+     */
+    protected void checkCommandStatus(int cmd, Buffer request) throws IOException {
+        int reqId = send(cmd, request);
+        Buffer response = receive(reqId);
+        checkResponseStatus(cmd, response);
+    }
+
+    /**
+     * Checks if the incoming response is an {@code SSH_FXP_STATUS} one,
+     * and if so whether the substatus is {@code SSH_FX_OK}.
+     *
+     * @param cmd The sent command opcode
+     * @param buffer The received response {@link Buffer}
+     * @throws IOException If response does not carry a status or carries
+     * a bad status code
+     * @see #checkResponseStatus(int, int, int, String, String)
+     */
+    protected void checkResponseStatus(int cmd, Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            checkResponseStatus(cmd, id, substatus, msg, lang);
+        } else {
+            //noinspection ThrowableResultOfMethodCallIgnored
+            handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_STATUS, id, type, length, buffer);
+        }
+    }
+
+    /**
+     * @param cmd The sent command opcode
+     * @param id The request id
+     * @param substatus The sub-status value
+     * @param msg The message
+     * @param lang The language
+     * @throws IOException if the sub-status is not {@code SSH_FX_OK}
+     * @see #throwStatusException(int, int, int, String, String)
+     */
+    protected void checkResponseStatus(int cmd, int id, int substatus, String msg, String lang) throws IOException {
+        if (log.isTraceEnabled()) {
+            log.trace("checkResponseStatus({})[id={}] cmd={} status={} lang={} msg={}",
+                      getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
+                      SftpConstants.getStatusName(substatus), lang, msg);
+        }
+
+        if (substatus != SftpConstants.SSH_FX_OK) {
+            throwStatusException(cmd, id, substatus, msg, lang);
+        }
+    }
+
+    protected void throwStatusException(int cmd, int id, int substatus, String msg, String lang) throws IOException {
+        throw new SftpException(substatus, msg);
+    }
+
+    /**
+     * @param cmd Command to be sent
+     * @param request The {@link Buffer} containing the request
+     * @return The received handle identifier
+     * @throws IOException If failed to send/receive or process the response
+     * @see #send(int, Buffer)
+     * @see #receive(int)
+     * @see #checkHandleResponse(int, Buffer)
+     */
+    protected byte[] checkHandle(int cmd, Buffer request) throws IOException {
+        int reqId = send(cmd, request);
+        Buffer response = receive(reqId);
+        return checkHandleResponse(cmd, response);
+    }
+
+    protected byte[] checkHandleResponse(int cmd, Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SftpConstants.SSH_FXP_HANDLE) {
+            return ValidateUtils.checkNotNullAndNotEmpty(buffer.getBytes(), "Null/empty handle in buffer", GenericUtils.EMPTY_OBJECT_ARRAY);
+        }
+
+        if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkHandleResponse({})[id={}] {} - status: {} [{}] {}",
+                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
+                          SftpConstants.getStatusName(substatus), lang, msg);
+            }
+            throwStatusException(cmd, id, substatus, msg, lang);
+        }
+
+        return handleUnexpectedHandlePacket(cmd, id, type, length, buffer);
+    }
+
+    protected byte[] handleUnexpectedHandlePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
+        handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_HANDLE, id, type, length, buffer);
+        throw new SshException("No handling for unexpected handle packet id=" + id
+                             + ", type=" + SftpConstants.getCommandMessageName(type) + ", length=" + length);
+    }
+
+    /**
+     * @param cmd Command to be sent
+     * @param request Request {@link Buffer}
+     * @return The decoded response {@code Attributes}
+     * @throws IOException If failed to send/receive or process the response
+     * @see #send(int, Buffer)
+     * @see #receive(int)
+     * @see #checkAttributesResponse(int, Buffer)
+     */
+    protected Attributes checkAttributes(int cmd, Buffer request) throws IOException {
+        int reqId = send(cmd, request);
+        Buffer response = receive(reqId);
+        return checkAttributesResponse(cmd, response);
+    }
+
+    protected Attributes checkAttributesResponse(int cmd, Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SftpConstants.SSH_FXP_ATTRS) {
+            return readAttributes(cmd, buffer);
+        }
+
+        if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkAttributesResponse()[id={}] {} - status: {} [{}] {}",
+                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
+                          SftpConstants.getStatusName(substatus), lang, msg);
+            }
+            throwStatusException(cmd, id, substatus, msg, lang);
+        }
+
+        return handleUnexpectedAttributesPacket(cmd, id, type, length, buffer);
+    }
+
+    protected Attributes handleUnexpectedAttributesPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
+        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_ATTRS, id, type, length, buffer);
+        if (err != null) {
+            throw err;
+        }
+
+        return null;
+    }
+
+    /**
+     * @param cmd Command to be sent
+     * @param request The request {@link Buffer}
+     * @return The retrieved name
+     * @throws IOException If failed to send/receive or process the response
+     * @see #send(int, Buffer)
+     * @see #receive(int)
+     * @see #checkOneNameResponse(int, Buffer)
+     */
+    protected String checkOneName(int cmd, Buffer request) throws IOException {
+        int reqId = send(cmd, request);
+        Buffer response = receive(reqId);
+        return checkOneNameResponse(cmd, response);
+    }
+
+    protected String checkOneNameResponse(int cmd, Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SftpConstants.SSH_FXP_NAME) {
+            int len = buffer.getInt();
+            if (len != 1) {
+                throw new SshException("SFTP error: received " + len + " names instead of 1");
+            }
+            String name = getReferencedName(cmd, buffer);
+            String longName = null;
+            int version = getVersion();
+            if (version == SftpConstants.SFTP_V3) {
+                longName = getReferencedName(cmd, buffer);
+            }
+
+            Attributes attrs = readAttributes(cmd, buffer);
+            Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
+            // TODO decide what to do if not-null and not TRUE
+            if (log.isTraceEnabled()) {
+                log.trace("checkOneNameResponse({})[id={}] {} ({})[{}] eol={}: {}",
+                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
+                          name, longName, indicator, attrs);
+            }
+            return name;
+        }
+
+        if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkOneNameResponse({})[id={}] {} status: {} [{}] {}",
+                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
+                          SftpConstants.getStatusName(substatus), lang, msg);
+            }
+
+            throwStatusException(cmd, id, substatus, msg, lang);
+        }
+
+        return handleUnknownOneNamePacket(cmd, id, type, length, buffer);
+    }
+
+    protected String handleUnknownOneNamePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
+        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer);
+        if (err != null) {
+            throw err;
+        }
+
+        return null;
+    }
+
+    protected Attributes readAttributes(int cmd, Buffer buffer) throws IOException {
+        Attributes attrs = new Attributes();
+        int flags = buffer.getInt();
+        int version = getVersion();
+        if (version == SftpConstants.SFTP_V3) {
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+                attrs.setSize(buffer.getLong());
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
+                attrs.owner(buffer.getInt(), buffer.getInt());
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                int perms = buffer.getInt();
+                attrs.setPermissions(perms);
+                attrs.setType(SftpHelper.permissionsToFileType(perms));
+            }
+
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+                attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags));
+                attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags));
+            }
+        } else if (version >= SftpConstants.SFTP_V4) {
+            attrs.setType(buffer.getUByte());
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+                attrs.setSize(buffer.getLong());
+            }
+
+            if ((version >= SftpConstants.SFTP_V6) && ((flags & SftpConstants.SSH_FILEXFER_ATTR_ALLOCATION_SIZE) != 0)) {
+                @SuppressWarnings("unused")
+                long allocSize = buffer.getLong();    // TODO handle allocation size
+            }
+
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
+                attrs.setOwner(buffer.getString());
+                attrs.setGroup(buffer.getString());
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                attrs.setPermissions(buffer.getInt());
+            }
+
+            // update the permissions according to the type
+            int perms = attrs.getPermissions();
+            perms |= SftpHelper.fileTypeToPermission(attrs.getType());
+            attrs.setPermissions(perms);
+
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+                attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags));
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+                attrs.setCreateTime(SftpHelper.readTime(buffer, version, flags));
+            }
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+                attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags));
+            }
+            if ((version >= SftpConstants.SFTP_V6) && (flags & SftpConstants.SSH_FILEXFER_ATTR_CTIME) != 0) {
+                @SuppressWarnings("unused")
+                FileTime attrsChangedTime = SftpHelper.readTime(buffer, version, flags);    // TODO the last time the file attributes were changed
+            }
+
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
+                attrs.setAcl(SftpHelper.readACLs(buffer, version));
+            }
+
+            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_BITS) != 0) {
+                @SuppressWarnings("unused")
+                int bits = buffer.getInt();
+                @SuppressWarnings("unused")
+                int valid = 0xffffffff;
+                if (version >= SftpConstants.SFTP_V6) {
+                    valid = buffer.getInt();
+                }
+                // TODO: handle attrib bits
+            }
+
+            if (version >= SftpConstants.SFTP_V6) {
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_TEXT_HINT) != 0) {
+                    @SuppressWarnings("unused")
+                    boolean text = buffer.getBoolean(); // TODO: handle text
+                }
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MIME_TYPE) != 0) {
+                    @SuppressWarnings("unused")
+                    String mimeType = buffer.getString(); // TODO: handle mime-type
+                }
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_LINK_COUNT) != 0) {
+                    @SuppressWarnings("unused")
+                    int nlink = buffer.getInt(); // TODO: handle link-count
+                }
+                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UNTRANSLATED_NAME) != 0) {
+                    @SuppressWarnings("unused")
+                    String untranslated = getReferencedName(cmd, buffer); // TODO: handle untranslated-name
+                }
+            }
+        } else {
+            throw new IllegalStateException("readAttributes - unsupported version: " + version);
+        }
+
+        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
+            attrs.setExtensions(SftpHelper.readExtensions(buffer));
+        }
+
+        return attrs;
+    }
+
+    protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
+        int version = getVersion();
+        int flagsMask = 0;
+        Collection<Attribute> flags = Objects.requireNonNull(attributes, "No attributes").getFlags();
+        if (version == SftpConstants.SFTP_V3) {
+            for (Attribute a : flags) {
+                switch (a) {
+                    case Size:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
+                        break;
+                    case UidGid:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_UIDGID;
+                        break;
+                    case Perms:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
+                        break;
+                    case AccessTime:
+                        if (flags.contains(Attribute.ModifyTime)) {
+                            flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
+                        }
+                        break;
+                    case ModifyTime:
+                        if (flags.contains(Attribute.AccessTime)) {
+                            flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
+                        }
+                        break;
+                    case Extensions:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
+                        break;
+                    default:    // do nothing
+                }
+            }
+            buffer.putInt(flagsMask);
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+                buffer.putLong(attributes.getSize());
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
+                buffer.putInt(attributes.getUserId());
+                buffer.putInt(attributes.getGroupId());
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                buffer.putInt(attributes.getPermissions());
+            }
+
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime());
+                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime());
+            }
+        } else if (version >= SftpConstants.SFTP_V4) {
+            for (Attribute a : flags) {
+                switch (a) {
+                    case Size:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
+                        break;
+                    case OwnerGroup:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP;
+                        break;
+                    case Perms:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
+                        break;
+                    case AccessTime:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME;
+                        break;
+                    case ModifyTime:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME;
+                        break;
+                    case CreateTime:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_CREATETIME;
+                        break;
+                    case Acl:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACL;
+                        break;
+                    case Extensions:
+                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
+                        break;
+                    default:    // do nothing
+                }
+            }
+            buffer.putInt(flagsMask);
+            buffer.putByte((byte) attributes.getType());
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
+                buffer.putLong(attributes.getSize());
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
+                String owner = attributes.getOwner();
+                buffer.putString(GenericUtils.isEmpty(owner) ? SftpUniversalOwnerAndGroup.Owner.getName() : owner);
+
+                String group = attributes.getGroup();
+                buffer.putString(GenericUtils.isEmpty(group) ? SftpUniversalOwnerAndGroup.Group.getName() : group);
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                buffer.putInt(attributes.getPermissions());
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime());
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getCreateTime());
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime());
+            }
+            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
+                SftpHelper.writeACLs(buffer, version, attributes.getAcl());
+            }
+
+            // TODO: for v6+ add CTIME (see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-21)
+        } else {
+            throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + version);
+        }
+
+        if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
+            SftpHelper.writeExtensions(buffer, attributes.getExtensions());
+        }
+    }
+
+    @Override
+    public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("open(" + path + ")[" + options + "] client is closed");
+        }
+
+        /*
+         * Be consistent with FileChannel#open - if no mode specified then READ is assumed
+         */
+        if (GenericUtils.isEmpty(options)) {
+            options = EnumSet.of(OpenMode.Read);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
+        buffer.putString(path);
+        int version = getVersion();
+        int mode = 0;
+        if (version < SftpConstants.SFTP_V5) {
+            for (OpenMode m : options) {
+                switch (m) {
+                    case Read:
+                        mode |= SftpConstants.SSH_FXF_READ;
+                        break;
+                    case Write:
+                        mode |= SftpConstants.SSH_FXF_WRITE;
+                        break;
+                    case Append:
+                        mode |= SftpConstants.SSH_FXF_APPEND;
+                        break;
+                    case Create:
+                        mode |= SftpConstants.SSH_FXF_CREAT;
+                        break;
+                    case Truncate:
+                        mode |= SftpConstants.SSH_FXF_TRUNC;
+                        break;
+                    case Exclusive:
+                        mode |= SftpConstants.SSH_FXF_EXCL;
+                        break;
+                    default:    // do nothing
+                }
+            }
+        } else {
+            int access = 0;
+            if (options.contains(OpenMode.Read)) {
+                access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES;
+            }
+            if (options.contains(OpenMode.Write)) {
+                access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES;
+            }
+            if (options.contains(OpenMode.Append)) {
+                access |= SftpConstants.ACE4_APPEND_DATA;
+            }
+            buffer.putInt(access);
+
+            if (options.contains(OpenMode.Create) && options.contains(OpenMode.Exclusive)) {
+                mode |= SftpConstants.SSH_FXF_CREATE_NEW;
+            } else if (options.contains(OpenMode.Create) && options.contains(OpenMode.Truncate)) {
+                mode |= SftpConstants.SSH_FXF_CREATE_TRUNCATE;
+            } else if (options.contains(OpenMode.Create)) {
+                mode |= SftpConstants.SSH_FXF_OPEN_OR_CREATE;
+            } else if (options.contains(OpenMode.Truncate)) {
+                mode |= SftpConstants.SSH_FXF_TRUNCATE_EXISTING;
+            } else {
+                mode |= SftpConstants.SSH_FXF_OPEN_EXISTING;
+            }
+        }
+        buffer.putInt(mode);
+        writeAttributes(buffer, fileOpenAttributes);
+
+        CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPEN, buffer));
+        if (log.isTraceEnabled()) {
+            log.trace("open({})[{}] options={}: {}", getClientSession(), path, options, handle);
+        }
+        return handle;
+    }
+
+    @Override
+    public void close(Handle handle) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("close(" + handle + ") client is closed");
+        }
+
+        if (log.isTraceEnabled()) {
+            log.trace("close({}) {}", getClientSession(), handle);
+        }
+
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false);
+        buffer.putBytes(id);
+        checkCommandStatus(SftpConstants.SSH_FXP_CLOSE, buffer);
+    }
+
+    @Override
+    public void remove(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("remove(" + path + ") client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("remove({}) {}", getClientSession(), path);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
+        buffer.putString(path);
+        checkCommandStatus(SftpConstants.SSH_FXP_REMOVE, buffer);
+    }
+
+    @Override
+    public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("rename(" + oldPath + " => " + newPath + ")[" + options + "] client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("rename({}) {} => {}", getClientSession(), oldPath, newPath);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */, false);
+        buffer.putString(oldPath);
+        buffer.putString(newPath);
+
+        int numOptions = GenericUtils.size(options);
+        int version = getVersion();
+        if (version >= SftpConstants.SFTP_V5) {
+            int opts = 0;
+            if (numOptions > 0) {
+                for (CopyMode opt : options) {
+                    switch (opt) {
+                        case Atomic:
+                            opts |= SftpConstants.SSH_FXP_RENAME_ATOMIC;
+                            break;
+                        case Overwrite:
+                            opts |= SftpConstants.SSH_FXP_RENAME_OVERWRITE;
+                            break;
+                        default:    // do nothing
+                    }
+                }
+            }
+            buffer.putInt(opts);
+        } else if (numOptions > 0) {
+            throw new UnsupportedOperationException("rename(" + oldPath + " => " + newPath + ")"
+                            + " - copy options can not be used with this SFTP version: " + options);
+        }
+        checkCommandStatus(SftpConstants.SSH_FXP_RENAME, buffer);
+    }
+
+    @Override
+    public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException {
+        if (eofSignalled != null) {
+            eofSignalled.set(null);
+        }
+
+        if (!isOpen()) {
+            throw new IOException("read(" + handle + "/" + fileOffset + ")[" + dstOffset + "/" + len + "] client is closed");
+        }
+
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false);
+        buffer.putBytes(id);
+        buffer.putLong(fileOffset);
+        buffer.putInt(len);
+        return checkData(SftpConstants.SSH_FXP_READ, buffer, dstOffset, dst, eofSignalled);
+    }
+
+    protected int checkData(int cmd, Buffer request, int dstOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
+        if (eofSignalled != null) {
+            eofSignalled.set(null);
+        }
+        int reqId = send(cmd, request);
+        Buffer response = receive(reqId);
+        return checkDataResponse(cmd, response, dstOffset, dst, eofSignalled);
+    }
+
+    protected int checkDataResponse(int cmd, Buffer buffer, int dstoff, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
+        if (eofSignalled != null) {
+            eofSignalled.set(null);
+        }
+
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SftpConstants.SSH_FXP_DATA) {
+            int len = buffer.getInt();
+            buffer.getRawBytes(dst, dstoff, len);
+            Boolean indicator = SftpHelper.getEndOfFileIndicatorValue(buffer, getVersion());
+            if (log.isTraceEnabled()) {
+                log.trace("checkDataResponse({}][id={}] {} offset={}, len={}, EOF={}",
+                          getClientChannel(), SftpConstants.getCommandMessageName(cmd),
+                          id, dstoff, len, indicator);
+            }
+            if (eofSignalled != null) {
+                eofSignalled.set(indicator);
+            }
+
+            return len;
+        }
+
+        if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkDataResponse({})[id={}] {} status: {} [{}] {}",
+                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
+                          SftpConstants.getStatusName(substatus), lang, msg);
+            }
+
+            if (substatus == SftpConstants.SSH_FX_EOF) {
+                return -1;
+            }
+
+            throwStatusException(cmd, id, substatus, msg, lang);
+        }
+
+        return handleUnknownDataPacket(cmd, id, type, length, buffer);
+    }
+
+    protected int handleUnknownDataPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
+        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_DATA, id, type, length, buffer);
+        if (err != null) {
+            throw err;
+        }
+
+        return 0;
+    }
+
+    @Override
+    public void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException {
+        // do some bounds checking first
+        if ((fileOffset < 0) || (srcOffset < 0) || (len < 0)) {
+            throw new IllegalArgumentException("write(" + handle + ") please ensure all parameters "
+                    + " are non-negative values: file-offset=" + fileOffset
+                    + ", src-offset=" + srcOffset + ", len=" + len);
+        }
+        if ((srcOffset + len) > src.length) {
+            throw new IllegalArgumentException("write(" + handle + ")"
+                    + " cannot read bytes " + srcOffset + " to " + (srcOffset + len)
+                    + " when array is only of length " + src.length);
+        }
+        if (!isOpen()) {
+            throw new IOException("write(" + handle + "/" + fileOffset + ")[" + srcOffset + "/" + len + "] client is closed");
+        }
+
+        if (log.isTraceEnabled()) {
+            log.trace("write({}) handle={}, file-offset={}, buf-offset={}, len={}",
+                      getClientChannel(), handle, fileOffset, srcOffset, len);
+        }
+
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + len + Long.SIZE /* some extra fields */, false);
+        buffer.putBytes(id);
+        buffer.putLong(fileOffset);
+        buffer.putBytes(src, srcOffset, len);
+        checkCommandStatus(SftpConstants.SSH_FXP_WRITE, buffer);
+    }
+
+    @Override
+    public void mkdir(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("mkdir(" + path + ") client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("mkdir({}) {}", getClientSession(), path);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
+        buffer.putString(path);
+        buffer.putInt(0);
+
+        int version = getVersion();
+        if (version != SftpConstants.SFTP_V3) {
+            buffer.putByte((byte) 0);
+        }
+
+        checkCommandStatus(SftpConstants.SSH_FXP_MKDIR, buffer);
+    }
+
+    @Override
+    public void rmdir(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("rmdir(" + path + ") client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("rmdir({}) {}", getClientSession(), path);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
+        buffer.putString(path);
+        checkCommandStatus(SftpConstants.SSH_FXP_RMDIR, buffer);
+    }
+
+    @Override
+    public CloseableHandle openDir(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("openDir(" + path + ") client is closed");
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
+        buffer.putString(path);
+
+        CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPENDIR, buffer));
+        if (log.isTraceEnabled()) {
+            log.trace("openDir({})[{}}: {}", getClientSession(), path, handle);
+        }
+
+        return handle;
+    }
+
+    @Override
+    public List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException {
+        if (eolIndicator != null) {
+            eolIndicator.set(null);    // assume unknown information
+        }
+        if (!isOpen()) {
+            throw new IOException("readDir(" + handle + ") client is closed");
+        }
+
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* some extra fields */, false);
+        buffer.putBytes(id);
+
+        int cmdId = send(SftpConstants.SSH_FXP_READDIR, buffer);
+        Buffer response = receive(cmdId);
+        return checkDirResponse(SftpConstants.SSH_FXP_READDIR, response, eolIndicator);
+    }
+
+    protected List<DirEntry> checkDirResponse(int cmd, Buffer buffer, AtomicReference<Boolean> eolIndicator) throws IOException {
+        if (eolIndicator != null) {
+            eolIndicator.set(null);    // assume unknown
+        }
+
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SftpConstants.SSH_FXP_NAME) {
+            int len = buffer.getInt();
+            int version = getVersion();
+            ClientChannel channel = getClientChannel();
+            if (log.isDebugEnabled()) {
+                log.debug("checkDirResponse({}}[id={}] reading {} entries", channel, id, len);
+            }
+
+            List<DirEntry> entries = new ArrayList<>(len);
+            for (int i = 0; i < len; i++) {
+                String name = getReferencedName(cmd, buffer);
+                String longName = (version == SftpConstants.SFTP_V3) ? getReferencedName(cmd, buffer) : null;
+                Attributes attrs = readAttributes(cmd, buffer);
+                if (log.isTraceEnabled()) {
+                    log.trace("checkDirResponse({})[id={}][{}] ({})[{}]: {}",
+                              channel, id, i, name, longName, attrs);
+                }
+
+                entries.add(new DirEntry(name, longName, attrs));
+            }
+
+            Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
+            if (eolIndicator != null) {
+                eolIndicator.set(indicator);
+            }
+
+            if (log.isDebugEnabled()) {
+                log.debug("checkDirResponse({}}[id={}] read count={}, eol={}", channel, entries.size(), indicator);
+            }
+            return entries;
+        }
+
+        if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkDirResponse({})[id={}] - status: {} [{}] {}",
+                          getClientChannel(), id, SftpConstants.getStatusName(substatus), lang, msg);
+            }
+
+            if (substatus == SftpConstants.SSH_FX_EOF) {
+                return null;
+            }
+
+            throwStatusException(cmd, id, substatus, msg, lang);
+        }
+
+        return handleUnknownDirListingPacket(cmd, id, type, length, buffer);
+    }
+
+    protected List<DirEntry> handleUnknownDirListingPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
+        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer);
+        if (err != null) {
+            throw err;
+        }
+        return Collections.emptyList();
+    }
+
+    protected IOException handleUnexpectedPacket(int cmd, int expected, int id, int type, int length, Buffer buffer) throws IOException {
+        throw new SshException("Unexpected SFTP packet received while awaiting " + SftpConstants.getCommandMessageName(expected)
+                        + " response to " + SftpConstants.getCommandMessageName(cmd)
+                        + ": type=" + SftpConstants.getCommandMessageName(type) + ", id=" + id + ", length=" + length);
+    }
+
+    @Override
+    public String canonicalPath(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("canonicalPath(" + path + ") client is closed");
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
+        buffer.putString(path);
+        return checkOneName(SftpConstants.SSH_FXP_REALPATH, buffer);
+    }
+
+    @Override
+    public Attributes stat(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("stat(" + path + ") client is closed");
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
+        buffer.putString(path);
+
+        int version = getVersion();
+        if (version >= SftpConstants.SFTP_V4) {
+            buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
+        }
+
+        return checkAttributes(SftpConstants.SSH_FXP_STAT, buffer);
+    }
+
+    @Override
+    public Attributes lstat(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("lstat(" + path + ") client is closed");
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
+        buffer.putString(path);
+
+        int version = getVersion();
+        if (version >= SftpConstants.SFTP_V4) {
+            buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
+        }
+
+        return checkAttributes(SftpConstants.SSH_FXP_LSTAT, buffer);
+    }
+
+    @Override
+    public Attributes stat(Handle handle) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("stat(" + handle + ") client is closed");
+        }
+
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* a bit extra */, false);
+        buffer.putBytes(id);
+
+        int version = getVersion();
+        if (version >= SftpConstants.SFTP_V4) {
+            buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
+        }
+
+        return checkAttributes(SftpConstants.SSH_FXP_FSTAT, buffer);
+    }
+
+    @Override
+    public void setStat(String path, Attributes attributes) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("setStat(" + path + ")[" + attributes + "] client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("setStat({})[{}]: {}", getClientSession(), path, attributes);
+        }
+
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(path);
+        writeAttributes(buffer, attributes);
+        checkCommandStatus(SftpConstants.SSH_FXP_SETSTAT, buffer);
+    }
+
+    @Override
+    public void setStat(Handle handle, Attributes attributes) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("setStat(" + handle + ")[" + attributes + "] client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("setStat({})[{}]: {}", getClientSession(), handle, attributes);
+        }
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + (2 * Long.SIZE) /* some extras */, false);
+        buffer.putBytes(id);
+        writeAttributes(buffer, attributes);
+        checkCommandStatus(SftpConstants.SSH_FXP_FSETSTAT, buffer);
+    }
+
+    @Override
+    public String readLink(String path) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("readLink(" + path + ") client is closed");
+        }
+
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
+        buffer.putString(path);
+        return checkOneName(SftpConstants.SSH_FXP_READLINK, buffer);
+    }
+
+    @Override
+    public void link(String linkPath, String targetPath, boolean symbolic) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("link(" + linkPath + " => " + targetPath + ")[symbolic=" + symbolic + "] client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("link({})[symbolic={}] {} => {}", getClientSession(), symbolic, linkPath, targetPath);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */, false);
+        int version = getVersion();
+        if (version < SftpConstants.SFTP_V6) {
+            if (!symbolic) {
+                throw new UnsupportedOperationException("Hard links are not supported in sftp v" + version);
+            }
+            buffer.putString(targetPath);
+            buffer.putString(linkPath);
+            checkCommandStatus(SftpConstants.SSH_FXP_SYMLINK, buffer);
+        } else {
+            buffer.putString(targetPath);
+            buffer.putString(linkPath);
+            buffer.putBoolean(symbolic);
+            checkCommandStatus(SftpConstants.SSH_FXP_LINK, buffer);
+        }
+    }
+
+    @Override
+    public void lock(Handle handle, long offset, long length, int mask) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("lock(" + handle + ")[offset=" + offset + ", length=" + length + ", mask=0x" + Integer.toHexString(mask) + "] client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("lock({})[{}] offset={}, length={}, mask=0x{}",
+                      getClientSession(), handle, offset, length, Integer.toHexString(mask));
+        }
+
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false);
+        buffer.putBytes(id);
+        buffer.putLong(offset);
+        buffer.putLong(length);
+        buffer.putInt(mask);
+        checkCommandStatus(SftpConstants.SSH_FXP_BLOCK, buffer);
+    }
+
+    @Override
+    public void unlock(Handle handle, long offset, long length) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("unlock" + handle + ")[offset=" + offset + ", length=" + length + "] client is closed");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("unlock({})[{}] offset={}, length={}", getClientSession(), handle, offset, length);
+        }
+
+        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
+        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false);
+        buffer.putBytes(id);
+        buffer.putLong(offset);
+        buffer.putLong(length);
+        checkCommandStatus(SftpConstants.SSH_FXP_UNBLOCK, buffer);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java
new file mode 100644
index 0000000..0fce423
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java
@@ -0,0 +1,92 @@
+/*
+ * 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.subsystem.sftp.impl;
+
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileAttributeView;
+import java.util.Objects;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpFileSystem;
+import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
+import org.apache.sshd.client.subsystem.sftp.SftpPath;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractSftpFileAttributeView extends AbstractLoggingBean implements FileAttributeView {
+    protected final SftpFileSystemProvider provider;
+    protected final Path path;
+    protected final LinkOption[] options;
+
+    protected AbstractSftpFileAttributeView(SftpFileSystemProvider provider, Path path, LinkOption... options) {
+        this.provider = Objects.requireNonNull(provider, "No file system provider instance");
+        this.path = Objects.requireNonNull(path, "No path");
+        this.options = options;
+    }
+
+    @Override
+    public String name() {
+        return "view";
+    }
+
+    /**
+     * @return The underlying {@link SftpFileSystemProvider} used to
+     * provide the view functionality
+     */
+    public final SftpFileSystemProvider provider() {
+        return provider;
+    }
+
+    /**
+     * @return The referenced view {@link Path}
+     */
+    public final Path getPath() {
+        return path;
+    }
+
+    protected SftpClient.Attributes readRemoteAttributes() throws IOException {
+        return provider.readRemoteAttributes(provider.toSftpPath(path), options);
+    }
+
+    protected void writeRemoteAttributes(SftpClient.Attributes attrs) throws IOException {
+        SftpPath p = provider.toSftpPath(path);
+        SftpFileSystem fs = p.getFileSystem();
+        try (SftpClient client = fs.getClient()) {
+            try {
+                if (log.isDebugEnabled()) {
+                    log.debug("writeRemoteAttributes({})[{}]: {}", fs, p, attrs);
+                }
+                client.setStat(p.toString(), attrs);
+            } catch (SftpException e) {
+                if (e.getStatus() == SftpConstants.SSH_FX_NO_SUCH_FILE) {
+                    throw new NoSuchFileException(p.toString());
+                }
+                throw e;
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultCloseableHandle.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultCloseableHandle.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultCloseableHandle.java
new file mode 100644
index 0000000..f6597f3
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultCloseableHandle.java
@@ -0,0 +1,66 @@
+/*
+ * 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.subsystem.sftp.impl;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.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 path, byte[] id) {
+        super(path, id);
+        this.client = ValidateUtils.checkNotNull(client, "No client for path=%s", path);
+    }
+
+    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);
+        }
+    }
+
+    @Override   // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override   // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
+    public boolean equals(Object obj) {
+        return super.equals(obj);
+    }
+}


[3/3] mina-sshd git commit: [SSHD-790] Allow users to register a custom SFTP client factory

Posted by lg...@apache.org.
[SSHD-790] Allow users to register a custom SFTP client factory


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

Branch: refs/heads/master
Commit: 154287462cde88314cc7cd7c44978fc04a7c9b88
Parents: c5b163f
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Thu Dec 28 10:37:12 2017 +0200
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Thu Dec 28 11:06:38 2017 +0200

----------------------------------------------------------------------
 README.md                                       |  111 ++
 .../sshd/client/ClientFactoryManager.java       |    2 +
 .../java/org/apache/sshd/client/SshClient.java  |   13 +
 .../client/session/AbstractClientSession.java   |   66 +-
 .../sshd/client/session/ClientSession.java      |    3 +-
 .../subsystem/sftp/AbstractSftpClient.java      | 1127 -----------------
 .../sftp/AbstractSftpFileAttributeView.java     |   88 --
 .../subsystem/sftp/DefaultCloseableHandle.java  |   65 -
 .../subsystem/sftp/DefaultSftpClient.java       |  461 -------
 .../sftp/SftpAclFileAttributeView.java          |    2 +
 .../sshd/client/subsystem/sftp/SftpClient.java  |    2 +-
 .../subsystem/sftp/SftpClientCreator.java       |   20 +-
 .../subsystem/sftp/SftpClientFactory.java       |   51 +
 .../sftp/SftpClientFactoryManager.java          |   37 +
 .../client/subsystem/sftp/SftpFileSystem.java   |    1 +
 .../subsystem/sftp/SftpFileSystemProvider.java  |    2 +-
 .../sftp/SftpPosixFileAttributeView.java        |    1 +
 .../subsystem/sftp/impl/AbstractSftpClient.java | 1134 ++++++++++++++++++
 .../impl/AbstractSftpFileAttributeView.java     |   92 ++
 .../sftp/impl/DefaultCloseableHandle.java       |   66 +
 .../subsystem/sftp/impl/DefaultSftpClient.java  |  462 +++++++
 .../sftp/impl/DefaultSftpClientFactory.java     |   81 ++
 .../sftp/DefaultCloseableHandleTest.java        |    1 +
 23 files changed, 2089 insertions(+), 1799 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index fb6560b..4e99d3e 100644
--- a/README.md
+++ b/README.md
@@ -543,6 +543,74 @@ configuration key. For more advanced restrictions one needs to sub-class `SftpSu
 `SftpSubsystemFactory` that uses the sub-classed code.
 
 
+### Registering a custom `SftpClientFactory`
+
+The code creates `SftpClient`-s and `SftpFileSystem`-s using a default built-in `SftpClientFactory` instance (see
+`DefaultSftpClientFactory`). Users may choose to register a custom factory in order to provide their own
+implementations - e.g., in order to override some default behavior. The custom factory may be registered either at
+the client or session level - e.g.:
+
+```java
+
+    SshClient client = ... setup client...
+    client.setSftpClientFactory(new MySuperDuperSftpClientFactory());
+
+    try (ClientSession session = client.connect(user, host, port).verify(timeout).getSession()) {
+        // override the default factory with a special one - but only for this session
+        session.setSftpClientFactory(new SpecialSessionSftpClientFactory());
+        session.addPasswordIdentity(password);
+        session.auth.verify(timeout);
+        
+        try (SftpClient sftp = session.createSftpClient()) {
+            ... instance created through SpecialSessionSftpClientFactory ...
+        }
+    }    
+    
+```
+
+If no factory provided or factory set to _null_ then code reverts to using the default built-in one. **Note:** setting
+the factory to _null_ on the session level, simply delegates the creation to whatever factory is registered at the
+client level - default or custom.
+
+```java
+
+    SshClient client = ... setup client...
+    client.setSftpClientFactory(new MySuperDuperSftpClientFactory());
+
+    try (ClientSession session = client.connect(user, host, port).verify(timeout).getSession()) {
+        // override the default factory with a special one - but only for this session
+        session.setSftpClientFactory(new SpecialSessionSftpClientFactory());
+        session.addPasswordIdentity(password);
+        session.auth.verify(timeout);
+        
+        try (SftpClient sftp = session.createSftpClient()) {
+            ... instance created through SpecialSessionSftpClientFactory ...
+        }
+
+        // revert to one from client
+        session.setSftpClientFactory(null);
+
+        try (SftpClient sftp = session.createSftpClient()) {
+            ... instance created through MySuperDuperSftpClientFactory ...
+        }
+        
+        // remove client-level factory
+        client.setSftpClientFactory(null);
+
+        try (SftpClient sftp = session.createSftpClient()) {
+            ... instance created through built-in DefaultSftpClientFactory ...
+        }
+
+        // re-instate session-level factory        
+        session.setSftpClientFactory(new SpecialSessionSftpClientFactory());
+
+        try (SftpClient sftp = session.createSftpClient()) {
+            ... instance created through SpecialSessionSftpClientFactory ...
+        }
+    }    
+    
+```
+
 ### Using `SftpFileSystemProvider` to create an `SftpFileSystem`
 
 
@@ -716,6 +784,49 @@ UTF-8 is used. **Note:** the value can be a charset name or a `java.nio.charset.
 
 ```
 
+Another option is to register a custom `SftpClientFactory` and create a `DefaultSftpClient` that overrides `getReferencedName` method:
+
+```java
+
+public class MyCustomSftpClient extends DefaultSftpClient {
+    public MyCustomSftpClient(ClientSession session) {
+        super(session);
+    }
+    
+    @Override
+    protected String getReferencedName(int cmd, Buffer buf) {
+        byte[] bytes = buf.getBytes();
+        Charset cs = detectCharset(bytes);
+        return new String(bytes, cs);
+    }
+}
+
+public class MyCustomSftpClientFactory extends DefaultSftpClientFactory {
+    public MyCustomSftpClientFactory() {
+        super();
+    }
+    
+    protected DefaultSftpClient createDefaultSftpClient(ClientSession session, SftpVersionSelector selector) throws IOException {
+        return MyCustomSftpClient(session);
+    }
+}
+
+    // Usage - register at client level and affect ALL SFTP interactions
+    SshClient client = ... setup/obtain an instance...
+    client.setSftpClientFactory(new MyCustomSftpClientFactory());
+
+    // Usage - selective session registration
+    SshClient client = ... setup/obtain an instance...
+    try (ClientSession session = client.connect(...)) {
+        if (...something special about the host/port/etc....) {
+            // affect only SFTP interactions for this session
+            session.setSftpClientFactory(new MyCustomSftpClientFactory());
+        }
+    }
+
+
+```
+
 ### Supported SFTP extensions
 
 Both client and server support several of the SFTP extensions specified in various drafts:

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
index 31b2a22..7627020 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
@@ -21,6 +21,7 @@ package org.apache.sshd.client;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.client.config.keys.ClientIdentityLoader;
 import org.apache.sshd.client.session.ClientProxyConnectorHolder;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactoryManager;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.scp.ScpFileOpenerHolder;
@@ -33,6 +34,7 @@ import org.apache.sshd.common.scp.ScpFileOpenerHolder;
  */
 public interface ClientFactoryManager
         extends FactoryManager,
+                SftpClientFactoryManager,
                 ScpFileOpenerHolder,
                 ClientProxyConnectorHolder,
                 ClientAuthenticationManager {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 0e8979e..6c5c291 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -86,6 +86,8 @@ import org.apache.sshd.client.session.ClientUserAuthServiceFactory;
 import org.apache.sshd.client.session.SessionFactory;
 import org.apache.sshd.client.simple.AbstractSimpleClientSessionCreator;
 import org.apache.sshd.client.simple.SimpleClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
+import org.apache.sshd.client.subsystem.sftp.impl.DefaultSftpClientFactory;
 import org.apache.sshd.common.Closeable;
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.FactoryManager;
@@ -211,6 +213,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     private FilePasswordProvider filePasswordProvider;
     private PasswordIdentityProvider passwordIdentityProvider;
     private ScpFileOpener scpOpener;
+    private SftpClientFactory sftpClientFactory;
 
     private final List<Object> identities = new CopyOnWriteArrayList<>();
     private final AuthenticationIdentitiesProvider identitiesProvider;
@@ -248,6 +251,16 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     }
 
     @Override
+    public SftpClientFactory getSftpClientFactory() {
+        return (sftpClientFactory == null) ? DefaultSftpClientFactory.INSTANCE : sftpClientFactory;
+    }
+
+    @Override
+    public void setSftpClientFactory(SftpClientFactory sftpClientFactory) {
+        this.sftpClientFactory = sftpClientFactory;
+    }
+
+    @Override
     public ServerKeyVerifier getServerKeyVerifier() {
         return serverKeyVerifier;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index b67310d..bc44d58 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -43,10 +43,8 @@ import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.scp.DefaultScpClient;
 import org.apache.sshd.client.scp.ScpClient;
-import org.apache.sshd.client.subsystem.sftp.DefaultSftpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpFileSystem;
-import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
 import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedFactory;
@@ -92,6 +90,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private ScpFileOpener scpOpener;
     private SocketAddress connectAddress;
     private ClientProxyConnector proxyConnector;
+    private SftpClientFactory sftpClientFactory;
 
     protected AbstractClientSession(ClientFactoryManager factoryManager, IoSession ioSession) {
         super(false, factoryManager, ioSession);
@@ -168,6 +167,16 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
+    public SftpClientFactory getSftpClientFactory() {
+        return resolveEffectiveProvider(SftpClientFactory.class, sftpClientFactory, getFactoryManager().getSftpClientFactory());
+    }
+
+    @Override
+    public void setSftpClientFactory(SftpClientFactory sftpClientFactory) {
+        this.sftpClientFactory = sftpClientFactory;
+    }
+
+    @Override
     public void addPasswordIdentity(String password) {
         // DO NOT USE checkNotNullOrNotEmpty SINCE IT TRIMS THE RESULT
         ValidateUtils.checkTrue((password != null) && (!password.isEmpty()), "No password provided");
@@ -333,57 +342,14 @@ public abstract class AbstractClientSession extends AbstractSession implements C
 
     @Override
     public SftpClient createSftpClient(SftpVersionSelector selector) throws IOException {
-        DefaultSftpClient client = new DefaultSftpClient(this);
-        try {
-            client.negotiateVersion(selector);
-        } catch (IOException | RuntimeException e) {
-            if (log.isDebugEnabled()) {
-                log.debug("createSftpClient({}) failed ({}) to negotiate version: {}",
-                          this, e.getClass().getSimpleName(), e.getMessage());
-            }
-            if (log.isTraceEnabled()) {
-                log.trace("createSftpClient(" + this + ") version negotiation failure details", e);
-            }
-
-            client.close();
-            throw e;
-        }
-
-        return client;
-    }
-
-    @Override
-    public FileSystem createSftpFileSystem() throws IOException {
-        return createSftpFileSystem(SftpVersionSelector.CURRENT);
-    }
-
-    @Override
-    public FileSystem createSftpFileSystem(int version) throws IOException {
-        return createSftpFileSystem(SftpVersionSelector.fixedVersionSelector(version));
-    }
-
-    @Override
-    public FileSystem createSftpFileSystem(SftpVersionSelector selector) throws IOException {
-        return createSftpFileSystem(selector, SftpClient.DEFAULT_READ_BUFFER_SIZE, SftpClient.DEFAULT_WRITE_BUFFER_SIZE);
-    }
-
-    @Override
-    public FileSystem createSftpFileSystem(int version, int readBufferSize, int writeBufferSize) throws IOException {
-        return createSftpFileSystem(SftpVersionSelector.fixedVersionSelector(version), readBufferSize, writeBufferSize);
-    }
-
-    @Override
-    public FileSystem createSftpFileSystem(int readBufferSize, int writeBufferSize) throws IOException {
-        return createSftpFileSystem(SftpVersionSelector.CURRENT, readBufferSize, writeBufferSize);
+        SftpClientFactory factory = getSftpClientFactory();
+        return factory.createSftpClient(this, selector);
     }
 
     @Override
     public FileSystem createSftpFileSystem(SftpVersionSelector selector, int readBufferSize, int writeBufferSize) throws IOException {
-        SftpFileSystemProvider provider = new SftpFileSystemProvider((org.apache.sshd.client.SshClient) getFactoryManager(), selector);
-        SftpFileSystem fs = provider.newFileSystem(this);
-        fs.setReadBufferSize(readBufferSize);
-        fs.setWriteBufferSize(writeBufferSize);
-        return fs;
+        SftpClientFactory factory = getSftpClientFactory();
+        return factory.createSftpFileSystem(this, selector, readBufferSize, writeBufferSize);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index 8c074f2..b807f17 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -46,6 +46,7 @@ import org.apache.sshd.client.scp.ScpClientCreator;
 import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker;
 import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
 import org.apache.sshd.client.subsystem.sftp.SftpClientCreator;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactoryManager;
 import org.apache.sshd.common.forward.PortForwardingManager;
 import org.apache.sshd.common.future.KeyExchangeFuture;
 import org.apache.sshd.common.session.Session;
@@ -82,7 +83,7 @@ import org.apache.sshd.common.util.net.SshdSocketAddress;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public interface ClientSession
-            extends Session, ScpClientCreator, SftpClientCreator,
+            extends Session, ScpClientCreator, SftpClientCreator, SftpClientFactoryManager,
             ClientProxyConnectorHolder, ClientAuthenticationManager,
             PortForwardingManager {
     enum ClientSessionEvent {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
deleted file mode 100644
index fd2875c..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
+++ /dev/null
@@ -1,1127 +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.subsystem.sftp;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.nio.file.attribute.FileTime;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.apache.sshd.client.channel.ClientChannel;
-import org.apache.sshd.client.subsystem.AbstractSubsystemClient;
-import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
-import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
-import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtensionFactory;
-import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.channel.Channel;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.subsystem.sftp.SftpException;
-import org.apache.sshd.common.subsystem.sftp.SftpHelper;
-import org.apache.sshd.common.subsystem.sftp.SftpUniversalOwnerAndGroup;
-import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
-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;
-
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class AbstractSftpClient extends AbstractSubsystemClient implements SftpClient, RawSftpClient {
-    private final Attributes fileOpenAttributes = new Attributes();
-    private final AtomicReference<Map<String, Object>> parsedExtensionsHolder = new AtomicReference<>(null);
-
-    protected AbstractSftpClient() {
-        fileOpenAttributes.setType(SftpConstants.SSH_FILEXFER_TYPE_REGULAR);
-    }
-
-    @Override
-    public Channel getChannel() {
-        return getClientChannel();
-    }
-
-    @Override
-    public <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType) {
-        Object instance = getExtension(BuiltinSftpClientExtensions.fromType(extensionType));
-        if (instance == null) {
-            return null;
-        } else {
-            return extensionType.cast(instance);
-        }
-    }
-
-    @Override
-    public SftpClientExtension getExtension(String extensionName) {
-        return getExtension(BuiltinSftpClientExtensions.fromName(extensionName));
-    }
-
-    protected SftpClientExtension getExtension(SftpClientExtensionFactory factory) {
-        if (factory == null) {
-            return null;
-        }
-
-        Map<String, byte[]> extensions = getServerExtensions();
-        Map<String, Object> parsed = getParsedServerExtensions(extensions);
-        return factory.create(this, this, extensions, parsed);
-    }
-
-    protected Map<String, Object> getParsedServerExtensions() {
-        return getParsedServerExtensions(getServerExtensions());
-    }
-
-    protected Map<String, Object> getParsedServerExtensions(Map<String, byte[]> extensions) {
-        Map<String, Object> parsed = parsedExtensionsHolder.get();
-        if (parsed == null) {
-            parsed = ParserUtils.parse(extensions);
-            if (parsed == null) {
-                parsed = Collections.emptyMap();
-            }
-            parsedExtensionsHolder.set(parsed);
-        }
-
-        return parsed;
-    }
-
-    protected String getReferencedName(Buffer buf) {
-        Charset cs = getNameDecodingCharset();
-        return buf.getString(cs);
-    }
-
-    /**
-     * Sends the specified command, waits for the response and then invokes {@link #checkResponseStatus(int, Buffer)}
-     * @param cmd The command to send
-     * @param request The request {@link Buffer}
-     * @throws IOException If failed to send, receive or check the returned status
-     * @see #send(int, Buffer)
-     * @see #receive(int)
-     * @see #checkResponseStatus(int, Buffer)
-     */
-    protected void checkCommandStatus(int cmd, Buffer request) throws IOException {
-        int reqId = send(cmd, request);
-        Buffer response = receive(reqId);
-        checkResponseStatus(cmd, response);
-    }
-
-    /**
-     * Checks if the incoming response is an {@code SSH_FXP_STATUS} one,
-     * and if so whether the substatus is {@code SSH_FX_OK}.
-     *
-     * @param cmd The sent command opcode
-     * @param buffer The received response {@link Buffer}
-     * @throws IOException If response does not carry a status or carries
-     * a bad status code
-     * @see #checkResponseStatus(int, int, int, String, String)
-     */
-    protected void checkResponseStatus(int cmd, Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SftpConstants.SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            checkResponseStatus(cmd, id, substatus, msg, lang);
-        } else {
-            //noinspection ThrowableResultOfMethodCallIgnored
-            handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_STATUS, id, type, length, buffer);
-        }
-    }
-
-    /**
-     * @param cmd The sent command opcode
-     * @param id The request id
-     * @param substatus The sub-status value
-     * @param msg The message
-     * @param lang The language
-     * @throws IOException if the sub-status is not {@code SSH_FX_OK}
-     * @see #throwStatusException(int, int, int, String, String)
-     */
-    protected void checkResponseStatus(int cmd, int id, int substatus, String msg, String lang) throws IOException {
-        if (log.isTraceEnabled()) {
-            log.trace("checkResponseStatus({})[id={}] cmd={} status={} lang={} msg={}",
-                      getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
-                      SftpConstants.getStatusName(substatus), lang, msg);
-        }
-
-        if (substatus != SftpConstants.SSH_FX_OK) {
-            throwStatusException(cmd, id, substatus, msg, lang);
-        }
-    }
-
-    protected void throwStatusException(int cmd, int id, int substatus, String msg, String lang) throws IOException {
-        throw new SftpException(substatus, msg);
-    }
-
-    /**
-     * @param cmd Command to be sent
-     * @param request The {@link Buffer} containing the request
-     * @return The received handle identifier
-     * @throws IOException If failed to send/receive or process the response
-     * @see #send(int, Buffer)
-     * @see #receive(int)
-     * @see #checkHandleResponse(int, Buffer)
-     */
-    protected byte[] checkHandle(int cmd, Buffer request) throws IOException {
-        int reqId = send(cmd, request);
-        Buffer response = receive(reqId);
-        return checkHandleResponse(cmd, response);
-    }
-
-    protected byte[] checkHandleResponse(int cmd, Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SftpConstants.SSH_FXP_HANDLE) {
-            return ValidateUtils.checkNotNullAndNotEmpty(buffer.getBytes(), "Null/empty handle in buffer", GenericUtils.EMPTY_OBJECT_ARRAY);
-        }
-
-        if (type == SftpConstants.SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkHandleResponse({})[id={}] {} - status: {} [{}] {}",
-                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
-                          SftpConstants.getStatusName(substatus), lang, msg);
-            }
-            throwStatusException(cmd, id, substatus, msg, lang);
-        }
-
-        return handleUnexpectedHandlePacket(cmd, id, type, length, buffer);
-    }
-
-    protected byte[] handleUnexpectedHandlePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
-        handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_HANDLE, id, type, length, buffer);
-        throw new SshException("No handling for unexpected handle packet id=" + id
-                             + ", type=" + SftpConstants.getCommandMessageName(type) + ", length=" + length);
-    }
-
-    /**
-     * @param cmd Command to be sent
-     * @param request Request {@link Buffer}
-     * @return The decoded response {@code Attributes}
-     * @throws IOException If failed to send/receive or process the response
-     * @see #send(int, Buffer)
-     * @see #receive(int)
-     * @see #checkAttributesResponse(int, Buffer)
-     */
-    protected Attributes checkAttributes(int cmd, Buffer request) throws IOException {
-        int reqId = send(cmd, request);
-        Buffer response = receive(reqId);
-        return checkAttributesResponse(cmd, response);
-    }
-
-    protected Attributes checkAttributesResponse(int cmd, Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SftpConstants.SSH_FXP_ATTRS) {
-            return readAttributes(buffer);
-        }
-
-        if (type == SftpConstants.SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkAttributesResponse()[id={}] {} - status: {} [{}] {}",
-                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
-                          SftpConstants.getStatusName(substatus), lang, msg);
-            }
-            throwStatusException(cmd, id, substatus, msg, lang);
-        }
-
-        return handleUnexpectedAttributesPacket(cmd, id, type, length, buffer);
-    }
-
-    protected Attributes handleUnexpectedAttributesPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
-        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_ATTRS, id, type, length, buffer);
-        if (err != null) {
-            throw err;
-        }
-
-        return null;
-    }
-
-    /**
-     * @param cmd Command to be sent
-     * @param request The request {@link Buffer}
-     * @return The retrieved name
-     * @throws IOException If failed to send/receive or process the response
-     * @see #send(int, Buffer)
-     * @see #receive(int)
-     * @see #checkOneNameResponse(int, Buffer)
-     */
-    protected String checkOneName(int cmd, Buffer request) throws IOException {
-        int reqId = send(cmd, request);
-        Buffer response = receive(reqId);
-        return checkOneNameResponse(cmd, response);
-    }
-
-    protected String checkOneNameResponse(int cmd, Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SftpConstants.SSH_FXP_NAME) {
-            int len = buffer.getInt();
-            if (len != 1) {
-                throw new SshException("SFTP error: received " + len + " names instead of 1");
-            }
-            String name = getReferencedName(buffer);
-            String longName = null;
-            int version = getVersion();
-            if (version == SftpConstants.SFTP_V3) {
-                longName = getReferencedName(buffer);
-            }
-
-            Attributes attrs = readAttributes(buffer);
-            Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
-            // TODO decide what to do if not-null and not TRUE
-            if (log.isTraceEnabled()) {
-                log.trace("checkOneNameResponse({})[id={}] {} ({})[{}] eol={}: {}",
-                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
-                          name, longName, indicator, attrs);
-            }
-            return name;
-        }
-
-        if (type == SftpConstants.SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkOneNameResponse({})[id={}] {} status: {} [{}] {}",
-                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
-                          SftpConstants.getStatusName(substatus), lang, msg);
-            }
-
-            throwStatusException(cmd, id, substatus, msg, lang);
-        }
-
-        return handleUnknownOneNamePacket(cmd, id, type, length, buffer);
-    }
-
-    protected String handleUnknownOneNamePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
-        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer);
-        if (err != null) {
-            throw err;
-        }
-
-        return null;
-    }
-
-    protected Attributes readAttributes(Buffer buffer) throws IOException {
-        Attributes attrs = new Attributes();
-        int flags = buffer.getInt();
-        int version = getVersion();
-        if (version == SftpConstants.SFTP_V3) {
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
-                attrs.setSize(buffer.getLong());
-            }
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
-                attrs.owner(buffer.getInt(), buffer.getInt());
-            }
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                int perms = buffer.getInt();
-                attrs.setPermissions(perms);
-                attrs.setType(SftpHelper.permissionsToFileType(perms));
-            }
-
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags));
-                attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags));
-            }
-        } else if (version >= SftpConstants.SFTP_V4) {
-            attrs.setType(buffer.getUByte());
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
-                attrs.setSize(buffer.getLong());
-            }
-
-            if ((version >= SftpConstants.SFTP_V6) && ((flags & SftpConstants.SSH_FILEXFER_ATTR_ALLOCATION_SIZE) != 0)) {
-                @SuppressWarnings("unused")
-                long allocSize = buffer.getLong();    // TODO handle allocation size
-            }
-
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-                attrs.setOwner(buffer.getString());
-                attrs.setGroup(buffer.getString());
-            }
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                attrs.setPermissions(buffer.getInt());
-            }
-
-            // update the permissions according to the type
-            int perms = attrs.getPermissions();
-            perms |= SftpHelper.fileTypeToPermission(attrs.getType());
-            attrs.setPermissions(perms);
-
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags));
-            }
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                attrs.setCreateTime(SftpHelper.readTime(buffer, version, flags));
-            }
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags));
-            }
-            if ((version >= SftpConstants.SFTP_V6) && (flags & SftpConstants.SSH_FILEXFER_ATTR_CTIME) != 0) {
-                @SuppressWarnings("unused")
-                FileTime attrsChangedTime = SftpHelper.readTime(buffer, version, flags);    // TODO the last time the file attributes were changed
-            }
-
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
-                attrs.setAcl(SftpHelper.readACLs(buffer, version));
-            }
-
-            if ((flags & SftpConstants.SSH_FILEXFER_ATTR_BITS) != 0) {
-                @SuppressWarnings("unused")
-                int bits = buffer.getInt();
-                @SuppressWarnings("unused")
-                int valid = 0xffffffff;
-                if (version >= SftpConstants.SFTP_V6) {
-                    valid = buffer.getInt();
-                }
-                // TODO: handle attrib bits
-            }
-
-            if (version >= SftpConstants.SFTP_V6) {
-                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_TEXT_HINT) != 0) {
-                    @SuppressWarnings("unused")
-                    boolean text = buffer.getBoolean(); // TODO: handle text
-                }
-                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MIME_TYPE) != 0) {
-                    @SuppressWarnings("unused")
-                    String mimeType = buffer.getString(); // TODO: handle mime-type
-                }
-                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_LINK_COUNT) != 0) {
-                    @SuppressWarnings("unused")
-                    int nlink = buffer.getInt(); // TODO: handle link-count
-                }
-                if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UNTRANSLATED_NAME) != 0) {
-                    @SuppressWarnings("unused")
-                    String untranslated = getReferencedName(buffer); // TODO: handle untranslated-name
-                }
-            }
-        } else {
-            throw new IllegalStateException("readAttributes - unsupported version: " + version);
-        }
-
-        if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
-            attrs.setExtensions(SftpHelper.readExtensions(buffer));
-        }
-
-        return attrs;
-    }
-
-    protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
-        int version = getVersion();
-        int flagsMask = 0;
-        Collection<Attribute> flags = Objects.requireNonNull(attributes, "No attributes").getFlags();
-        if (version == SftpConstants.SFTP_V3) {
-            for (Attribute a : flags) {
-                switch (a) {
-                    case Size:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
-                        break;
-                    case UidGid:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_UIDGID;
-                        break;
-                    case Perms:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
-                        break;
-                    case AccessTime:
-                        if (flags.contains(Attribute.ModifyTime)) {
-                            flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
-                        }
-                        break;
-                    case ModifyTime:
-                        if (flags.contains(Attribute.AccessTime)) {
-                            flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
-                        }
-                        break;
-                    case Extensions:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
-                        break;
-                    default:    // do nothing
-                }
-            }
-            buffer.putInt(flagsMask);
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(attributes.getSize());
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
-                buffer.putInt(attributes.getUserId());
-                buffer.putInt(attributes.getGroupId());
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributes.getPermissions());
-            }
-
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime());
-                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime());
-            }
-        } else if (version >= SftpConstants.SFTP_V4) {
-            for (Attribute a : flags) {
-                switch (a) {
-                    case Size:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
-                        break;
-                    case OwnerGroup:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP;
-                        break;
-                    case Perms:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
-                        break;
-                    case AccessTime:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME;
-                        break;
-                    case ModifyTime:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME;
-                        break;
-                    case CreateTime:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_CREATETIME;
-                        break;
-                    case Acl:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACL;
-                        break;
-                    case Extensions:
-                        flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
-                        break;
-                    default:    // do nothing
-                }
-            }
-            buffer.putInt(flagsMask);
-            buffer.putByte((byte) attributes.getType());
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(attributes.getSize());
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-                String owner = attributes.getOwner();
-                buffer.putString(GenericUtils.isEmpty(owner) ? SftpUniversalOwnerAndGroup.Owner.getName() : owner);
-
-                String group = attributes.getGroup();
-                buffer.putString(GenericUtils.isEmpty(group) ? SftpUniversalOwnerAndGroup.Group.getName() : group);
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributes.getPermissions());
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime());
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getCreateTime());
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime());
-            }
-            if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
-                SftpHelper.writeACLs(buffer, version, attributes.getAcl());
-            }
-
-            // TODO: for v6+ add CTIME (see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-21)
-        } else {
-            throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + version);
-        }
-
-        if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
-            SftpHelper.writeExtensions(buffer, attributes.getExtensions());
-        }
-    }
-
-    @Override
-    public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("open(" + path + ")[" + options + "] client is closed");
-        }
-
-        /*
-         * Be consistent with FileChannel#open - if no mode specified then READ is assumed
-         */
-        if (GenericUtils.isEmpty(options)) {
-            options = EnumSet.of(OpenMode.Read);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
-        buffer.putString(path);
-        int version = getVersion();
-        int mode = 0;
-        if (version < SftpConstants.SFTP_V5) {
-            for (OpenMode m : options) {
-                switch (m) {
-                    case Read:
-                        mode |= SftpConstants.SSH_FXF_READ;
-                        break;
-                    case Write:
-                        mode |= SftpConstants.SSH_FXF_WRITE;
-                        break;
-                    case Append:
-                        mode |= SftpConstants.SSH_FXF_APPEND;
-                        break;
-                    case Create:
-                        mode |= SftpConstants.SSH_FXF_CREAT;
-                        break;
-                    case Truncate:
-                        mode |= SftpConstants.SSH_FXF_TRUNC;
-                        break;
-                    case Exclusive:
-                        mode |= SftpConstants.SSH_FXF_EXCL;
-                        break;
-                    default:    // do nothing
-                }
-            }
-        } else {
-            int access = 0;
-            if (options.contains(OpenMode.Read)) {
-                access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES;
-            }
-            if (options.contains(OpenMode.Write)) {
-                access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES;
-            }
-            if (options.contains(OpenMode.Append)) {
-                access |= SftpConstants.ACE4_APPEND_DATA;
-            }
-            buffer.putInt(access);
-
-            if (options.contains(OpenMode.Create) && options.contains(OpenMode.Exclusive)) {
-                mode |= SftpConstants.SSH_FXF_CREATE_NEW;
-            } else if (options.contains(OpenMode.Create) && options.contains(OpenMode.Truncate)) {
-                mode |= SftpConstants.SSH_FXF_CREATE_TRUNCATE;
-            } else if (options.contains(OpenMode.Create)) {
-                mode |= SftpConstants.SSH_FXF_OPEN_OR_CREATE;
-            } else if (options.contains(OpenMode.Truncate)) {
-                mode |= SftpConstants.SSH_FXF_TRUNCATE_EXISTING;
-            } else {
-                mode |= SftpConstants.SSH_FXF_OPEN_EXISTING;
-            }
-        }
-        buffer.putInt(mode);
-        writeAttributes(buffer, fileOpenAttributes);
-
-        CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPEN, buffer));
-        if (log.isTraceEnabled()) {
-            log.trace("open({})[{}] options={}: {}", getClientSession(), path, options, handle);
-        }
-        return handle;
-    }
-
-    @Override
-    public void close(Handle handle) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("close(" + handle + ") client is closed");
-        }
-
-        if (log.isTraceEnabled()) {
-            log.trace("close({}) {}", getClientSession(), handle);
-        }
-
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false);
-        buffer.putBytes(id);
-        checkCommandStatus(SftpConstants.SSH_FXP_CLOSE, buffer);
-    }
-
-    @Override
-    public void remove(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("remove(" + path + ") client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("remove({}) {}", getClientSession(), path);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
-        buffer.putString(path);
-        checkCommandStatus(SftpConstants.SSH_FXP_REMOVE, buffer);
-    }
-
-    @Override
-    public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("rename(" + oldPath + " => " + newPath + ")[" + options + "] client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("rename({}) {} => {}", getClientSession(), oldPath, newPath);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */, false);
-        buffer.putString(oldPath);
-        buffer.putString(newPath);
-
-        int numOptions = GenericUtils.size(options);
-        int version = getVersion();
-        if (version >= SftpConstants.SFTP_V5) {
-            int opts = 0;
-            if (numOptions > 0) {
-                for (CopyMode opt : options) {
-                    switch (opt) {
-                        case Atomic:
-                            opts |= SftpConstants.SSH_FXP_RENAME_ATOMIC;
-                            break;
-                        case Overwrite:
-                            opts |= SftpConstants.SSH_FXP_RENAME_OVERWRITE;
-                            break;
-                        default:    // do nothing
-                    }
-                }
-            }
-            buffer.putInt(opts);
-        } else if (numOptions > 0) {
-            throw new UnsupportedOperationException("rename(" + oldPath + " => " + newPath + ")"
-                            + " - copy options can not be used with this SFTP version: " + options);
-        }
-        checkCommandStatus(SftpConstants.SSH_FXP_RENAME, buffer);
-    }
-
-    @Override
-    public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException {
-        if (eofSignalled != null) {
-            eofSignalled.set(null);
-        }
-
-        if (!isOpen()) {
-            throw new IOException("read(" + handle + "/" + fileOffset + ")[" + dstOffset + "/" + len + "] client is closed");
-        }
-
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false);
-        buffer.putBytes(id);
-        buffer.putLong(fileOffset);
-        buffer.putInt(len);
-        return checkData(SftpConstants.SSH_FXP_READ, buffer, dstOffset, dst, eofSignalled);
-    }
-
-    protected int checkData(int cmd, Buffer request, int dstOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
-        if (eofSignalled != null) {
-            eofSignalled.set(null);
-        }
-        int reqId = send(cmd, request);
-        Buffer response = receive(reqId);
-        return checkDataResponse(cmd, response, dstOffset, dst, eofSignalled);
-    }
-
-    protected int checkDataResponse(int cmd, Buffer buffer, int dstoff, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
-        if (eofSignalled != null) {
-            eofSignalled.set(null);
-        }
-
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SftpConstants.SSH_FXP_DATA) {
-            int len = buffer.getInt();
-            buffer.getRawBytes(dst, dstoff, len);
-            Boolean indicator = SftpHelper.getEndOfFileIndicatorValue(buffer, getVersion());
-            if (log.isTraceEnabled()) {
-                log.trace("checkDataResponse({}][id={}] {} offset={}, len={}, EOF={}",
-                          getClientChannel(), SftpConstants.getCommandMessageName(cmd),
-                          id, dstoff, len, indicator);
-            }
-            if (eofSignalled != null) {
-                eofSignalled.set(indicator);
-            }
-
-            return len;
-        }
-
-        if (type == SftpConstants.SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkDataResponse({})[id={}] {} status: {} [{}] {}",
-                          getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
-                          SftpConstants.getStatusName(substatus), lang, msg);
-            }
-
-            if (substatus == SftpConstants.SSH_FX_EOF) {
-                return -1;
-            }
-
-            throwStatusException(cmd, id, substatus, msg, lang);
-        }
-
-        return handleUnknownDataPacket(cmd, id, type, length, buffer);
-    }
-
-    protected int handleUnknownDataPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
-        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_DATA, id, type, length, buffer);
-        if (err != null) {
-            throw err;
-        }
-
-        return 0;
-    }
-
-    @Override
-    public void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException {
-        // do some bounds checking first
-        if ((fileOffset < 0) || (srcOffset < 0) || (len < 0)) {
-            throw new IllegalArgumentException("write(" + handle + ") please ensure all parameters "
-                    + " are non-negative values: file-offset=" + fileOffset
-                    + ", src-offset=" + srcOffset + ", len=" + len);
-        }
-        if ((srcOffset + len) > src.length) {
-            throw new IllegalArgumentException("write(" + handle + ")"
-                    + " cannot read bytes " + srcOffset + " to " + (srcOffset + len)
-                    + " when array is only of length " + src.length);
-        }
-        if (!isOpen()) {
-            throw new IOException("write(" + handle + "/" + fileOffset + ")[" + srcOffset + "/" + len + "] client is closed");
-        }
-
-        if (log.isTraceEnabled()) {
-            log.trace("write({}) handle={}, file-offset={}, buf-offset={}, len={}",
-                      getClientChannel(), handle, fileOffset, srcOffset, len);
-        }
-
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + len + Long.SIZE /* some extra fields */, false);
-        buffer.putBytes(id);
-        buffer.putLong(fileOffset);
-        buffer.putBytes(src, srcOffset, len);
-        checkCommandStatus(SftpConstants.SSH_FXP_WRITE, buffer);
-    }
-
-    @Override
-    public void mkdir(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("mkdir(" + path + ") client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("mkdir({}) {}", getClientSession(), path);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
-        buffer.putString(path);
-        buffer.putInt(0);
-
-        int version = getVersion();
-        if (version != SftpConstants.SFTP_V3) {
-            buffer.putByte((byte) 0);
-        }
-
-        checkCommandStatus(SftpConstants.SSH_FXP_MKDIR, buffer);
-    }
-
-    @Override
-    public void rmdir(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("rmdir(" + path + ") client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("rmdir({}) {}", getClientSession(), path);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
-        buffer.putString(path);
-        checkCommandStatus(SftpConstants.SSH_FXP_RMDIR, buffer);
-    }
-
-    @Override
-    public CloseableHandle openDir(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("openDir(" + path + ") client is closed");
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
-        buffer.putString(path);
-
-        CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPENDIR, buffer));
-        if (log.isTraceEnabled()) {
-            log.trace("openDir({})[{}}: {}", getClientSession(), path, handle);
-        }
-
-        return handle;
-    }
-
-    @Override
-    public List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException {
-        if (eolIndicator != null) {
-            eolIndicator.set(null);    // assume unknown information
-        }
-        if (!isOpen()) {
-            throw new IOException("readDir(" + handle + ") client is closed");
-        }
-
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* some extra fields */, false);
-        buffer.putBytes(id);
-
-        int cmdId = send(SftpConstants.SSH_FXP_READDIR, buffer);
-        Buffer response = receive(cmdId);
-        return checkDirResponse(SftpConstants.SSH_FXP_READDIR, response, eolIndicator);
-    }
-
-    protected List<DirEntry> checkDirResponse(int cmd, Buffer buffer, AtomicReference<Boolean> eolIndicator) throws IOException {
-        if (eolIndicator != null) {
-            eolIndicator.set(null);    // assume unknown
-        }
-
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SftpConstants.SSH_FXP_NAME) {
-            int len = buffer.getInt();
-            int version = getVersion();
-            ClientChannel channel = getClientChannel();
-            if (log.isDebugEnabled()) {
-                log.debug("checkDirResponse({}}[id={}] reading {} entries", channel, id, len);
-            }
-
-            List<DirEntry> entries = new ArrayList<>(len);
-            for (int i = 0; i < len; i++) {
-                String name = getReferencedName(buffer);
-                String longName = (version == SftpConstants.SFTP_V3) ? getReferencedName(buffer) : null;
-                Attributes attrs = readAttributes(buffer);
-                if (log.isTraceEnabled()) {
-                    log.trace("checkDirResponse({})[id={}][{}] ({})[{}]: {}",
-                              channel, id, i, name, longName, attrs);
-                }
-
-                entries.add(new DirEntry(name, longName, attrs));
-            }
-
-            Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
-            if (eolIndicator != null) {
-                eolIndicator.set(indicator);
-            }
-
-            if (log.isDebugEnabled()) {
-                log.debug("checkDirResponse({}}[id={}] read count={}, eol={}", channel, entries.size(), indicator);
-            }
-            return entries;
-        }
-
-        if (type == SftpConstants.SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkDirResponse({})[id={}] - status: {} [{}] {}",
-                          getClientChannel(), id, SftpConstants.getStatusName(substatus), lang, msg);
-            }
-
-            if (substatus == SftpConstants.SSH_FX_EOF) {
-                return null;
-            }
-
-            throwStatusException(cmd, id, substatus, msg, lang);
-        }
-
-        return handleUnknownDirListingPacket(cmd, id, type, length, buffer);
-    }
-
-    protected List<DirEntry> handleUnknownDirListingPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
-        IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer);
-        if (err != null) {
-            throw err;
-        }
-        return Collections.emptyList();
-    }
-
-    protected IOException handleUnexpectedPacket(int cmd, int expected, int id, int type, int length, Buffer buffer) throws IOException {
-        throw new SshException("Unexpected SFTP packet received while awaiting " + SftpConstants.getCommandMessageName(expected)
-                        + " response to " + SftpConstants.getCommandMessageName(cmd)
-                        + ": type=" + SftpConstants.getCommandMessageName(type) + ", id=" + id + ", length=" + length);
-    }
-
-    @Override
-    public String canonicalPath(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("canonicalPath(" + path + ") client is closed");
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
-        buffer.putString(path);
-        return checkOneName(SftpConstants.SSH_FXP_REALPATH, buffer);
-    }
-
-    @Override
-    public Attributes stat(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("stat(" + path + ") client is closed");
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
-        buffer.putString(path);
-
-        int version = getVersion();
-        if (version >= SftpConstants.SFTP_V4) {
-            buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
-        }
-
-        return checkAttributes(SftpConstants.SSH_FXP_STAT, buffer);
-    }
-
-    @Override
-    public Attributes lstat(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("lstat(" + path + ") client is closed");
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
-        buffer.putString(path);
-
-        int version = getVersion();
-        if (version >= SftpConstants.SFTP_V4) {
-            buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
-        }
-
-        return checkAttributes(SftpConstants.SSH_FXP_LSTAT, buffer);
-    }
-
-    @Override
-    public Attributes stat(Handle handle) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("stat(" + handle + ") client is closed");
-        }
-
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* a bit extra */, false);
-        buffer.putBytes(id);
-
-        int version = getVersion();
-        if (version >= SftpConstants.SFTP_V4) {
-            buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
-        }
-
-        return checkAttributes(SftpConstants.SSH_FXP_FSTAT, buffer);
-    }
-
-    @Override
-    public void setStat(String path, Attributes attributes) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("setStat(" + path + ")[" + attributes + "] client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("setStat({})[{}]: {}", getClientSession(), path, attributes);
-        }
-
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(path);
-        writeAttributes(buffer, attributes);
-        checkCommandStatus(SftpConstants.SSH_FXP_SETSTAT, buffer);
-    }
-
-    @Override
-    public void setStat(Handle handle, Attributes attributes) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("setStat(" + handle + ")[" + attributes + "] client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("setStat({})[{}]: {}", getClientSession(), handle, attributes);
-        }
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + (2 * Long.SIZE) /* some extras */, false);
-        buffer.putBytes(id);
-        writeAttributes(buffer, attributes);
-        checkCommandStatus(SftpConstants.SSH_FXP_FSETSTAT, buffer);
-    }
-
-    @Override
-    public String readLink(String path) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("readLink(" + path + ") client is closed");
-        }
-
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
-        buffer.putString(path);
-        return checkOneName(SftpConstants.SSH_FXP_READLINK, buffer);
-    }
-
-    @Override
-    public void link(String linkPath, String targetPath, boolean symbolic) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("link(" + linkPath + " => " + targetPath + ")[symbolic=" + symbolic + "] client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("link({})[symbolic={}] {} => {}", getClientSession(), symbolic, linkPath, targetPath);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */, false);
-        int version = getVersion();
-        if (version < SftpConstants.SFTP_V6) {
-            if (!symbolic) {
-                throw new UnsupportedOperationException("Hard links are not supported in sftp v" + version);
-            }
-            buffer.putString(targetPath);
-            buffer.putString(linkPath);
-            checkCommandStatus(SftpConstants.SSH_FXP_SYMLINK, buffer);
-        } else {
-            buffer.putString(targetPath);
-            buffer.putString(linkPath);
-            buffer.putBoolean(symbolic);
-            checkCommandStatus(SftpConstants.SSH_FXP_LINK, buffer);
-        }
-    }
-
-    @Override
-    public void lock(Handle handle, long offset, long length, int mask) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("lock(" + handle + ")[offset=" + offset + ", length=" + length + ", mask=0x" + Integer.toHexString(mask) + "] client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("lock({})[{}] offset={}, length={}, mask=0x{}",
-                      getClientSession(), handle, offset, length, Integer.toHexString(mask));
-        }
-
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false);
-        buffer.putBytes(id);
-        buffer.putLong(offset);
-        buffer.putLong(length);
-        buffer.putInt(mask);
-        checkCommandStatus(SftpConstants.SSH_FXP_BLOCK, buffer);
-    }
-
-    @Override
-    public void unlock(Handle handle, long offset, long length) throws IOException {
-        if (!isOpen()) {
-            throw new IOException("unlock" + handle + ")[offset=" + offset + ", length=" + length + "] client is closed");
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("unlock({})[{}] offset={}, length={}", getClientSession(), handle, offset, length);
-        }
-
-        byte[] id = Objects.requireNonNull(handle, "No handle").getIdentifier();
-        Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false);
-        buffer.putBytes(id);
-        buffer.putLong(offset);
-        buffer.putLong(length);
-        checkCommandStatus(SftpConstants.SSH_FXP_UNBLOCK, buffer);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpFileAttributeView.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpFileAttributeView.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpFileAttributeView.java
deleted file mode 100644
index 71b42a3..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpFileAttributeView.java
+++ /dev/null
@@ -1,88 +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.subsystem.sftp;
-
-import java.io.IOException;
-import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.attribute.FileAttributeView;
-import java.util.Objects;
-
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.subsystem.sftp.SftpException;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class AbstractSftpFileAttributeView extends AbstractLoggingBean implements FileAttributeView {
-    protected final SftpFileSystemProvider provider;
-    protected final Path path;
-    protected final LinkOption[] options;
-
-    protected AbstractSftpFileAttributeView(SftpFileSystemProvider provider, Path path, LinkOption... options) {
-        this.provider = Objects.requireNonNull(provider, "No file system provider instance");
-        this.path = Objects.requireNonNull(path, "No path");
-        this.options = options;
-    }
-
-    @Override
-    public String name() {
-        return "view";
-    }
-
-    /**
-     * @return The underlying {@link SftpFileSystemProvider} used to
-     * provide the view functionality
-     */
-    public final SftpFileSystemProvider provider() {
-        return provider;
-    }
-
-    /**
-     * @return The referenced view {@link Path}
-     */
-    public final Path getPath() {
-        return path;
-    }
-
-    protected SftpClient.Attributes readRemoteAttributes() throws IOException {
-        return provider.readRemoteAttributes(provider.toSftpPath(path), options);
-    }
-
-    protected void writeRemoteAttributes(SftpClient.Attributes attrs) throws IOException {
-        SftpPath p = provider.toSftpPath(path);
-        SftpFileSystem fs = p.getFileSystem();
-        try (SftpClient client = fs.getClient()) {
-            try {
-                if (log.isDebugEnabled()) {
-                    log.debug("writeRemoteAttributes({})[{}]: {}", fs, p, attrs);
-                }
-                client.setStat(p.toString(), attrs);
-            } catch (SftpException e) {
-                if (e.getStatus() == SftpConstants.SSH_FX_NO_SUCH_FILE) {
-                    throw new NoSuchFileException(p.toString());
-                }
-                throw e;
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/15428746/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandle.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandle.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandle.java
deleted file mode 100644
index 67ad906..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandle.java
+++ /dev/null
@@ -1,65 +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.subsystem.sftp;
-
-import java.io.IOException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.apache.sshd.client.subsystem.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 path, byte[] id) {
-        super(path, id);
-        this.client = ValidateUtils.checkNotNull(client, "No client for path=%s", path);
-    }
-
-    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);
-        }
-    }
-
-    @Override   // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
-    public int hashCode() {
-        return super.hashCode();
-    }
-
-    @Override   // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
-    public boolean equals(Object obj) {
-        return super.equals(obj);
-    }
-}