You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2018/04/16 11:48:01 UTC

[14/30] mina-sshd git commit: [SSHD-815] Extract SFTP in its own module

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirectoryStream.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirectoryStream.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirectoryStream.java
new file mode 100644
index 0000000..5f48966
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirectoryStream.java
@@ -0,0 +1,65 @@
+/*
+ * 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.DirectoryStream;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+/**
+ * Implements a remote {@link DirectoryStream}
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpDirectoryStream implements DirectoryStream<Path> {
+    private final SftpClient sftp;
+    private final Iterable<SftpClient.DirEntry> iter;
+    private final SftpPath p;
+
+    /**
+     * @param path The remote {@link SftpPath}
+     * @throws IOException If failed to initialize the directory access handle
+     */
+    public SftpDirectoryStream(SftpPath path) throws IOException {
+        SftpFileSystem fs = path.getFileSystem();
+        p = path;
+        sftp = fs.getClient();
+        iter = sftp.readDir(path.toString());
+    }
+
+    /**
+     * Client instance used to access the remote directory
+     *
+     * @return The {@link SftpClient} instance used to access the remote directory
+     */
+    public final SftpClient getClient() {
+        return sftp;
+    }
+
+    @Override
+    public Iterator<Path> iterator() {
+        return new SftpPathIterator(p, iter);
+    }
+
+    @Override
+    public void close() throws IOException {
+        sftp.close();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileStore.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileStore.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileStore.java
new file mode 100644
index 0000000..8a6f1f1
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileStore.java
@@ -0,0 +1,105 @@
+/*
+ * 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.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileStoreAttributeView;
+import java.util.Collection;
+
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpFileStore extends FileStore {
+    private final SftpFileSystem fs;
+    private final String name;
+
+    public SftpFileStore(String name, SftpFileSystem fs) {
+        this.name = name;
+        this.fs = fs;
+    }
+
+    public final SftpFileSystem getFileSystem() {
+        return fs;
+    }
+
+    @Override
+    public String name() {
+        return name;
+    }
+
+    @Override
+    public String type() {
+        return SftpConstants.SFTP_SUBSYSTEM_NAME;
+    }
+
+    @Override
+    public boolean isReadOnly() {
+        return false;
+    }
+
+    @Override
+    public long getTotalSpace() throws IOException {
+        return Long.MAX_VALUE;  // TODO use SFTPv6 space-available extension
+    }
+
+    @Override
+    public long getUsableSpace() throws IOException {
+        return Long.MAX_VALUE;
+    }
+
+    @Override
+    public long getUnallocatedSpace() throws IOException {
+        return Long.MAX_VALUE;
+    }
+
+    @Override
+    public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
+        SftpFileSystem sftpFs = getFileSystem();
+        SftpFileSystemProvider provider = sftpFs.provider();
+        return provider.isSupportedFileAttributeView(sftpFs, type);
+    }
+
+    @Override
+    public boolean supportsFileAttributeView(String name) {
+        if (GenericUtils.isEmpty(name)) {
+            return false;   // debug breakpoint
+        }
+
+        FileSystem sftpFs = getFileSystem();
+        Collection<String> views = sftpFs.supportedFileAttributeViews();
+        return !GenericUtils.isEmpty(views) && views.contains(name);
+    }
+
+    @Override
+    public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
+        return null;    // no special views supported
+    }
+
+    @Override
+    public Object getAttribute(String attribute) throws IOException {
+        return null;    // no special attributes supported
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
new file mode 100644
index 0000000..0ea8cd7
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
@@ -0,0 +1,598 @@
+/*
+ * 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.io.InputStream;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
+import java.nio.charset.Charset;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystemException;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.LinkedBlockingQueue;
+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;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+public class SftpFileSystem extends BaseFileSystem<SftpPath> implements ClientSessionHolder {
+    public static final String POOL_SIZE_PROP = "sftp-fs-pool-size";
+    public static final int DEFAULT_POOL_SIZE = 8;
+
+    public static final Set<String> UNIVERSAL_SUPPORTED_VIEWS =
+            Collections.unmodifiableSet(
+                    GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER,
+                            "basic", "posix", "owner"));
+
+    private final String id;
+    private final ClientSession clientSession;
+    private final SftpClientFactory factory;
+    private final SftpVersionSelector selector;
+    private final Queue<SftpClient> pool;
+    private final ThreadLocal<Wrapper> wrappers = new ThreadLocal<>();
+    private final int version;
+    private final Set<String> supportedViews;
+    private SftpPath defaultDir;
+    private int readBufferSize = SftpClient.DEFAULT_READ_BUFFER_SIZE;
+    private int writeBufferSize = SftpClient.DEFAULT_WRITE_BUFFER_SIZE;
+    private final List<FileStore> stores;
+
+    public SftpFileSystem(SftpFileSystemProvider provider, String id, ClientSession session, SftpClientFactory factory, SftpVersionSelector selector) throws IOException {
+        super(provider);
+        this.id = id;
+        this.clientSession = Objects.requireNonNull(session, "No client session");
+        this.factory = factory != null ? factory : SftpClientFactory.instance();
+        this.selector = selector;
+        this.stores = Collections.unmodifiableList(Collections.<FileStore>singletonList(new SftpFileStore(id, this)));
+        this.pool = new LinkedBlockingQueue<>(session.getIntProperty(POOL_SIZE_PROP, DEFAULT_POOL_SIZE));
+        try (SftpClient client = getClient()) {
+            version = client.getVersion();
+            defaultDir = getPath(client.canonicalPath("."));
+        }
+
+        if (version >= SftpConstants.SFTP_V4) {
+            Set<String> views = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+            views.addAll(UNIVERSAL_SUPPORTED_VIEWS);
+            views.add("acl");
+            supportedViews = Collections.unmodifiableSet(views);
+        } else {
+            supportedViews = UNIVERSAL_SUPPORTED_VIEWS;
+        }
+    }
+
+    public final SftpVersionSelector getSftpVersionSelector() {
+        return selector;
+    }
+
+    public final String getId() {
+        return id;
+    }
+
+    public final int getVersion() {
+        return version;
+    }
+
+    @Override
+    public SftpFileSystemProvider provider() {
+        return (SftpFileSystemProvider) super.provider();
+    }
+
+    @Override   // NOTE: co-variant return
+    public List<FileStore> getFileStores() {
+        return this.stores;
+    }
+
+    public int getReadBufferSize() {
+        return readBufferSize;
+    }
+
+    public void setReadBufferSize(int size) {
+        if (size < SftpClient.MIN_READ_BUFFER_SIZE) {
+            throw new IllegalArgumentException("Insufficient read buffer size: " + size + ", min.=" + SftpClient.MIN_READ_BUFFER_SIZE);
+        }
+
+        readBufferSize = size;
+    }
+
+    public int getWriteBufferSize() {
+        return writeBufferSize;
+    }
+
+    public void setWriteBufferSize(int size) {
+        if (size < SftpClient.MIN_WRITE_BUFFER_SIZE) {
+            throw new IllegalArgumentException("Insufficient write buffer size: " + size + ", min.=" + SftpClient.MIN_WRITE_BUFFER_SIZE);
+        }
+
+        writeBufferSize = size;
+    }
+
+    @Override
+    protected SftpPath create(String root, List<String> names) {
+        return new SftpPath(this, root, names);
+    }
+
+    @Override
+    public ClientSession getClientSession() {
+        return clientSession;
+    }
+
+    @SuppressWarnings("synthetic-access")
+    public SftpClient getClient() throws IOException {
+        Wrapper wrapper = wrappers.get();
+        if (wrapper == null) {
+            while (wrapper == null) {
+                SftpClient client = pool.poll();
+                if (client == null) {
+                    ClientSession session = getClientSession();
+                    client = factory.createSftpClient(session, getSftpVersionSelector());
+                }
+                if (!client.isClosing()) {
+                    wrapper = new Wrapper(client, getReadBufferSize(), getWriteBufferSize());
+                }
+            }
+            wrappers.set(wrapper);
+        } else {
+            wrapper.increment();
+        }
+        return wrapper;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (isOpen()) {
+            SftpFileSystemProvider provider = provider();
+            String fsId = getId();
+            SftpFileSystem fs = provider.removeFileSystem(fsId);
+            ClientSession session = getClientSession();
+            session.close(true);
+
+            if ((fs != null) && (fs != this)) {
+                throw new FileSystemException(fsId, fsId, "Mismatched FS instance for id=" + fsId);
+            }
+        }
+    }
+
+    @Override
+    public boolean isOpen() {
+        ClientSession session = getClientSession();
+        return session.isOpen();
+    }
+
+    @Override
+    public Set<String> supportedFileAttributeViews() {
+        return supportedViews;
+    }
+
+    @Override
+    public UserPrincipalLookupService getUserPrincipalLookupService() {
+        return DefaultUserPrincipalLookupService.INSTANCE;
+    }
+
+    @Override
+    public SftpPath getDefaultDir() {
+        return defaultDir;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + String.valueOf(getClientSession()) + "]";
+    }
+
+    private final class Wrapper extends AbstractSftpClient {
+        private final SftpClient delegate;
+        private final AtomicInteger count = new AtomicInteger(1);
+        private final int readSize;
+        private final int writeSize;
+
+        private Wrapper(SftpClient delegate, int readSize, int writeSize) {
+            this.delegate = delegate;
+            this.readSize = readSize;
+            this.writeSize = writeSize;
+        }
+
+        @Override
+        public int getVersion() {
+            return delegate.getVersion();
+        }
+
+        @Override
+        public ClientSession getClientSession() {
+            return delegate.getClientSession();
+        }
+
+        @Override
+        public ClientChannel getClientChannel() {
+            return delegate.getClientChannel();
+        }
+
+        @Override
+        public NavigableMap<String, byte[]> getServerExtensions() {
+            return delegate.getServerExtensions();
+        }
+
+        @Override
+        public Charset getNameDecodingCharset() {
+            return delegate.getNameDecodingCharset();
+        }
+
+        @Override
+        public void setNameDecodingCharset(Charset cs) {
+            delegate.setNameDecodingCharset(cs);
+        }
+
+        @Override
+        public boolean isClosing() {
+            return false;
+        }
+
+        @Override
+        public boolean isOpen() {
+            return count.get() > 0;
+        }
+
+        @SuppressWarnings("synthetic-access")
+        @Override
+        public void close() throws IOException {
+            if (count.decrementAndGet() <= 0) {
+                if (!pool.offer(delegate)) {
+                    delegate.close();
+                }
+                wrappers.set(null);
+            }
+        }
+
+        public void increment() {
+            count.incrementAndGet();
+        }
+
+        @Override
+        public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("open(" + path + ")[" + options + "] client is closed");
+            }
+            return delegate.open(path, options);
+        }
+
+        @Override
+        public void close(Handle handle) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("close(" + handle + ") client is closed");
+            }
+            delegate.close(handle);
+        }
+
+        @Override
+        public void remove(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("remove(" + path + ") client is closed");
+            }
+            delegate.remove(path);
+        }
+
+        @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");
+            }
+            delegate.rename(oldPath, newPath, options);
+        }
+
+        @Override
+        public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("read(" + handle + "/" + fileOffset + ")[" + dstOffset + "/" + len + "] client is closed");
+            }
+            return delegate.read(handle, fileOffset, dst, dstOffset, len);
+        }
+
+        @Override
+        public void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("write(" + handle + "/" + fileOffset + ")[" + srcOffset + "/" + len + "] client is closed");
+            }
+            delegate.write(handle, fileOffset, src, srcOffset, len);
+        }
+
+        @Override
+        public void mkdir(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("mkdir(" + path + ") client is closed");
+            }
+            delegate.mkdir(path);
+        }
+
+        @Override
+        public void rmdir(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("rmdir(" + path + ") client is closed");
+            }
+            delegate.rmdir(path);
+        }
+
+        @Override
+        public CloseableHandle openDir(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("openDir(" + path + ") client is closed");
+            }
+            return delegate.openDir(path);
+        }
+
+        @Override
+        public List<DirEntry> readDir(Handle handle) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("readDir(" + handle + ") client is closed");
+            }
+            return delegate.readDir(handle);
+        }
+
+        @Override
+        public Iterable<DirEntry> listDir(Handle handle) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("readDir(" + handle + ") client is closed");
+            }
+            return delegate.listDir(handle);
+        }
+
+        @Override
+        public String canonicalPath(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("canonicalPath(" + path + ") client is closed");
+            }
+            return delegate.canonicalPath(path);
+        }
+
+        @Override
+        public Attributes stat(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("stat(" + path + ") client is closed");
+            }
+            return delegate.stat(path);
+        }
+
+        @Override
+        public Attributes lstat(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("lstat(" + path + ") client is closed");
+            }
+            return delegate.lstat(path);
+        }
+
+        @Override
+        public Attributes stat(Handle handle) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("stat(" + handle + ") client is closed");
+            }
+            return delegate.stat(handle);
+        }
+
+        @Override
+        public void setStat(String path, Attributes attributes) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("setStat(" + path + ")[" + attributes + "] client is closed");
+            }
+            delegate.setStat(path, attributes);
+        }
+
+        @Override
+        public void setStat(Handle handle, Attributes attributes) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("setStat(" + handle + ")[" + attributes + "] client is closed");
+            }
+            delegate.setStat(handle, attributes);
+        }
+
+        @Override
+        public String readLink(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("readLink(" + path + ") client is closed");
+            }
+            return delegate.readLink(path);
+        }
+
+        @Override
+        public void symLink(String linkPath, String targetPath) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("symLink(" + linkPath + " => " + targetPath + ") client is closed");
+            }
+            delegate.symLink(linkPath, targetPath);
+        }
+
+        @Override
+        public Iterable<DirEntry> readDir(String path) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("readDir(" + path + ") client is closed");
+            }
+            return delegate.readDir(path);
+        }
+
+        @Override
+        public InputStream read(String path) throws IOException {
+            return read(path, readSize);
+        }
+
+        @Override
+        public InputStream read(String path, OpenMode... mode) throws IOException {
+            return read(path, readSize, mode);
+        }
+
+        @Override
+        public InputStream read(String path, Collection<OpenMode> mode) throws IOException {
+            return read(path, readSize, mode);
+        }
+
+        @Override
+        public InputStream read(String path, int bufferSize, Collection<OpenMode> mode) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("read(" + path + ")[" + mode + "] size=" + bufferSize + ": client is closed");
+            }
+            return delegate.read(path, bufferSize, mode);
+        }
+
+        @Override
+        public OutputStream write(String path) throws IOException {
+            return write(path, writeSize);
+        }
+
+        @Override
+        public OutputStream write(String path, OpenMode... mode) throws IOException {
+            return write(path, writeSize, mode);
+        }
+
+        @Override
+        public OutputStream write(String path, Collection<OpenMode> mode) throws IOException {
+            return write(path, writeSize, mode);
+        }
+
+        @Override
+        public OutputStream write(String path, int bufferSize, Collection<OpenMode> mode) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("write(" + path + ")[" + mode + "] size=" + bufferSize + ": client is closed");
+            }
+            return delegate.write(path, bufferSize, mode);
+        }
+
+        @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");
+            }
+            delegate.link(linkPath, targetPath, symbolic);
+        }
+
+        @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");
+            }
+            delegate.lock(handle, offset, length, mask);
+        }
+
+        @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");
+            }
+            delegate.unlock(handle, offset, length);
+        }
+
+        @Override
+        public int send(int cmd, Buffer buffer) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("send(cmd=" + SftpConstants.getCommandMessageName(cmd) + ") client is closed");
+            }
+
+            if (delegate instanceof RawSftpClient) {
+                return ((RawSftpClient) delegate).send(cmd, buffer);
+            } else {
+                throw new StreamCorruptedException("send(cmd=" + SftpConstants.getCommandMessageName(cmd) + ") delegate is not a " + RawSftpClient.class.getSimpleName());
+            }
+        }
+
+        @Override
+        public Buffer receive(int id) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("receive(id=" + id + ") client is closed");
+            }
+
+            if (delegate instanceof RawSftpClient) {
+                return ((RawSftpClient) delegate).receive(id);
+            } else {
+                throw new StreamCorruptedException("receive(id=" + id + ") delegate is not a " + RawSftpClient.class.getSimpleName());
+            }
+        }
+    }
+
+    public static class DefaultUserPrincipalLookupService extends UserPrincipalLookupService {
+        public static final DefaultUserPrincipalLookupService INSTANCE = new DefaultUserPrincipalLookupService();
+
+        public DefaultUserPrincipalLookupService() {
+            super();
+        }
+
+        @Override
+        public UserPrincipal lookupPrincipalByName(String name) throws IOException {
+            return new DefaultUserPrincipal(name);
+        }
+
+        @Override
+        public GroupPrincipal lookupPrincipalByGroupName(String group) throws IOException {
+            return new DefaultGroupPrincipal(group);
+        }
+    }
+
+    public static class DefaultUserPrincipal implements UserPrincipal {
+
+        private final String name;
+
+        public DefaultUserPrincipal(String name) {
+            this.name = Objects.requireNonNull(name, "name is null");
+        }
+
+        @Override
+        public final String getName() {
+            return name;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            DefaultUserPrincipal that = (DefaultUserPrincipal) o;
+            return Objects.equals(this.getName(), that.getName());
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(getName());
+        }
+
+        @Override
+        public String toString() {
+            return getName();
+        }
+    }
+
+    public static class DefaultGroupPrincipal extends DefaultUserPrincipal implements GroupPrincipal {
+
+        public DefaultGroupPrincipal(String name) {
+            super(name);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemChannel.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemChannel.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemChannel.java
new file mode 100644
index 0000000..40948bf
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemChannel.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;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpFileSystemChannel extends SftpRemotePathChannel {
+    public SftpFileSystemChannel(SftpPath p, Collection<SftpClient.OpenMode> modes) throws IOException {
+        this(Objects.requireNonNull(p, "No target path").toString(), p.getFileSystem(), modes);
+    }
+
+    public SftpFileSystemChannel(String remotePath, SftpFileSystem fs, Collection<SftpClient.OpenMode> modes) throws IOException {
+        super(remotePath, Objects.requireNonNull(fs, "No SFTP file system").getClient(), true, modes);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
new file mode 100644
index 0000000..df33c61
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
@@ -0,0 +1,1255 @@
+/*
+ * 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.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.FileSystemException;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.ProviderMismatchException;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+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 org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A registered {@link FileSystemProvider} that registers the &quot;sftp://&quot;
+ * scheme so that URLs with this protocol are handled as remote SFTP {@link Path}-s
+ * - e.g., &quot;{@code sftp://user:password@host/remote/file/path}&quot;
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpFileSystemProvider extends FileSystemProvider {
+    public static final String READ_BUFFER_PROP_NAME = "sftp-fs-read-buffer-size";
+    public static final int DEFAULT_READ_BUFFER_SIZE = SftpClient.DEFAULT_READ_BUFFER_SIZE;
+    public static final String WRITE_BUFFER_PROP_NAME = "sftp-fs-write-buffer-size";
+    public static final int DEFAULT_WRITE_BUFFER_SIZE = SftpClient.DEFAULT_WRITE_BUFFER_SIZE;
+    public static final String CONNECT_TIME_PROP_NAME = "sftp-fs-connect-time";
+    public static final long DEFAULT_CONNECT_TIME = SftpClient.DEFAULT_WAIT_TIMEOUT;
+    public static final String AUTH_TIME_PROP_NAME = "sftp-fs-auth-time";
+    public static final long DEFAULT_AUTH_TIME = SftpClient.DEFAULT_WAIT_TIMEOUT;
+    public static final String NAME_DECORDER_CHARSET_PROP_NAME = "sftp-fs-name-decoder-charset";
+    public static final Charset DEFAULT_NAME_DECODER_CHARSET = SftpClient.DEFAULT_NAME_DECODING_CHARSET;
+
+    /**
+     * <P>
+     * URI parameter that can be used to specify a special version selection. Options are:
+     * </P>
+     * <UL>
+     *      <LI>{@code max} - select maximum available version for the client</LI>
+     *      <LI>{@code min} - select minimum available version for the client</LI>
+     *      <LI>{@code current} - whatever version is reported by the server</LI>
+     *      <LI>{@code nnn} - select <U>only</U> the specified version</LI>
+     *      <LI>{@code a,b,c} - select one of the specified versions (if available) in preference order</LI>
+     * </UL>
+     */
+    public static final String VERSION_PARAM = "version";
+
+    public static final Set<Class<? extends FileAttributeView>> UNIVERSAL_SUPPORTED_VIEWS =
+            Collections.unmodifiableSet(GenericUtils.asSet(
+                    PosixFileAttributeView.class,
+                    FileOwnerAttributeView.class,
+                    BasicFileAttributeView.class
+            ));
+
+    protected final Logger log;
+
+    private final SshClient client;
+    private final SftpClientFactory factory;
+    private final SftpVersionSelector selector;
+    private final NavigableMap<String, SftpFileSystem> fileSystems = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+    public SftpFileSystemProvider() {
+        this((SshClient) null);
+    }
+
+    public SftpFileSystemProvider(SftpVersionSelector selector) {
+        this(null, selector);
+    }
+
+    /**
+     * @param client The {@link SshClient} to use - if {@code null} then a
+     *               default one will be setup and started. Otherwise, it is assumed that
+     *               the client has already been started
+     * @see SshClient#setUpDefaultClient()
+     */
+    public SftpFileSystemProvider(SshClient client) {
+        this(client, SftpVersionSelector.CURRENT);
+    }
+
+    public SftpFileSystemProvider(SshClient client, SftpVersionSelector selector) {
+        this(client, null, selector);
+    }
+
+    public SftpFileSystemProvider(SshClient client, SftpClientFactory factory, SftpVersionSelector selector) {
+        this.log = LoggerFactory.getLogger(getClass());
+        this.factory = factory;
+        this.selector = selector;
+        if (client == null) {
+            // TODO: make this configurable using system properties
+            client = SshClient.setUpDefaultClient();
+            client.start();
+        }
+        this.client = client;
+    }
+
+    @Override
+    public String getScheme() {
+        return SftpConstants.SFTP_SUBSYSTEM_NAME;
+    }
+
+    public final SftpVersionSelector getSftpVersionSelector() {
+        return selector;
+    }
+
+    @Override // NOTE: co-variant return
+    public SftpFileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+        String host = ValidateUtils.checkNotNullAndNotEmpty(uri.getHost(), "Host not provided");
+        int port = uri.getPort();
+        if (port <= 0) {
+            port = SshConfigFileReader.DEFAULT_PORT;
+        }
+
+        String userInfo = ValidateUtils.checkNotNullAndNotEmpty(uri.getUserInfo(), "UserInfo not provided");
+        String[] ui = GenericUtils.split(userInfo, ':');
+        ValidateUtils.checkTrue(GenericUtils.length(ui) == 2, "Invalid user info: %s", userInfo);
+
+        String username = ui[0];
+        String password = ui[1];
+        String id = getFileSystemIdentifier(host, port, username);
+        Map<String, Object> params = resolveFileSystemParameters(env, parseURIParameters(uri));
+        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(params);
+        SftpVersionSelector selector = resolveSftpVersionSelector(uri, getSftpVersionSelector(), resolver);
+        Charset decodingCharset =
+            PropertyResolverUtils.getCharset(resolver, NAME_DECORDER_CHARSET_PROP_NAME, DEFAULT_NAME_DECODER_CHARSET);
+        long maxConnectTime = resolver.getLongProperty(CONNECT_TIME_PROP_NAME, DEFAULT_CONNECT_TIME);
+        long maxAuthTime = resolver.getLongProperty(AUTH_TIME_PROP_NAME, DEFAULT_AUTH_TIME);
+
+        SftpFileSystem fileSystem;
+        synchronized (fileSystems) {
+            if (fileSystems.containsKey(id)) {
+                throw new FileSystemAlreadyExistsException(id);
+            }
+
+            // TODO try and find a way to avoid doing this while locking the file systems cache
+            ClientSession session = null;
+            try {
+                session = client.connect(username, host, port)
+                        .verify(maxConnectTime)
+                        .getSession();
+                if (GenericUtils.size(params) > 0) {
+                    // Cannot use forEach because the session is not effectively final
+                    for (Map.Entry<String, ?> pe : params.entrySet()) {
+                        String key = pe.getKey();
+                        Object value = pe.getValue();
+                        if (VERSION_PARAM.equalsIgnoreCase(key)) {
+                            continue;
+                        }
+
+                        PropertyResolverUtils.updateProperty(session, key, value);
+                    }
+
+                    PropertyResolverUtils.updateProperty(session, SftpClient.NAME_DECODING_CHARSET, decodingCharset);
+                }
+
+                session.addPasswordIdentity(password);
+                session.auth().verify(maxAuthTime);
+
+                fileSystem = new SftpFileSystem(this, id, session, factory, selector);
+                fileSystems.put(id, fileSystem);
+            } catch (Exception e) {
+                if (session != null) {
+                    try {
+                        session.close();
+                    } catch (IOException t) {
+                        if (log.isDebugEnabled()) {
+                            log.debug("Failed (" + t.getClass().getSimpleName() + ")"
+                                    + " to close session for new file system on " + host + ":" + port
+                                    + " due to " + e.getClass().getSimpleName() + "[" + e.getMessage() + "]"
+                                    + ": " + t.getMessage());
+                        }
+                    }
+                }
+
+                if (e instanceof IOException) {
+                    throw (IOException) e;
+                } else if (e instanceof RuntimeException) {
+                    throw (RuntimeException) e;
+                } else {
+                    throw new IOException(e);
+                }
+            }
+        }
+
+        fileSystem.setReadBufferSize(resolver.getIntProperty(READ_BUFFER_PROP_NAME, DEFAULT_READ_BUFFER_SIZE));
+        fileSystem.setWriteBufferSize(resolver.getIntProperty(WRITE_BUFFER_PROP_NAME, DEFAULT_WRITE_BUFFER_SIZE));
+        if (log.isDebugEnabled()) {
+            log.debug("newFileSystem({}): {}", uri.toASCIIString(), fileSystem);
+        }
+        return fileSystem;
+    }
+
+    protected SftpVersionSelector resolveSftpVersionSelector(URI uri, SftpVersionSelector defaultSelector, PropertyResolver resolver) {
+        String preference = resolver.getString(VERSION_PARAM);
+        if (GenericUtils.isEmpty(preference)) {
+            return defaultSelector;
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("resolveSftpVersionSelector({}) preference={}", uri, preference);
+        }
+
+        if ("max".equalsIgnoreCase(preference)) {
+            return SftpVersionSelector.MAXIMUM;
+        } else if ("min".equalsIgnoreCase(preference)) {
+            return SftpVersionSelector.MINIMUM;
+        } else if ("current".equalsIgnoreCase(preference)) {
+            return SftpVersionSelector.CURRENT;
+        }
+
+        String[] values = GenericUtils.split(preference, ',');
+        if (values.length == 1) {
+            return SftpVersionSelector.fixedVersionSelector(Integer.parseInt(values[0]));
+        }
+
+        int[] preferred = new int[values.length];
+        for (int index = 0; index < values.length; index++) {
+            preferred[index] = Integer.parseInt(values[index]);
+        }
+
+        return SftpVersionSelector.preferredVersionSelector(preferred);
+    }
+
+    // NOTE: URI parameters override environment ones
+    public static Map<String, Object> resolveFileSystemParameters(Map<String, ?> env, Map<String, Object> uriParams) {
+        if (GenericUtils.isEmpty(env)) {
+            return GenericUtils.isEmpty(uriParams) ? Collections.emptyMap() : uriParams;
+        } else if (GenericUtils.isEmpty(uriParams)) {
+            return Collections.unmodifiableMap(env);
+        }
+
+        Map<String, Object> resolved = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        resolved.putAll(env);
+        resolved.putAll(uriParams);
+        return resolved;
+    }
+
+    public static Map<String, Object> parseURIParameters(URI uri) {
+        return parseURIParameters((uri == null) ? "" : uri.getQuery());
+    }
+
+    public static Map<String, Object> parseURIParameters(String params) {
+        if (GenericUtils.isEmpty(params)) {
+            return Collections.emptyMap();
+        }
+
+        if (params.charAt(0) == '?') {
+            if (params.length() == 1) {
+                return Collections.emptyMap();
+            }
+            params = params.substring(1);
+        }
+
+        String[] pairs = GenericUtils.split(params, '&');
+        Map<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (String p : pairs) {
+            int pos = p.indexOf('=');
+            if (pos < 0) {
+                map.put(p, Boolean.TRUE);
+                continue;
+            }
+
+            String key = p.substring(0, pos);
+            String value = p.substring(pos + 1);
+            if (NumberUtils.isIntegerNumber(value)) {
+                map.put(key, Long.parseLong(value));
+            } else {
+                map.put(key, value);
+            }
+        }
+
+        return map;
+    }
+
+    public SftpFileSystem newFileSystem(ClientSession session) throws IOException {
+        String id = getFileSystemIdentifier(session);
+        SftpFileSystem fileSystem;
+        synchronized (fileSystems) {
+            if (fileSystems.containsKey(id)) {
+                throw new FileSystemAlreadyExistsException(id);
+            }
+            fileSystem = new SftpFileSystem(this, id, session, factory, getSftpVersionSelector());
+            fileSystems.put(id, fileSystem);
+        }
+
+        fileSystem.setReadBufferSize(session.getIntProperty(READ_BUFFER_PROP_NAME, DEFAULT_READ_BUFFER_SIZE));
+        fileSystem.setWriteBufferSize(session.getIntProperty(WRITE_BUFFER_PROP_NAME, DEFAULT_WRITE_BUFFER_SIZE));
+        if (log.isDebugEnabled()) {
+            log.debug("newFileSystem: {}", fileSystem);
+        }
+
+        return fileSystem;
+    }
+
+    @Override
+    public FileSystem getFileSystem(URI uri) {
+        String id = getFileSystemIdentifier(uri);
+        SftpFileSystem fs = getFileSystem(id);
+        if (fs == null) {
+            throw new FileSystemNotFoundException(id);
+        }
+        return fs;
+    }
+
+    /**
+     * @param id File system identifier - ignored if {@code null}/empty
+     * @return The removed {@link SftpFileSystem} - {@code null} if no match
+     */
+    public SftpFileSystem removeFileSystem(String id) {
+        if (GenericUtils.isEmpty(id)) {
+            return null;
+        }
+
+        SftpFileSystem removed;
+        synchronized (fileSystems) {
+            removed = fileSystems.remove(id);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("removeFileSystem({}): {}", id, removed);
+        }
+        return removed;
+    }
+
+    /**
+     * @param id File system identifier - ignored if {@code null}/empty
+     * @return The cached {@link SftpFileSystem} - {@code null} if no match
+     */
+    public SftpFileSystem getFileSystem(String id) {
+        if (GenericUtils.isEmpty(id)) {
+            return null;
+        }
+
+        synchronized (fileSystems) {
+            return fileSystems.get(id);
+        }
+    }
+
+    @Override
+    public Path getPath(URI uri) {
+        FileSystem fs = getFileSystem(uri);
+        return fs.getPath(uri.getPath());
+    }
+
+    @Override
+    public FileChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+        return newFileChannel(path, options, attrs);
+    }
+
+    @Override
+    public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+        Collection<SftpClient.OpenMode> modes = SftpClient.OpenMode.fromOpenOptions(options);
+        if (modes.isEmpty()) {
+            modes = EnumSet.of(SftpClient.OpenMode.Read, SftpClient.OpenMode.Write);
+        }
+        // TODO: process file attributes
+        return new SftpFileSystemChannel(toSftpPath(path), modes);
+    }
+
+    @Override
+    public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
+        final SftpPath p = toSftpPath(dir);
+        return new SftpDirectoryStream(p);
+    }
+
+    @Override
+    public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+        SftpPath p = toSftpPath(dir);
+        SftpFileSystem fs = p.getFileSystem();
+        if (log.isDebugEnabled()) {
+            log.debug("createDirectory({}) {} ({})", fs, dir, Arrays.asList(attrs));
+        }
+        try (SftpClient sftp = fs.getClient()) {
+            try {
+                sftp.mkdir(dir.toString());
+            } catch (SftpException e) {
+                int sftpStatus = e.getStatus();
+                if ((sftp.getVersion() == SftpConstants.SFTP_V3) && (sftpStatus == SftpConstants.SSH_FX_FAILURE)) {
+                    try {
+                        Attributes attributes = sftp.stat(dir.toString());
+                        if (attributes != null) {
+                            throw new FileAlreadyExistsException(p.toString());
+                        }
+                    } catch (SshException e2) {
+                        e.addSuppressed(e2);
+                    }
+                }
+                if (sftpStatus == SftpConstants.SSH_FX_FILE_ALREADY_EXISTS) {
+                    throw new FileAlreadyExistsException(p.toString());
+                }
+                throw e;
+            }
+            for (FileAttribute<?> attr : attrs) {
+                setAttribute(p, attr.name(), attr.value());
+            }
+        }
+    }
+
+    @Override
+    public void delete(Path path) throws IOException {
+        SftpPath p = toSftpPath(path);
+        checkAccess(p, AccessMode.WRITE);
+
+        SftpFileSystem fs = p.getFileSystem();
+        if (log.isDebugEnabled()) {
+            log.debug("delete({}) {}", fs, path);
+        }
+
+        try (SftpClient sftp = fs.getClient()) {
+            BasicFileAttributes attributes = readAttributes(path, BasicFileAttributes.class);
+            if (attributes.isDirectory()) {
+                sftp.rmdir(path.toString());
+            } else {
+                sftp.remove(path.toString());
+            }
+        }
+    }
+
+    @Override
+    public void copy(Path source, Path target, CopyOption... options) throws IOException {
+        SftpPath src = toSftpPath(source);
+        SftpPath dst = toSftpPath(target);
+        if (src.getFileSystem() != dst.getFileSystem()) {
+            throw new ProviderMismatchException("Mismatched file system providers for " + src + " vs. " + dst);
+        }
+        checkAccess(src);
+
+        boolean replaceExisting = false;
+        boolean copyAttributes = false;
+        boolean noFollowLinks = false;
+        for (CopyOption opt : options) {
+            replaceExisting |= opt == StandardCopyOption.REPLACE_EXISTING;
+            copyAttributes |= opt == StandardCopyOption.COPY_ATTRIBUTES;
+            noFollowLinks |= opt == LinkOption.NOFOLLOW_LINKS;
+        }
+        LinkOption[] linkOptions = IoUtils.getLinkOptions(!noFollowLinks);
+
+        // attributes of source file
+        BasicFileAttributes attrs = readAttributes(source, BasicFileAttributes.class, linkOptions);
+        if (attrs.isSymbolicLink()) {
+            throw new IOException("Copying of symbolic links not supported");
+        }
+
+        // delete target if it exists and REPLACE_EXISTING is specified
+        Boolean status = IoUtils.checkFileExists(target, linkOptions);
+        if (status == null) {
+            throw new AccessDeniedException("Existence cannot be determined for copy target: " + target);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("copy({})[{}] {} => {}", src.getFileSystem(), Arrays.asList(options), src, dst);
+        }
+
+        if (replaceExisting) {
+            deleteIfExists(target);
+        } else {
+            if (status) {
+                throw new FileAlreadyExistsException(target.toString());
+            }
+        }
+
+        // create directory or copy file
+        if (attrs.isDirectory()) {
+            createDirectory(target);
+        } else {
+            try (InputStream in = newInputStream(source);
+                 OutputStream os = newOutputStream(target)) {
+                IoUtils.copy(in, os);
+            }
+        }
+
+        // copy basic attributes to target
+        if (copyAttributes) {
+            BasicFileAttributeView view = getFileAttributeView(target, BasicFileAttributeView.class, linkOptions);
+            try {
+                view.setTimes(attrs.lastModifiedTime(), attrs.lastAccessTime(), attrs.creationTime());
+            } catch (Throwable x) {
+                // rollback
+                try {
+                    delete(target);
+                } catch (Throwable suppressed) {
+                    x.addSuppressed(suppressed);
+                }
+                throw x;
+            }
+        }
+    }
+
+    @Override
+    public void move(Path source, Path target, CopyOption... options) throws IOException {
+        SftpPath src = toSftpPath(source);
+        SftpFileSystem fsSrc = src.getFileSystem();
+        SftpPath dst = toSftpPath(target);
+
+        if (src.getFileSystem() != dst.getFileSystem()) {
+            throw new ProviderMismatchException("Mismatched file system providers for " + src + " vs. " + dst);
+        }
+        checkAccess(src);
+
+        boolean replaceExisting = false;
+        boolean copyAttributes = false;
+        boolean noFollowLinks = false;
+        for (CopyOption opt : options) {
+            replaceExisting |= opt == StandardCopyOption.REPLACE_EXISTING;
+            copyAttributes |= opt == StandardCopyOption.COPY_ATTRIBUTES;
+            noFollowLinks |= opt == LinkOption.NOFOLLOW_LINKS;
+        }
+        LinkOption[] linkOptions = IoUtils.getLinkOptions(noFollowLinks);
+
+        // attributes of source file
+        BasicFileAttributes attrs = readAttributes(source, BasicFileAttributes.class, linkOptions);
+        if (attrs.isSymbolicLink()) {
+            throw new IOException("Moving of source symbolic link (" + source + ") to " + target + " not supported");
+        }
+
+        // delete target if it exists and REPLACE_EXISTING is specified
+        Boolean status = IoUtils.checkFileExists(target, linkOptions);
+        if (status == null) {
+            throw new AccessDeniedException("Existence cannot be determined for move target " + target);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("move({})[{}] {} => {}", src.getFileSystem(), Arrays.asList(options), src, dst);
+        }
+
+        if (replaceExisting) {
+            deleteIfExists(target);
+        } else if (status) {
+            throw new FileAlreadyExistsException(target.toString());
+        }
+
+        try (SftpClient sftp = fsSrc.getClient()) {
+            sftp.rename(src.toString(), dst.toString());
+        }
+
+        // copy basic attributes to target
+        if (copyAttributes) {
+            BasicFileAttributeView view = getFileAttributeView(target, BasicFileAttributeView.class, linkOptions);
+            try {
+                view.setTimes(attrs.lastModifiedTime(), attrs.lastAccessTime(), attrs.creationTime());
+            } catch (Throwable x) {
+                // rollback
+                try {
+                    delete(target);
+                } catch (Throwable suppressed) {
+                    x.addSuppressed(suppressed);
+                }
+                throw x;
+            }
+        }
+    }
+
+    @Override
+    public boolean isSameFile(Path path1, Path path2) throws IOException {
+        SftpPath p1 = toSftpPath(path1);
+        SftpPath p2 = toSftpPath(path2);
+        if (p1.getFileSystem() != p2.getFileSystem()) {
+            throw new ProviderMismatchException("Mismatched file system providers for " + p1 + " vs. " + p2);
+        }
+        checkAccess(p1);
+        checkAccess(p2);
+        return p1.equals(p2);
+    }
+
+    @Override
+    public boolean isHidden(Path path) throws IOException {
+        return false;
+    }
+
+    @Override
+    public FileStore getFileStore(Path path) throws IOException {
+        FileSystem fs = path.getFileSystem();
+        if (!(fs instanceof SftpFileSystem)) {
+            throw new FileSystemException(path.toString(), path.toString(), "getFileStore(" + path + ") path not attached to an SFTP file system");
+        }
+
+        SftpFileSystem sftpFs = (SftpFileSystem) fs;
+        String id = sftpFs.getId();
+        SftpFileSystem cached = getFileSystem(id);
+        if (cached != sftpFs) {
+            throw new FileSystemException(path.toString(), path.toString(), "Mismatched file system instance for id=" + id);
+        }
+
+        return sftpFs.getFileStores().get(0);
+    }
+
+    @Override
+    public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
+        SftpPath l = toSftpPath(link);
+        SftpFileSystem fsLink = l.getFileSystem();
+        SftpPath t = toSftpPath(target);
+        if (fsLink != t.getFileSystem()) {
+            throw new ProviderMismatchException("Mismatched file system providers for " + l + " vs. " + t);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("createSymbolicLink({})[{}] {} => {}", fsLink, Arrays.asList(attrs), link, target);
+        }
+
+        try (SftpClient client = fsLink.getClient()) {
+            client.symLink(l.toString(), t.toString());
+        }
+    }
+
+    @Override
+    public Path readSymbolicLink(Path link) throws IOException {
+        SftpPath l = toSftpPath(link);
+        SftpFileSystem fsLink = l.getFileSystem();
+        try (SftpClient client = fsLink.getClient()) {
+            String linkPath = client.readLink(l.toString());
+            if (log.isDebugEnabled()) {
+                log.debug("readSymbolicLink({})[{}] {} => {}", fsLink, link, linkPath);
+            }
+
+            return fsLink.getPath(linkPath);
+        }
+    }
+
+    @Override
+    public void checkAccess(Path path, AccessMode... modes) throws IOException {
+        SftpPath p = toSftpPath(path);
+        boolean w = false;
+        boolean x = false;
+        if (GenericUtils.length(modes) > 0) {
+            for (AccessMode mode : modes) {
+                switch (mode) {
+                    case READ:
+                        break;
+                    case WRITE:
+                        w = true;
+                        break;
+                    case EXECUTE:
+                        x = true;
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("Unsupported mode: " + mode);
+                }
+            }
+        }
+
+        BasicFileAttributes attrs = getFileAttributeView(p, BasicFileAttributeView.class).readAttributes();
+        if ((attrs == null) && !(p.isAbsolute() && p.getNameCount() == 0)) {
+            throw new NoSuchFileException(path.toString());
+        }
+
+        SftpFileSystem fs = p.getFileSystem();
+        if (x || (w && fs.isReadOnly())) {
+            throw new AccessDeniedException("Filesystem is read-only: " + path.toString());
+        }
+    }
+
+    @Override
+    public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, final LinkOption... options) {
+        if (isSupportedFileAttributeView(path, type)) {
+            if (AclFileAttributeView.class.isAssignableFrom(type)) {
+                return type.cast(new SftpAclFileAttributeView(this, path, options));
+            } else if (BasicFileAttributeView.class.isAssignableFrom(type)) {
+                return type.cast(new SftpPosixFileAttributeView(this, path, options));
+            }
+        }
+
+        throw new UnsupportedOperationException("getFileAttributeView(" + path + ") view not supported: " + type.getSimpleName());
+    }
+
+    public boolean isSupportedFileAttributeView(Path path, Class<? extends FileAttributeView> type) {
+        return isSupportedFileAttributeView(toSftpPath(path).getFileSystem(), type);
+    }
+
+    public boolean isSupportedFileAttributeView(SftpFileSystem fs, Class<? extends FileAttributeView> type) {
+        Collection<String> views = fs.supportedFileAttributeViews();
+        if ((type == null) || GenericUtils.isEmpty(views)) {
+            return false;
+        } else if (PosixFileAttributeView.class.isAssignableFrom(type)) {
+            return views.contains("posix");
+        } else if (AclFileAttributeView.class.isAssignableFrom(type)) {
+            return views.contains("acl");   // must come before owner view
+        } else if (FileOwnerAttributeView.class.isAssignableFrom(type)) {
+            return views.contains("owner");
+        } else if (BasicFileAttributeView.class.isAssignableFrom(type)) {
+            return views.contains("basic"); // must be last
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
+        if (type.isAssignableFrom(PosixFileAttributes.class)) {
+            return type.cast(getFileAttributeView(path, PosixFileAttributeView.class, options).readAttributes());
+        }
+
+        throw new UnsupportedOperationException("readAttributes(" + path + ")[" + type.getSimpleName() + "] N/A");
+    }
+
+    @Override
+    public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
+        String view;
+        String attrs;
+        int i = attributes.indexOf(':');
+        if (i == -1) {
+            view = "basic";
+            attrs = attributes;
+        } else {
+            view = attributes.substring(0, i++);
+            attrs = attributes.substring(i);
+        }
+
+        return readAttributes(path, view, attrs, options);
+    }
+
+    public Map<String, Object> readAttributes(Path path, String view, String attrs, LinkOption... options) throws IOException {
+        SftpPath p = toSftpPath(path);
+        SftpFileSystem fs = p.getFileSystem();
+        Collection<String> views = fs.supportedFileAttributeViews();
+        if (GenericUtils.isEmpty(views) || (!views.contains(view))) {
+            throw new UnsupportedOperationException("readAttributes(" + path + ")[" + view + ":" + attrs + "] view not supported: " + views);
+        }
+
+        if ("basic".equalsIgnoreCase(view) || "posix".equalsIgnoreCase(view) || "owner".equalsIgnoreCase(view)) {
+            return readPosixViewAttributes(p, view, attrs, options);
+        } else if ("acl".equalsIgnoreCase(view)) {
+            return readAclViewAttributes(p, view, attrs, options);
+        } else  {
+            return readCustomViewAttributes(p, view, attrs, options);
+        }
+    }
+
+    protected Map<String, Object> readCustomViewAttributes(SftpPath path, String view, String attrs, LinkOption... options) throws IOException {
+        throw new UnsupportedOperationException("readCustomViewAttributes(" + path + ")[" + view + ":" + attrs + "] view not supported");
+    }
+
+    protected NavigableMap<String, Object> readAclViewAttributes(SftpPath path, String view, String attrs, LinkOption... options) throws IOException {
+        if ("*".equals(attrs)) {
+            attrs = "acl,owner";
+        }
+
+        SftpFileSystem fs = path.getFileSystem();
+        SftpClient.Attributes attributes;
+        try (SftpClient client = fs.getClient()) {
+            attributes = readRemoteAttributes(path, options);
+        }
+
+        NavigableMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        String[] attrValues = GenericUtils.split(attrs, ',');
+        boolean traceEnabled = log.isTraceEnabled();
+        for (String attr : attrValues) {
+            switch (attr) {
+                case "acl":
+                    List<AclEntry> acl = attributes.getAcl();
+                    if (acl != null) {
+                        map.put(attr, acl);
+                    }
+                    break;
+                case "owner":
+                    String owner = attributes.getOwner();
+                    if (GenericUtils.length(owner) > 0) {
+                        map.put(attr, new SftpFileSystem.DefaultUserPrincipal(owner));
+                    }
+                    break;
+                default:
+                    if (traceEnabled) {
+                        log.trace("readAclViewAttributes({})[{}] unknown attribute: {}", fs, attrs, attr);
+                    }
+            }
+        }
+
+        return map;
+    }
+
+    public SftpClient.Attributes readRemoteAttributes(SftpPath path, LinkOption... options) throws IOException {
+        SftpFileSystem fs = path.getFileSystem();
+        try (SftpClient client = fs.getClient()) {
+            try {
+                SftpClient.Attributes attrs;
+                if (IoUtils.followLinks(options)) {
+                    attrs = client.stat(path.toString());
+                } else {
+                    attrs = client.lstat(path.toString());
+                }
+                if (log.isTraceEnabled()) {
+                    log.trace("readRemoteAttributes({})[{}]: {}", fs, path, attrs);
+                }
+                return attrs;
+            } catch (SftpException e) {
+                if (e.getStatus() == SftpConstants.SSH_FX_NO_SUCH_FILE) {
+                    throw new NoSuchFileException(path.toString());
+                }
+                throw e;
+            }
+        }
+    }
+
+    protected NavigableMap<String, Object> readPosixViewAttributes(
+            SftpPath path, String view, String attrs, LinkOption... options)
+                throws IOException {
+        PosixFileAttributes v = readAttributes(path, PosixFileAttributes.class, options);
+        if ("*".equals(attrs)) {
+            attrs = "lastModifiedTime,lastAccessTime,creationTime,size,isRegularFile,isDirectory,isSymbolicLink,isOther,fileKey,owner,permissions,group";
+        }
+
+        NavigableMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        boolean traceEnabled = log.isTraceEnabled();
+        String[] attrValues = GenericUtils.split(attrs, ',');
+        for (String attr : attrValues) {
+            switch (attr) {
+                case "lastModifiedTime":
+                    map.put(attr, v.lastModifiedTime());
+                    break;
+                case "lastAccessTime":
+                    map.put(attr, v.lastAccessTime());
+                    break;
+                case "creationTime":
+                    map.put(attr, v.creationTime());
+                    break;
+                case "size":
+                    map.put(attr, v.size());
+                    break;
+                case "isRegularFile":
+                    map.put(attr, v.isRegularFile());
+                    break;
+                case "isDirectory":
+                    map.put(attr, v.isDirectory());
+                    break;
+                case "isSymbolicLink":
+                    map.put(attr, v.isSymbolicLink());
+                    break;
+                case "isOther":
+                    map.put(attr, v.isOther());
+                    break;
+                case "fileKey":
+                    map.put(attr, v.fileKey());
+                    break;
+                case "owner":
+                    map.put(attr, v.owner());
+                    break;
+                case "permissions":
+                    map.put(attr, v.permissions());
+                    break;
+                case "group":
+                    map.put(attr, v.group());
+                    break;
+                default:
+                    if (traceEnabled) {
+                        log.trace("readPosixViewAttributes({})[{}:{}] ignored for {}", path, view, attr, attrs);
+                    }
+            }
+        }
+        return map;
+    }
+
+    @Override
+    public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
+        String view;
+        String attr;
+        int i = attribute.indexOf(':');
+        if (i == -1) {
+            view = "basic";
+            attr = attribute;
+        } else {
+            view = attribute.substring(0, i++);
+            attr = attribute.substring(i);
+        }
+
+        setAttribute(path, view, attr, value, options);
+    }
+
+    public void setAttribute(Path path, String view, String attr, Object value, LinkOption... options) throws IOException {
+        SftpPath p = toSftpPath(path);
+        SftpFileSystem fs = p.getFileSystem();
+        Collection<String> views = fs.supportedFileAttributeViews();
+        if (GenericUtils.isEmpty(views) || (!views.contains(view))) {
+            throw new UnsupportedOperationException("setAttribute(" + path + ")[" + view + ":" + attr + "=" + value + "] view " + view + " not supported: " + views);
+        }
+
+        SftpClient.Attributes attributes = new SftpClient.Attributes();
+        switch (attr) {
+            case "lastModifiedTime":
+                attributes.modifyTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+                break;
+            case "lastAccessTime":
+                attributes.accessTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+                break;
+            case "creationTime":
+                attributes.createTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+                break;
+            case "size":
+                attributes.size(((Number) value).longValue());
+                break;
+            case "permissions": {
+                @SuppressWarnings("unchecked")
+                Set<PosixFilePermission> attrSet = (Set<PosixFilePermission>) value;
+                attributes.perms(attributesToPermissions(path, attrSet));
+                break;
+            }
+            case "owner":
+                attributes.owner(((UserPrincipal) value).getName());
+                break;
+            case "group":
+                attributes.group(((GroupPrincipal) value).getName());
+                break;
+            case "acl": {
+                ValidateUtils.checkTrue("acl".equalsIgnoreCase(view), "ACL cannot be set via view=%s", view);
+                @SuppressWarnings("unchecked")
+                List<AclEntry> acl = (List<AclEntry>) value;
+                attributes.acl(acl);
+                break;
+            }
+            case "isRegularFile":
+            case "isDirectory":
+            case "isSymbolicLink":
+            case "isOther":
+            case "fileKey":
+                throw new UnsupportedOperationException("setAttribute(" + path + ")[" + view + ":" + attr + "=" + value + "] modification N/A");
+            default:
+                if (log.isTraceEnabled()) {
+                    log.trace("setAttribute({})[{}] ignore {}:{}={}", fs, path, view, attr, value);
+                }
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("setAttribute({}) {}: {}", fs, path, attributes);
+        }
+
+        try (SftpClient client = fs.getClient()) {
+            client.setStat(p.toString(), attributes);
+        }
+    }
+
+    public SftpPath toSftpPath(Path path) {
+        Objects.requireNonNull(path, "No path provided");
+        if (!(path instanceof SftpPath)) {
+            throw new ProviderMismatchException("Path is not SFTP: " + path);
+        }
+        return (SftpPath) path;
+    }
+
+    protected int attributesToPermissions(Path path, Collection<PosixFilePermission> perms) {
+        if (GenericUtils.isEmpty(perms)) {
+            return 0;
+        }
+
+        int pf = 0;
+        boolean traceEnabled = log.isTraceEnabled();
+        for (PosixFilePermission p : perms) {
+            switch (p) {
+                case OWNER_READ:
+                    pf |= SftpConstants.S_IRUSR;
+                    break;
+                case OWNER_WRITE:
+                    pf |= SftpConstants.S_IWUSR;
+                    break;
+                case OWNER_EXECUTE:
+                    pf |= SftpConstants.S_IXUSR;
+                    break;
+                case GROUP_READ:
+                    pf |= SftpConstants.S_IRGRP;
+                    break;
+                case GROUP_WRITE:
+                    pf |= SftpConstants.S_IWGRP;
+                    break;
+                case GROUP_EXECUTE:
+                    pf |= SftpConstants.S_IXGRP;
+                    break;
+                case OTHERS_READ:
+                    pf |= SftpConstants.S_IROTH;
+                    break;
+                case OTHERS_WRITE:
+                    pf |= SftpConstants.S_IWOTH;
+                    break;
+                case OTHERS_EXECUTE:
+                    pf |= SftpConstants.S_IXOTH;
+                    break;
+                default:
+                    if (traceEnabled) {
+                        log.trace("attributesToPermissions(" + path + ") ignored " + p);
+                    }
+            }
+        }
+
+        return pf;
+    }
+
+    public static String getRWXPermissions(int perms) {
+        StringBuilder sb = new StringBuilder(10 /* 3 * rwx + (d)irectory */);
+        if ((perms & SftpConstants.S_IFLNK) == SftpConstants.S_IFLNK) {
+            sb.append('l');
+        } else if ((perms & SftpConstants.S_IFDIR) == SftpConstants.S_IFDIR) {
+            sb.append('d');
+        } else {
+            sb.append('-');
+        }
+
+        if ((perms & SftpConstants.S_IRUSR) == SftpConstants.S_IRUSR) {
+            sb.append('r');
+        } else {
+            sb.append('-');
+        }
+        if ((perms & SftpConstants.S_IWUSR) == SftpConstants.S_IWUSR) {
+            sb.append('w');
+        } else {
+            sb.append('-');
+        }
+        if ((perms & SftpConstants.S_IXUSR) == SftpConstants.S_IXUSR) {
+            sb.append('x');
+        } else {
+            sb.append('-');
+        }
+
+        if ((perms & SftpConstants.S_IRGRP) == SftpConstants.S_IRGRP) {
+            sb.append('r');
+        } else {
+            sb.append('-');
+        }
+        if ((perms & SftpConstants.S_IWGRP) == SftpConstants.S_IWGRP) {
+            sb.append('w');
+        } else {
+            sb.append('-');
+        }
+        if ((perms & SftpConstants.S_IXGRP) == SftpConstants.S_IXGRP) {
+            sb.append('x');
+        } else {
+            sb.append('-');
+        }
+
+        if ((perms & SftpConstants.S_IROTH) == SftpConstants.S_IROTH) {
+            sb.append('r');
+        } else {
+            sb.append('-');
+        }
+        if ((perms & SftpConstants.S_IWOTH) == SftpConstants.S_IWOTH) {
+            sb.append('w');
+        } else {
+            sb.append('-');
+        }
+        if ((perms & SftpConstants.S_IXOTH) == SftpConstants.S_IXOTH) {
+            sb.append('x');
+        } else {
+            sb.append('-');
+        }
+
+        return sb.toString();
+    }
+
+    public static String getOctalPermissions(int perms) {
+        Collection<PosixFilePermission> attrs = permissionsToAttributes(perms);
+        return getOctalPermissions(attrs);
+    }
+
+    public static Set<PosixFilePermission> permissionsToAttributes(int perms) {
+        Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class);
+        if ((perms & SftpConstants.S_IRUSR) == SftpConstants.S_IRUSR) {
+            p.add(PosixFilePermission.OWNER_READ);
+        }
+        if ((perms & SftpConstants.S_IWUSR) == SftpConstants.S_IWUSR) {
+            p.add(PosixFilePermission.OWNER_WRITE);
+        }
+        if ((perms & SftpConstants.S_IXUSR) == SftpConstants.S_IXUSR) {
+            p.add(PosixFilePermission.OWNER_EXECUTE);
+        }
+        if ((perms & SftpConstants.S_IRGRP) == SftpConstants.S_IRGRP) {
+            p.add(PosixFilePermission.GROUP_READ);
+        }
+        if ((perms & SftpConstants.S_IWGRP) == SftpConstants.S_IWGRP) {
+            p.add(PosixFilePermission.GROUP_WRITE);
+        }
+        if ((perms & SftpConstants.S_IXGRP) == SftpConstants.S_IXGRP) {
+            p.add(PosixFilePermission.GROUP_EXECUTE);
+        }
+        if ((perms & SftpConstants.S_IROTH) == SftpConstants.S_IROTH) {
+            p.add(PosixFilePermission.OTHERS_READ);
+        }
+        if ((perms & SftpConstants.S_IWOTH) == SftpConstants.S_IWOTH) {
+            p.add(PosixFilePermission.OTHERS_WRITE);
+        }
+        if ((perms & SftpConstants.S_IXOTH) == SftpConstants.S_IXOTH) {
+            p.add(PosixFilePermission.OTHERS_EXECUTE);
+        }
+        return p;
+    }
+
+    public static String getOctalPermissions(Collection<PosixFilePermission> perms) {
+        int pf = 0;
+
+        for (PosixFilePermission p : perms) {
+            switch (p) {
+                case OWNER_READ:
+                    pf |= SftpConstants.S_IRUSR;
+                    break;
+                case OWNER_WRITE:
+                    pf |= SftpConstants.S_IWUSR;
+                    break;
+                case OWNER_EXECUTE:
+                    pf |= SftpConstants.S_IXUSR;
+                    break;
+                case GROUP_READ:
+                    pf |= SftpConstants.S_IRGRP;
+                    break;
+                case GROUP_WRITE:
+                    pf |= SftpConstants.S_IWGRP;
+                    break;
+                case GROUP_EXECUTE:
+                    pf |= SftpConstants.S_IXGRP;
+                    break;
+                case OTHERS_READ:
+                    pf |= SftpConstants.S_IROTH;
+                    break;
+                case OTHERS_WRITE:
+                    pf |= SftpConstants.S_IWOTH;
+                    break;
+                case OTHERS_EXECUTE:
+                    pf |= SftpConstants.S_IXOTH;
+                    break;
+                default:    // ignored
+            }
+        }
+
+        return String.format("%04o", pf);
+    }
+
+    /**
+     * Uses the host, port and username to create a unique identifier
+     *
+     * @param uri The {@link URI} - <B>Note:</B> not checked to make sure
+     *            that the scheme is {@code sftp://}
+     * @return The unique identifier
+     * @see #getFileSystemIdentifier(String, int, String)
+     */
+    public static String getFileSystemIdentifier(URI uri) {
+        String userInfo = ValidateUtils.checkNotNullAndNotEmpty(uri.getUserInfo(), "UserInfo not provided");
+        String[] ui = GenericUtils.split(userInfo, ':');
+        ValidateUtils.checkTrue(GenericUtils.length(ui) == 2, "Invalid user info: %s", userInfo);
+        return getFileSystemIdentifier(uri.getHost(), uri.getPort(), ui[0]);
+    }
+
+    /**
+     * Uses the remote host address, port and current username to create a unique identifier
+     *
+     * @param session The {@link ClientSession}
+     * @return The unique identifier
+     * @see #getFileSystemIdentifier(String, int, String)
+     */
+    public static String getFileSystemIdentifier(ClientSession session) {
+        IoSession ioSession = session.getIoSession();
+        SocketAddress addr = ioSession.getRemoteAddress();
+        String username = session.getUsername();
+        if (addr instanceof InetSocketAddress) {
+            InetSocketAddress inetAddr = (InetSocketAddress) addr;
+            return getFileSystemIdentifier(inetAddr.getHostString(), inetAddr.getPort(), username);
+        } else {
+            return getFileSystemIdentifier(addr.toString(), SshConfigFileReader.DEFAULT_PORT, username);
+        }
+    }
+
+    public static String getFileSystemIdentifier(String host, int port, String username) {
+        return GenericUtils.trimToEmpty(host) + ':'
+                + ((port <= 0) ? SshConfigFileReader.DEFAULT_PORT : port) + ':'
+                + GenericUtils.trimToEmpty(username);
+    }
+
+    public static URI createFileSystemURI(String host, int port, String username, String password) {
+        return createFileSystemURI(host, port, username, password, Collections.emptyMap());
+    }
+
+    public static URI createFileSystemURI(String host, int port, String username, String password, Map<String, ?> params) {
+        StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
+        sb.append(SftpConstants.SFTP_SUBSYSTEM_NAME)
+            .append("://").append(username).append(':').append(password)
+            .append('@').append(host).append(':').append(port)
+            .append('/');
+        if (GenericUtils.size(params) > 0) {
+            boolean firstParam = true;
+            // Cannot use forEach because firstParam is not effectively final
+            for (Map.Entry<String, ?> pe : params.entrySet()) {
+                String key = pe.getKey();
+                Object value = pe.getValue();
+                sb.append(firstParam ? '?' : '&').append(key).append('=').append(Objects.toString(value, null));
+                firstParam = false;
+            }
+        }
+
+        return URI.create(sb.toString());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpInputStreamWithChannel.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpInputStreamWithChannel.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpInputStreamWithChannel.java
new file mode 100644
index 0000000..cb75fb6
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpInputStreamWithChannel.java
@@ -0,0 +1,179 @@
+/*
+ * 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.Collection;
+import java.util.Objects;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.OpenMode;
+import org.apache.sshd.common.util.io.InputStreamWithChannel;
+
+/**
+ * Implements an input stream for reading from a remote file
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpInputStreamWithChannel extends InputStreamWithChannel {
+    private final SftpClient client;
+    private final String path;
+    private byte[] bb;
+    private byte[] buffer;
+    private int index;
+    private int available;
+    private CloseableHandle handle;
+    private long offset;
+
+    public SftpInputStreamWithChannel(SftpClient client, int bufferSize, String path, Collection<OpenMode> mode) throws IOException {
+        this.client = Objects.requireNonNull(client, "No SFTP client instance");
+        this.path = path;
+        bb = new byte[1];
+        buffer = new byte[bufferSize];
+        handle = client.open(path, mode);
+    }
+
+    /**
+     * The client instance
+     *
+     * @return {@link SftpClient} instance used to access the remote file
+     */
+    public final SftpClient getClient() {
+        return client;
+    }
+
+    /**
+     * The remotely accessed file path
+     *
+     * @return Remote file path
+     */
+    public final String getPath() {
+        return path;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return (handle != null) && handle.isOpen();
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+        throw new UnsupportedOperationException("mark(" + readlimit + ") N/A");
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        long skipLen;
+        long newIndex = index + n;
+        long bufLen = Math.max(0L, available);
+        if (newIndex > bufLen) {
+            // exceeded current buffer
+            long extraLen = newIndex - bufLen;
+            offset += extraLen;
+            skipLen = Math.max(0, bufLen - index) + extraLen;
+            // force re-fill of read buffer
+            index = 0;
+            available = 0;
+        } else if (newIndex < 0) {
+            // went back - check how far back
+            long startOffset = offset - bufLen;
+            long newOffset = startOffset + newIndex; // actually a subtraction since newIndex is negative
+            newOffset = Math.max(0L, newOffset);
+            skipLen = index - newIndex; // actually a adding it since newIndex is negative
+            offset = newOffset;
+            // force re-fill of read buffer
+            index = 0;
+            available = 0;
+        } else {
+            // still within current buffer
+            index = (int) newIndex;
+            // need to use absolute value since skip size may have been negative
+            skipLen = Math.abs(n);
+        }
+
+        return skipLen;
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        offset = 0L;
+        // force re-fill of read buffer
+        index = 0;
+        available = 0;
+    }
+
+    @Override
+    public int read() throws IOException {
+        int read = read(bb, 0, 1);
+        if (read > 0) {
+            return bb[0] & 0xFF;
+        }
+
+        return read;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("read(" + getPath() + ") stream closed");
+        }
+
+        int idx = off;
+        while (len > 0) {
+            if (index >= available) {
+                available = client.read(handle, offset, buffer, 0, buffer.length);
+                if (available < 0) {
+                    if (idx == off) {
+                        return -1;
+                    } else {
+                        break;
+                    }
+                }
+                offset += available;
+                index = 0;
+            }
+            if (index >= available) {
+                break;
+            }
+            int nb = Math.min(len, available - index);
+            System.arraycopy(buffer, index, b, idx, nb);
+            index += nb;
+            idx += nb;
+            len -= nb;
+        }
+
+        return idx - off;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (isOpen()) {
+            try {
+                handle.close();
+            } finally {
+                handle = null;
+            }
+        }
+    }
+}
\ No newline at end of file