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 2015/02/23 16:30:08 UTC

[07/15] mina-sshd git commit: [SSHD-377] Create a nio FileSystem implementation for sftp

[SSHD-377] Create a nio FileSystem implementation for sftp


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

Branch: refs/heads/master
Commit: cc4d787733388f192a2f776ae4fcde2a471f858d
Parents: e9ee318
Author: Guillaume Nodet <gn...@apache.org>
Authored: Fri Nov 28 15:12:06 2014 +0100
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Mon Feb 23 16:21:30 2015 +0100

----------------------------------------------------------------------
 .../java.nio.file.spi.FileSystemProvider        |  20 +
 .../java/org/apache/sshd/ClientSession.java     |   3 +
 .../java/org/apache/sshd/client/SftpClient.java |   5 +-
 .../sshd/client/session/ClientSessionImpl.java  |   7 +
 .../sshd/client/sftp/DefaultSftpClient.java     |   4 +
 .../apache/sshd/client/sftp/SftpFileSystem.java | 314 +++++++++
 .../client/sftp/SftpFileSystemProvider.java     | 665 +++++++++++++++++++
 .../org/apache/sshd/client/sftp/SftpPath.java   |  49 ++
 .../common/file/nativefs/NativeSshFileNio.java  |   4 +-
 .../sshd/common/file/util/BaseFileSystem.java   | 239 +++++++
 .../apache/sshd/common/file/util/BasePath.java  | 367 ++++++++++
 .../sshd/common/file/util/ImmutableList.java    |  61 ++
 .../apache/sshd/server/sftp/SftpSubsystem.java  |   5 +-
 .../org/apache/sshd/SftpFileSystemTest.java     | 112 ++++
 .../sshd/common/file/util/BasePathTest.java     | 560 ++++++++++++++++
 15 files changed, 2411 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/filtered-resources/META-INF/services/java.nio.file.spi.FileSystemProvider
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/filtered-resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/sshd-core/src/main/filtered-resources/META-INF/services/java.nio.file.spi.FileSystemProvider
new file mode 100644
index 0000000..08d4b4a
--- /dev/null
+++ b/sshd-core/src/main/filtered-resources/META-INF/services/java.nio.file.spi.FileSystemProvider
@@ -0,0 +1,20 @@
+##
+## 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.
+##
+
+org.apache.sshd.client.sftp.SftpFileSystemProvider

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
index bee81f7..867dadb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
@@ -19,6 +19,7 @@
 package org.apache.sshd;
 
 import java.io.IOException;
+import java.nio.file.FileSystem;
 import java.security.KeyPair;
 import java.util.Map;
 
@@ -160,6 +161,8 @@ public interface ClientSession extends Session {
      */
     SftpClient createSftpClient() throws IOException;
 
+    FileSystem createSftpFileSystem() throws IOException;
+
     /**
      * Start forwarding the given local address on the client to the given address on the server.
      */

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
index 35b4260..18a9ba4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
@@ -21,12 +21,13 @@ package org.apache.sshd.client;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.ByteBuffer;
 import java.util.EnumSet;
 
 /**
  * @author <a href="http://mina.apache.org">Apache MINA Project</a>
  */
-public interface SftpClient {
+public interface SftpClient extends AutoCloseable {
 
     //
     // Permission flags
@@ -135,6 +136,8 @@ public interface SftpClient {
      */
     void close() throws IOException;
 
+    boolean isClosing();
+
     //
     // Low level API
     //

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index 7d9a8c9..a1872b8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -20,6 +20,7 @@ package org.apache.sshd.client.session;
 
 import java.io.IOException;
 import java.net.SocketAddress;
+import java.nio.file.FileSystem;
 import java.security.KeyPair;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -47,6 +48,8 @@ import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.future.DefaultAuthFuture;
 import org.apache.sshd.client.scp.DefaultScpClient;
 import org.apache.sshd.client.sftp.DefaultSftpClient;
+import org.apache.sshd.client.sftp.SftpFileSystem;
+import org.apache.sshd.client.sftp.SftpFileSystemProvider;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.Service;
 import org.apache.sshd.common.ServiceFactory;
@@ -278,6 +281,10 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession
         return new DefaultSftpClient(this);
     }
 
+    public FileSystem createSftpFileSystem() throws IOException {
+        return new SftpFileSystem(new SftpFileSystemProvider((org.apache.sshd.SshClient) factoryManager), this);
+    }
+
     public SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException {
         return getConnectionService().getTcpipForwarder().startLocalPortForwarding(local, remote);
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
index cbed805..526d92d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
@@ -132,6 +132,10 @@ public class DefaultSftpClient implements SftpClient {
         init();
     }
 
+    public boolean isClosing() {
+        return closing;
+    }
+
     public void close() throws IOException {
         this.channel.close(false);
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java
new file mode 100644
index 0000000..d6de9df
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystem.java
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.sftp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.client.SftpClient;
+import org.apache.sshd.common.file.util.BaseFileSystem;
+import org.apache.sshd.common.file.util.ImmutableList;
+
+public class SftpFileSystem extends BaseFileSystem<SftpPath> {
+
+    private final ClientSession session;
+    private final Queue<SftpClient> pool;
+    private final ThreadLocal<Wrapper> wrappers = new ThreadLocal<>();
+    private SftpPath defaultDir;
+
+    public SftpFileSystem(SftpFileSystemProvider provider, ClientSession session) throws IOException {
+        super(provider);
+        this.session = session;
+        this.pool = new LinkedBlockingQueue<>(8);
+        try (SftpClient client = getClient()) {
+            defaultDir = getPath(client.canonicalPath("."));
+        }
+    }
+
+    @Override
+    protected SftpPath create(String root, ImmutableList<String> names) {
+        return new SftpPath(this, root, names);
+    }
+
+    public ClientSession getSession() {
+        return session;
+    }
+
+    public SftpClient getClient() throws IOException {
+        Wrapper wrapper = wrappers.get();
+        if (wrapper == null) {
+            while (wrapper == null) {
+                SftpClient client = pool.poll();
+                if (client == null) {
+                    client = session.createSftpClient();
+                }
+                if (!client.isClosing()) {
+                    wrapper = new Wrapper(client);
+                }
+            }
+            wrappers.set(wrapper);
+        } else {
+            wrapper.increment();
+        }
+        return wrapper;
+    }
+
+    @Override
+    public void close() throws IOException {
+        session.close(true);
+    }
+
+    @Override
+    public boolean isOpen() {
+        return !session.isClosing();
+    }
+
+    @Override
+    public Set<String> supportedFileAttributeViews() {
+        Set<String> set = new HashSet<>();
+        set.addAll(Arrays.asList("basic", "posix", "owner"));
+        return Collections.unmodifiableSet(set);
+    }
+
+    @Override
+    public UserPrincipalLookupService getUserPrincipalLookupService() {
+        return new DefaultUserPrincipalLookupService();
+    }
+
+    public SftpPath getDefaultDir() {
+        return defaultDir;
+    }
+
+    private class Wrapper implements SftpClient {
+
+        private final SftpClient delegate;
+        private final AtomicInteger count = new AtomicInteger(1);
+
+        private Wrapper(SftpClient delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public boolean isClosing() {
+            return false;
+        }
+
+        @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 Handle open(String path, EnumSet<OpenMode> options) throws IOException {
+            return delegate.open(path, options);
+        }
+
+        @Override
+        public void close(Handle handle) throws IOException {
+            delegate.close(handle);
+        }
+
+        @Override
+        public void remove(String path) throws IOException {
+            delegate.remove(path);
+        }
+
+        @Override
+        public void rename(String oldPath, String newPath) throws IOException {
+            delegate.rename(oldPath, newPath);
+        }
+
+        @Override
+        public int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException {
+            return delegate.read(handle, fileOffset, dst, dstoff, len);
+        }
+
+        @Override
+        public void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException {
+            delegate.write(handle, fileOffset, src, srcoff, len);
+        }
+
+        @Override
+        public void mkdir(String path) throws IOException {
+            delegate.mkdir(path);
+        }
+
+        @Override
+        public void rmdir(String path) throws IOException {
+            delegate.rmdir(path);
+        }
+
+        @Override
+        public Handle openDir(String path) throws IOException {
+            return delegate.openDir(path);
+        }
+
+        @Override
+        public DirEntry[] readDir(Handle handle) throws IOException {
+            return delegate.readDir(handle);
+        }
+
+        @Override
+        public String canonicalPath(String canonical) throws IOException {
+            return delegate.canonicalPath(canonical);
+        }
+
+        @Override
+        public Attributes stat(String path) throws IOException {
+            return delegate.stat(path);
+        }
+
+        @Override
+        public Attributes lstat(String path) throws IOException {
+            return delegate.lstat(path);
+        }
+
+        @Override
+        public Attributes stat(Handle handle) throws IOException {
+            return delegate.stat(handle);
+        }
+
+        @Override
+        public void setStat(String path, Attributes attributes) throws IOException {
+            delegate.setStat(path, attributes);
+        }
+
+        @Override
+        public void setStat(Handle handle, Attributes attributes) throws IOException {
+            delegate.setStat(handle, attributes);
+        }
+
+        @Override
+        public String readLink(String path) throws IOException {
+            return delegate.readLink(path);
+        }
+
+        @Override
+        public void symLink(String linkPath, String targetPath) throws IOException {
+            delegate.symLink(linkPath, targetPath);
+        }
+
+        @Override
+        public Iterable<DirEntry> readDir(String path) throws IOException {
+            return delegate.readDir(path);
+        }
+
+        @Override
+        public InputStream read(String path) throws IOException {
+            return delegate.read(path);
+        }
+
+        @Override
+        public InputStream read(String path, EnumSet<OpenMode> mode) throws IOException {
+            return delegate.read(path, mode);
+        }
+
+        @Override
+        public OutputStream write(String path) throws IOException {
+            return delegate.write(path);
+        }
+
+        @Override
+        public OutputStream write(String path, EnumSet<OpenMode> mode) throws IOException {
+            return delegate.write(path, mode);
+        }
+
+    }
+
+    protected static class DefaultUserPrincipalLookupService extends UserPrincipalLookupService {
+
+        @Override
+        public UserPrincipal lookupPrincipalByName(String name) throws IOException {
+            return new DefaultUserPrincipal(name);
+        }
+
+        @Override
+        public GroupPrincipal lookupPrincipalByGroupName(String group) throws IOException {
+            return new DefaultGroupPrincipal(group);
+        }
+
+    }
+
+    protected static class DefaultUserPrincipal implements UserPrincipal {
+
+        private final String name;
+
+        public DefaultUserPrincipal(String name) {
+            if (name == null) {
+                throw new IllegalArgumentException("name is null");
+            }
+            this.name = name;
+        }
+
+        @Override
+        public 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 name.equals(that.name);
+        }
+
+        @Override
+        public int hashCode() {
+            return name.hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return name;
+        }
+
+    }
+
+    protected static class DefaultGroupPrincipal extends DefaultUserPrincipal implements GroupPrincipal {
+
+        public DefaultGroupPrincipal(String name) {
+            super(name);
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java
new file mode 100644
index 0000000..07c0dc2
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpFileSystemProvider.java
@@ -0,0 +1,665 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.sftp;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SeekableByteChannel;
+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.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.StandardOpenOption;
+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.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.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.SshBuilder;
+import org.apache.sshd.SshClient;
+import org.apache.sshd.client.SftpClient;
+import org.apache.sshd.client.SftpException;
+
+public class SftpFileSystemProvider extends FileSystemProvider {
+
+    public static final int SSH_FX_NO_SUCH_FILE =         2;
+    public static final int SSH_FX_FILE_ALREADY_EXISTS = 11;
+
+    final SshClient client;
+    final Map<String, SftpFileSystem> fileSystems = new HashMap<String, SftpFileSystem>();
+
+    public SftpFileSystemProvider() {
+        this(null);
+    }
+
+    public SftpFileSystemProvider(SshClient client) {
+        if (client == null) {
+            // TODO: make this configurable using system properties
+            client = SshBuilder.client().build();
+        }
+        this.client = client;
+        this.client.start();
+    }
+
+    @Override
+    public String getScheme() {
+        return "sftp";
+    }
+
+    @Override
+    public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+        synchronized (fileSystems) {
+            String authority = uri.getAuthority();
+            SftpFileSystem fileSystem = fileSystems.get(authority);
+            if (fileSystem != null) {
+                throw new FileSystemAlreadyExistsException(authority);
+            }
+            String host = uri.getHost();
+            String userInfo = uri.getUserInfo();
+            if (host == null) {
+                throw new IllegalArgumentException("Host not provided");
+            }
+            if (userInfo == null) {
+                throw new IllegalArgumentException("UserInfo not provided");
+            }
+            String[] ui = userInfo.split(":");
+            ClientSession session;
+            try {
+                session = client.connect(ui[0], host, uri.getPort() > 0 ? uri.getPort() : 22)
+                        .await().getSession();
+            } catch (InterruptedException e) {
+                throw new InterruptedIOException();
+            }
+            session.addPasswordIdentity(ui[1]);
+            session.auth().verify();
+            fileSystem = new SftpFileSystem(this, session);
+            fileSystems.put(authority, fileSystem);
+            return fileSystem;
+        }
+    }
+
+    @Override
+    public FileSystem getFileSystem(URI uri) {
+        synchronized (fileSystems) {
+            String authority = uri.getAuthority();
+            SftpFileSystem fileSystem = fileSystems.get(authority);
+            if (fileSystem == null) {
+                throw new FileSystemNotFoundException(authority);
+            }
+            return fileSystem;
+        }
+    }
+
+    @Override
+    public Path getPath(URI uri) {
+        return getFileSystem(uri).getPath(uri.getPath());
+    }
+
+    @Override
+    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+        final SftpPath p = toSftpPath(path);
+        final EnumSet<SftpClient.OpenMode> modes = EnumSet.noneOf(SftpClient.OpenMode.class);
+        for (OpenOption option : options) {
+            if (option == StandardOpenOption.READ) {
+                modes.add(SftpClient.OpenMode.Read);
+            } else if (option == StandardOpenOption.APPEND) {
+                modes.add(SftpClient.OpenMode.Append);
+            } else if (option == StandardOpenOption.CREATE) {
+                modes.add(SftpClient.OpenMode.Create);
+            } else if (option == StandardOpenOption.TRUNCATE_EXISTING) {
+                modes.add(SftpClient.OpenMode.Truncate);
+            } else if (option == StandardOpenOption.WRITE) {
+                modes.add(SftpClient.OpenMode.Write);
+            } else if (option == StandardOpenOption.CREATE_NEW) {
+                modes.add(SftpClient.OpenMode.Create);
+                modes.add(SftpClient.OpenMode.Exclusive);
+            } else {
+                throw new IllegalArgumentException("Unsupported open option " + option);
+            }
+        }
+        if (modes.isEmpty()) {
+            modes.add(SftpClient.OpenMode.Read);
+        }
+        return new SeekableByteChannel() {
+            final SftpClient sftp = p.getFileSystem().getClient();
+            final SftpClient.Handle handle = sftp.open(p.toString(), modes);
+            long pos = 0;
+            @Override
+            public int read(ByteBuffer dst) throws IOException {
+                if (dst.hasArray()) {
+                    int read = sftp.read(handle, pos, dst.array(), dst.arrayOffset() + dst.position(), dst.remaining());
+                    if (read > 0) {
+                        dst.position(dst.position() + read);
+                        pos += read;
+                    }
+                    return read;
+                } else {
+                    int remaining = Math.min(8192, dst.remaining());
+                    byte[] buf = new byte[remaining];
+                    int read = sftp.read(handle, pos, buf, 0, remaining);
+                    if (read > 0) {
+                        dst.put(buf, 0, read);
+                        pos += read;
+                    }
+                    return read;
+                }
+            }
+
+            @Override
+            public int write(ByteBuffer src) throws IOException {
+                if (src.hasArray()) {
+                    int rem = src.remaining();
+                    sftp.write(handle, pos, src.array(), src.arrayOffset() + src.position(), rem);
+                    src.position(src.position() + rem);
+                    pos += rem;
+                    return rem;
+                } else {
+                    byte[] buf = new byte[Math.min(8192, src.remaining())];
+                    src.get(buf);
+                    sftp.write(handle, pos, buf, 0, buf.length);
+                    pos += buf.length;
+                    return buf.length;
+                }
+            }
+
+            @Override
+            public long position() throws IOException {
+                if (pos < 0) {
+                    throw new ClosedChannelException();
+                }
+                return pos;
+            }
+
+            @Override
+            public SeekableByteChannel position(long newPosition) throws IOException {
+                if (newPosition < 0) {
+                    throw new IllegalArgumentException();
+                }
+                pos = newPosition;
+                return this;
+            }
+
+            @Override
+            public long size() throws IOException {
+                return sftp.stat(handle).size;
+            }
+
+            @Override
+            public SeekableByteChannel truncate(long size) throws IOException {
+                sftp.setStat(handle, new SftpClient.Attributes().size(size));
+                return this;
+            }
+
+            @Override
+            public boolean isOpen() {
+                return pos >= 0;
+            }
+
+            @Override
+            public void close() throws IOException {
+                sftp.close(handle);
+                sftp.close();
+                pos = -1;
+            }
+        };
+    }
+
+    @Override
+    public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
+        final SftpPath p = toSftpPath(dir);
+        return new DirectoryStream<Path>() {
+            final SftpClient sftp = p.getFileSystem().getClient();
+            final Iterable<SftpClient.DirEntry> iter = sftp.readDir(p.toString());
+            @Override
+            public Iterator<Path> iterator() {
+                return new Iterator<Path>() {
+                    final Iterator<SftpClient.DirEntry> it = iter.iterator();
+                    @Override
+                    public boolean hasNext() {
+                        return it.hasNext();
+                    }
+
+                    @Override
+                    public Path next() {
+                        SftpClient.DirEntry entry = it.next();
+                        return p.resolve(entry.filename);
+                    }
+
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException();
+                    }
+                };
+            }
+
+            @Override
+            public void close() throws IOException {
+                sftp.close();
+            }
+        };
+    }
+
+    @Override
+    public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+        SftpPath p = toSftpPath(dir);
+        try (SftpClient sftp = p.getFileSystem().getClient()) {
+            // TODO: handle attributes
+            try {
+                sftp.mkdir(dir.toString());
+            } catch (SftpException e) {
+                if (e.getStatus() == SSH_FX_FILE_ALREADY_EXISTS) {
+                    throw new FileAlreadyExistsException(p.toString());
+                }
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public void delete(Path path) throws IOException {
+        SftpPath p = toSftpPath(path);
+        checkAccess(p, AccessMode.WRITE);
+        try (SftpClient sftp = p.getFileSystem().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 {
+        // TODO
+    }
+
+    @Override
+    public void move(Path source, Path target, CopyOption... options) throws IOException {
+        // TODO
+
+    }
+
+    @Override
+    public boolean isSameFile(Path path1, Path path2) throws IOException {
+        SftpPath p1 = toSftpPath(path1);
+        SftpPath p2 = toSftpPath(path2);
+        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 {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
+        SftpPath l = toSftpPath(link);
+        SftpPath t = toSftpPath(target);
+        if (l.getFileSystem() != t.getFileSystem()) {
+            throw new ProviderMismatchException();
+        }
+        try (SftpClient client = l.getFileSystem().getClient()) {
+            client.symLink(l.toString(), t.toString());
+        }
+    }
+
+    @Override
+    public Path readSymbolicLink(Path link) throws IOException {
+        SftpPath l = toSftpPath(link);
+        try (SftpClient client = l.getFileSystem().getClient()) {
+            return l.getFileSystem().getPath(client.readLink(l.toString()));
+        }
+    }
+
+    @Override
+    public void checkAccess(Path path, AccessMode... modes) throws IOException {
+        SftpPath p = toSftpPath(path);
+        boolean w = false;
+        boolean x = false;
+        for (AccessMode mode : modes) {
+            switch (mode) {
+            case READ:
+                break;
+            case WRITE:
+                w = true;
+                break;
+            case EXECUTE:
+                x = true;
+                break;
+            default:
+                throw new UnsupportedOperationException();
+            }
+        }
+        BasicFileAttributes attrs = getFileAttributeView(p, BasicFileAttributeView.class).readAttributes();
+        if (attrs == null && !(p.isAbsolute() && p.getNameCount() == 0)) {
+            throw new NoSuchFileException(toString());
+        }
+        if (x || w && p.getFileSystem().isReadOnly()) {
+            throw new AccessDeniedException(toString());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <V extends FileAttributeView> V getFileAttributeView(final Path path, Class<V> type, final LinkOption... options) {
+        if (type.isAssignableFrom(PosixFileAttributeView.class)) {
+            return (V) new PosixFileAttributeView() {
+                @Override
+                public String name() {
+                    return "view";
+                }
+
+                @Override
+                public PosixFileAttributes readAttributes() throws IOException {
+                    SftpPath p = toSftpPath(path);
+                    final SftpClient.Attributes attributes;
+                    try (SftpClient client = p.getFileSystem().getClient()) {
+                        try {
+                            if (followLinks(options)) {
+                                attributes = client.stat(p.toString());
+                            } else {
+                                attributes = client.lstat(p.toString());
+                            }
+                        } catch (SftpException e) {
+                            if (e.getStatus() == SSH_FX_NO_SUCH_FILE) {
+                                throw new NoSuchFileException(p.toString());
+                            }
+                            throw e;
+                        }
+                    }
+                    return new PosixFileAttributes() {
+                        @Override
+                        public UserPrincipal owner() {
+                            // TODO
+                            return null;
+                        }
+
+                        @Override
+                        public GroupPrincipal group() {
+                            // TODO
+                            return null;
+                        }
+
+                        @Override
+                        public Set<PosixFilePermission> permissions() {
+                            // TODO
+                            return null;
+                        }
+
+                        @Override
+                        public FileTime lastModifiedTime() {
+                            return FileTime.from(attributes.mtime, TimeUnit.SECONDS);
+                        }
+
+                        @Override
+                        public FileTime lastAccessTime() {
+                            return FileTime.from(attributes.atime, TimeUnit.SECONDS);
+                        }
+
+                        @Override
+                        public FileTime creationTime() {
+                            // TODO
+                            return null;
+                        }
+
+                        @Override
+                        public boolean isRegularFile() {
+                            return attributes.isRegularFile();
+                        }
+
+                        @Override
+                        public boolean isDirectory() {
+                            return attributes.isDirectory();
+                        }
+
+                        @Override
+                        public boolean isSymbolicLink() {
+                            return attributes.isSymbolicLink();
+                        }
+
+                        @Override
+                        public boolean isOther() {
+                            return attributes.isOther();
+                        }
+
+                        @Override
+                        public long size() {
+                            return attributes.size;
+                        }
+
+                        @Override
+                        public Object fileKey() {
+                            // TODO
+                            return null;
+                        }
+                    };
+                }
+
+                @Override
+                public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException {
+                    if (lastModifiedTime != null) {
+                        setAttribute(path, "lastModifiedTime", lastModifiedTime, options);
+                    }
+                    if (lastAccessTime != null) {
+                        setAttribute(path, "lastAccessTime", lastAccessTime, options);
+                    }
+                    if (createTime != null) {
+                        setAttribute(path, "createTime", createTime, options);
+                    }
+                }
+
+                @Override
+                public void setPermissions(Set<PosixFilePermission> perms) throws IOException {
+                    setAttribute(path, "permissions", perms, options);
+                }
+
+                @Override
+                public void setGroup(GroupPrincipal group) throws IOException {
+                    setAttribute(path, "group", group, options);
+                }
+
+                @Override
+                public UserPrincipal getOwner() throws IOException {
+                    return readAttributes().owner();
+                }
+
+                @Override
+                public void setOwner(UserPrincipal owner) throws IOException {
+                    setAttribute(path, "owner", owner, options);
+                }
+            };
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
+        if (type.isAssignableFrom(PosixFileAttributes.class)) {
+            return (A) getFileAttributeView(path, PosixFileAttributeView.class, options).readAttributes();
+        }
+        throw new UnsupportedOperationException();
+    }
+
+    @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);
+        }
+        SftpPath p = toSftpPath(path);
+        if (!p.getFileSystem().supportedFileAttributeViews().contains(view)) {
+            throw new UnsupportedOperationException();
+        }
+        PosixFileAttributes v = readAttributes(path, PosixFileAttributes.class, options);
+        if ("*".equals(attrs)) {
+            attrs = "lastModifiedTime,lastAccessTime,creationTime,size,isRegularFile,isDirectory,isSymbolicLink,isOther,fileKey,owner,permissions,group";
+        }
+        Map<String, Object> map = new HashMap<>();
+        for (String attr : attrs.split(",")) {
+            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;
+            }
+        }
+        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);
+        }
+        SftpPath p = toSftpPath(path);
+        if (!p.getFileSystem().supportedFileAttributeViews().contains(view)) {
+            throw new UnsupportedOperationException();
+        }
+        SftpClient.Attributes attributes = new SftpClient.Attributes();
+        switch (attr) {
+        case "lastModifiedTime":
+            attributes.mtime = (int) ((FileTime) value).to(TimeUnit.SECONDS);
+            break;
+        case "lastAccessTime":
+            attributes.atime = (int) ((FileTime) value).to(TimeUnit.SECONDS);
+            break;
+        case "size":
+            attributes.size = (long) value;
+            break;
+        case "owner":
+        case "permissions":
+        case "group":
+            // TODO: handle those
+            throw new IllegalArgumentException(attr);
+        case "creationTime":
+        case "isRegularFile":
+        case "isDirectory":
+        case "isSymbolicLink":
+        case "isOther":
+        case "fileKey":
+            throw new IllegalArgumentException(attr);
+        }
+        try (SftpClient client = p.getFileSystem().getClient()) {
+            client.setStat(p.toString(), attributes);
+        }
+    }
+
+    private SftpPath toSftpPath(Path path) {
+        if (path == null) {
+            throw new NullPointerException();
+        }
+        if (!(path instanceof SftpPath)) {
+            throw new ProviderMismatchException();
+        }
+        return (SftpPath) path;
+    }
+
+    static boolean followLinks(LinkOption... paramVarArgs)
+    {
+        boolean bool = true;
+        for (LinkOption localLinkOption : paramVarArgs) {
+            if (localLinkOption == LinkOption.NOFOLLOW_LINKS) {
+                bool = false;
+            }
+        }
+        return bool;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpPath.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpPath.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpPath.java
new file mode 100644
index 0000000..8e70c62
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/SftpPath.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.sftp;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.LinkOption;
+
+import org.apache.sshd.common.file.util.BasePath;
+import org.apache.sshd.common.file.util.ImmutableList;
+
+public class SftpPath extends BasePath<SftpPath, SftpFileSystem> {
+
+    public SftpPath(SftpFileSystem fileSystem, String root, ImmutableList<String> names) {
+        super(fileSystem, root, names);
+    }
+
+    public URI toUri() {
+        // TODO
+        return null;
+    }
+
+    public SftpPath toRealPath(LinkOption... options) throws IOException {
+//        try (SftpClient client = fileSystem.getClient()) {
+//            client.realP
+//        }
+        // TODO: handle links
+        SftpPath absolute = toAbsolutePath();
+        fileSystem.provider().checkAccess(absolute);
+        return absolute;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFileNio.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFileNio.java b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFileNio.java
index 9753ed8..b0596d1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFileNio.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFileNio.java
@@ -174,8 +174,8 @@ public class NativeSshFileNio extends NativeSshFile {
 
     public void createSymbolicLink(SshFile destination) throws IOException {
         Path link = file.toPath();
-        Path target = Paths.get(destination.getAbsolutePath());
-        Files.createSymbolicLink(target, link);
+        Path target = Paths.get(destination.toString());
+        Files.createSymbolicLink(link, target);
     }
 
     private EnumSet<Permission> fromPerms(Set<PosixFilePermission> perms) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/common/file/util/BaseFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/util/BaseFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/common/file/util/BaseFileSystem.java
new file mode 100644
index 0000000..68869ca
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/util/BaseFileSystem.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.common.file.util;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.WatchService;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+public abstract class BaseFileSystem<T extends Path> extends FileSystem {
+
+    private final FileSystemProvider fileSystemProvider;
+
+    public BaseFileSystem(FileSystemProvider fileSystemProvider) {
+        this.fileSystemProvider = fileSystemProvider;
+    }
+
+    public T getDefaultDir() {
+        return getPath("/");
+    }
+
+    @Override
+    public boolean isReadOnly() {
+        return false;
+    }
+
+    @Override
+    public FileSystemProvider provider() {
+        return fileSystemProvider;
+    }
+
+    @Override
+    public String getSeparator() {
+        return "/";
+    }
+
+    @Override
+    public Iterable<Path> getRootDirectories() {
+        return Collections.<Path>singleton(create("/"));
+    }
+
+    @Override
+    public Iterable<FileStore> getFileStores() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public T getPath(String first, String... more) {
+        StringBuilder sb = new StringBuilder();
+        appendDedupSep(sb, first);
+        for (String segment : more) {
+            if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '/') {
+                sb.append('/');
+            }
+            appendDedupSep(sb, segment);
+        }
+        if (sb.length() > 1 && sb.charAt(sb.length() - 1) == '/') {
+            sb.setLength(sb.length() - 1);
+        }
+        String path = sb.toString();
+        String root = null;
+        if (path.startsWith("/")) {
+            root = "/";
+            path = path.substring(1);
+        }
+        String[] names;
+        if (path.length() > 0) {
+            names = path.split("/");
+        } else {
+            names = new String[0];
+        }
+        return create(root, names);
+    }
+
+    private void appendDedupSep(StringBuilder sb, String s) {
+        for (int i = 0; i < s.length(); i++) {
+            char ch = s.charAt(i);
+            if (ch != '/' || sb.length() == 0 || sb.charAt(sb.length() - 1) != '/') {
+                sb.append(ch);
+            }
+        }
+    }
+
+    @Override
+    public PathMatcher getPathMatcher(String syntaxAndPattern) {
+        int colonIndex = syntaxAndPattern.indexOf(':');
+        if (colonIndex <= 0 || colonIndex == syntaxAndPattern.length() - 1) {
+            throw new IllegalArgumentException("syntaxAndPattern must have form \"syntax:pattern\" but was \"" + syntaxAndPattern + "\"");
+        }
+
+        String syntax = syntaxAndPattern.substring(0, colonIndex);
+        String pattern = syntaxAndPattern.substring(colonIndex + 1);
+        String expr;
+        switch (syntax) {
+        case "glob":
+            expr = globToRegex(pattern);
+            break;
+        case "regex":
+            expr = pattern;
+            break;
+        default:
+            throw new UnsupportedOperationException("Unsupported syntax \'" + syntax + "\'");
+        }
+        final Pattern regex = Pattern.compile(expr);
+        return new PathMatcher() {
+            @Override
+            public boolean matches(Path path) {
+                return regex.matcher(path.toString()).matches();
+            }
+        };
+    }
+
+    private String globToRegex(String pattern) {
+        StringBuilder sb = new StringBuilder(pattern.length());
+        int inGroup = 0;
+        int inClass = 0;
+        int firstIndexInClass = -1;
+        char[] arr = pattern.toCharArray();
+        for (int i = 0; i < arr.length; i++) {
+            char ch = arr[i];
+            switch (ch) {
+            case '\\':
+                if (++i >= arr.length) {
+                    sb.append('\\');
+                } else {
+                    char next = arr[i];
+                    switch (next) {
+                    case ',':
+                        // escape not needed
+                        break;
+                    case 'Q':
+                    case 'E':
+                        // extra escape needed
+                        sb.append('\\');
+                    default:
+                        sb.append('\\');
+                    }
+                    sb.append(next);
+                }
+                break;
+            case '*':
+                if (inClass == 0)
+                    sb.append(".*");
+                else
+                    sb.append('*');
+                break;
+            case '?':
+                if (inClass == 0)
+                    sb.append('.');
+                else
+                    sb.append('?');
+                break;
+            case '[':
+                inClass++;
+                firstIndexInClass = i+1;
+                sb.append('[');
+                break;
+            case ']':
+                inClass--;
+                sb.append(']');
+                break;
+            case '.':
+            case '(':
+            case ')':
+            case '+':
+            case '|':
+            case '^':
+            case '$':
+            case '@':
+            case '%':
+                if (inClass == 0 || (firstIndexInClass == i && ch == '^'))
+                    sb.append('\\');
+                sb.append(ch);
+                break;
+            case '!':
+                if (firstIndexInClass == i)
+                    sb.append('^');
+                else
+                    sb.append('!');
+                break;
+            case '{':
+                inGroup++;
+                sb.append('(');
+                break;
+            case '}':
+                inGroup--;
+                sb.append(')');
+                break;
+            case ',':
+                if (inGroup > 0)
+                    sb.append('|');
+                else
+                    sb.append(',');
+                break;
+            default:
+                sb.append(ch);
+            }
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public WatchService newWatchService() throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    protected T create(String root, String... names) {
+        return create(root, new ImmutableList<>(names));
+    }
+
+    protected T create(String root, Collection<String> names) {
+        return create(root, new ImmutableList<>(names.toArray(new String[names.size()])));
+    }
+
+    protected abstract T create(String root, ImmutableList<String> names);
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/common/file/util/BasePath.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/util/BasePath.java b/sshd-core/src/main/java/org/apache/sshd/common/file/util/BasePath.java
new file mode 100644
index 0000000..a5fde82
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/util/BasePath.java
@@ -0,0 +1,367 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.common.file.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.ProviderMismatchException;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.AbstractList;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+public abstract class BasePath<T extends BasePath<T, FS>, FS extends BaseFileSystem<T>> implements Path {
+
+    protected final FS fileSystem;
+    protected final String root;
+    protected final ImmutableList<String> names;
+
+    public BasePath(FS fileSystem, String root, ImmutableList<String> names) {
+        this.fileSystem = fileSystem;
+        this.root = root;
+        this.names = names;
+    }
+
+    @SuppressWarnings("unchecked")
+    protected T asT() {
+        return (T) this;
+    }
+    protected T create(String root, String... names) {
+        return create(root, new ImmutableList<>(names));
+    }
+
+    protected T create(String root, Collection<String> names) {
+        return create(root, new ImmutableList<>(names.toArray(new String[names.size()])));
+    }
+
+    protected T create(String root, ImmutableList<String> names) {
+        return fileSystem.create(root, names);
+    }
+
+    public FS getFileSystem() {
+        return fileSystem;
+    }
+
+    public boolean isAbsolute() {
+        return root != null;
+    }
+
+    public T getRoot() {
+        if (isAbsolute()) {
+            return create(root);
+        }
+        return null;
+    }
+
+    public T getFileName() {
+        if (!names.isEmpty()) {
+            return create(null, names.get(names.size() - 1));
+        }
+        return null;
+    }
+
+    public T getParent() {
+        if (names.isEmpty() || names.size() == 1 && root == null) {
+            return null;
+        }
+        return create(root, names.subList(0, names.size() - 1));
+    }
+
+    public int getNameCount() {
+        return names.size();
+    }
+
+    public T getName(int index) {
+        if (index < 0 || index >= names.size()) {
+            throw new IllegalArgumentException();
+        }
+        return create(null, names.subList(index, index + 1));
+    }
+
+    public T subpath(int beginIndex, int endIndex) {
+        if ((beginIndex < 0) || (beginIndex >= names.size()) || (endIndex > names.size()) || (beginIndex >= endIndex)) {
+            throw new IllegalArgumentException();
+        }
+        return create(null, names.subList(beginIndex, endIndex));
+    }
+
+    private static boolean startsWith(List<?> list, List<?> other) {
+        return list.size() >= other.size() && list.subList(0, other.size()).equals(other);
+    }
+
+    public boolean startsWith(Path other) {
+        T p1 = asT();
+        T p2 = checkPath(other);
+        return p1.getFileSystem().equals(p2.getFileSystem())
+                && Objects.equals(p1.root, p2.root)
+                && startsWith(p1.names, p2.names);
+    }
+
+    public boolean startsWith(String other) {
+        return startsWith(getFileSystem().getPath(other));
+    }
+
+    private static boolean endsWith(List<?> list, List<?> other) {
+        return other.size() <= list.size() && list.subList(list.size() - other.size(), list.size()).equals(other);
+    }
+
+    public boolean endsWith(Path other) {
+        T p1 = asT();
+        T p2 = checkPath(other);
+        if (p2.isAbsolute()) {
+            return p1.compareTo(p2) == 0;
+        }
+        return endsWith(p1.names, p2.names);
+    }
+
+    public boolean endsWith(String other) {
+        return endsWith(getFileSystem().getPath(other));
+    }
+
+    private boolean isNormal() {
+        if (getNameCount() == 0 || getNameCount() == 1 && !isAbsolute()) {
+            return true;
+        }
+        boolean foundNonParentName = isAbsolute(); // if there's a root, the path doesn't start with ..
+        boolean normal = true;
+        for (String name : names) {
+            if (name.equals("..")) {
+                if (foundNonParentName) {
+                    normal = false;
+                    break;
+                }
+            } else {
+                if (name.equals(".")) {
+                    normal = false;
+                    break;
+                }
+                foundNonParentName = true;
+            }
+        }
+        return normal;
+    }
+
+    public T normalize() {
+        if (isNormal()) {
+            return asT();
+        }
+
+        Deque<String> newNames = new ArrayDeque<>();
+        for (String name : names) {
+            if (name.equals("..")) {
+                String lastName = newNames.peekLast();
+                if (lastName != null && !lastName.equals("..")) {
+                    newNames.removeLast();
+                } else if (!isAbsolute()) {
+                    // if there's a root and we have an extra ".." that would go up above the root, ignore it
+                    newNames.add(name);
+                }
+            } else if (!name.equals(".")) {
+                newNames.add(name);
+            }
+        }
+
+        return newNames.equals(names) ? asT() : create(root, newNames);
+    }
+
+    public T resolve(Path other) {
+        T p1 = asT();
+        T p2 = checkPath(other);
+        if (p2.isAbsolute()) {
+            return p2;
+        }
+        if (p2.names.isEmpty()) {
+            return p1;
+        }
+        String[] names = new String[p1.names.size() + p2.names.size()];
+        int index = 0;
+        for (String p : p1.names) {
+            names[index++] = p;
+        }
+        for (String p : p2.names) {
+            names[index++] = p;
+        }
+        return create(p1.root, names);
+    }
+
+    public T resolve(String other) {
+        return resolve(getFileSystem().getPath(other));
+    }
+
+    public Path resolveSibling(Path other) {
+        if (other == null) {
+            throw new NullPointerException();
+        }
+        T parent = getParent();
+        return parent == null ? other : parent.resolve(other);
+    }
+
+    public Path resolveSibling(String other) {
+        return resolveSibling(getFileSystem().getPath(other));
+    }
+
+    public T relativize(Path other) {
+        T p1 = asT();
+        T p2 = checkPath(other);
+        if (!Objects.equals(p1.getRoot(), p2.getRoot())) {
+            throw new IllegalArgumentException("Paths have different roots: " + this + ", " + other);
+        }
+        if (p2.equals(p1)) {
+            return create(null);
+        }
+        if (p1.root == null && p1.names.isEmpty()) {
+            return p2;
+        }
+        // Common subsequence
+        int sharedSubsequenceLength = 0;
+        for (int i = 0; i < Math.min(p1.names.size(), p2.names.size()); i++) {
+            if (p1.names.get(i).equals(p2.names.get(i))) {
+                sharedSubsequenceLength++;
+            } else {
+                break;
+            }
+        }
+        int extraNamesInThis = Math.max(0, p1.names.size() - sharedSubsequenceLength);
+        List<String> extraNamesInOther = (p2.names.size() <= sharedSubsequenceLength)
+                ? Collections.<String>emptyList()
+                : p2.names.subList(sharedSubsequenceLength, p2.names.size());
+        List<String> parts = new ArrayList<>(extraNamesInThis + extraNamesInOther.size());
+        // add .. for each extra name in this path
+        parts.addAll(Collections.nCopies(extraNamesInThis, ".."));
+        // add each extra name in the other path
+        parts.addAll(extraNamesInOther);
+        return create(null, parts);
+    }
+
+    public T toAbsolutePath() {
+        if (isAbsolute()) {
+            return asT();
+        }
+        return fileSystem.getDefaultDir().resolve(this);
+    }
+
+    public File toFile() {
+        throw new UnsupportedOperationException();
+    }
+
+    public WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public Iterator<Path> iterator() {
+        return new AbstractList<Path>() {
+            @Override
+            public Path get(int index) {
+                return getName(index);
+            }
+
+            @Override
+            public int size() {
+                return getNameCount();
+            }
+        }.iterator();
+    }
+
+    public int compareTo(Path paramPath) {
+        T p1 = asT();
+        T p2 = checkPath(paramPath);
+        int c = compare(p1.root, p2.root);
+        if (c != 0) {
+            return c;
+        }
+        for (int i = 0; i < Math.min(p1.names.size(), p2.names.size()); i++) {
+            String n1 = p1.names.get(i);
+            String n2 = p2.names.get(i);
+            c = compare(n1, n2);
+            if (c != 0) {
+                return c;
+            }
+        }
+        return p1.names.size() - p2.names.size();
+    }
+
+    private int compare(String s1, String s2) {
+        if (s1 == null) {
+            return s2 == null ? 0 : -1;
+        } else {
+            return s2 == null ? +1 : s1.compareTo(s2);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private T checkPath(Path paramPath) {
+        if (paramPath == null) {
+            throw new NullPointerException();
+        }
+        if (paramPath.getClass() != getClass()) {
+            throw new ProviderMismatchException();
+        }
+        T t = (T) paramPath;
+        if (t.fileSystem.provider() != this.fileSystem.provider()) {
+            throw new ProviderMismatchException();
+        }
+        return t;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = getFileSystem().hashCode();
+        // use hash codes from toString() form of names
+        hash = 31 * hash + (root == null ? 0 : root.hashCode());
+        for (String name : names) {
+            hash = 31 * hash + name.hashCode();
+        }
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof Path
+                && compareTo((Path) obj) == 0;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        if (root != null) {
+            sb.append(root);
+        }
+        for (String name : names) {
+            if (sb.length() > 0 && sb.charAt(sb.length()  - 1) != '/') {
+                sb.append(fileSystem.getSeparator());
+            }
+            sb.append(name);
+        }
+        return sb.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/common/file/util/ImmutableList.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/util/ImmutableList.java b/sshd-core/src/main/java/org/apache/sshd/common/file/util/ImmutableList.java
new file mode 100644
index 0000000..88d9f5c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/util/ImmutableList.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.common.file.util;
+
+import java.util.AbstractList;
+
+/**
+ * Simple immutable array list
+ * @param <T>
+ */
+public class ImmutableList<T> extends AbstractList<T> {
+
+    final T[] data;
+    final int from;
+    final int to;
+
+    public ImmutableList(T[] data) {
+        this(data, 0, data.length);
+    }
+
+    public ImmutableList(T[] data, int from, int to) {
+        this.data = data;
+        this.from = from;
+        this.to = to;
+    }
+
+    @Override
+    public T get(int index) {
+        return data[from + index];
+    }
+
+    @Override
+    public int size() {
+        return to - from;
+    }
+
+    @Override
+    public ImmutableList<T> subList(int fromIndex, int toIndex) {
+        if (fromIndex == from && toIndex == to) {
+            return this;
+        }
+        return new ImmutableList<>(data, from + fromIndex, from + toIndex);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
index 3ce1c78..643a1b7 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
@@ -165,6 +165,8 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     public static final int SSH_FX_CONNECTION_LOST =   7;
     public static final int SSH_FX_OP_UNSUPPORTED =    8;
 
+    public static final int SSH_FX_FILE_ALREADY_EXISTS = 11; // Not in v3, but we need it
+
     public static final int SSH_FILEXFER_ATTR_SIZE =        0x00000001;
     public static final int SSH_FILEXFER_ATTR_UIDGID =      0x00000002;
     public static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004;
@@ -768,7 +770,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                     SshFile p = resolveFile(path);
                     if (p.doesExist()) {
                         if (p.isDirectory()) {
-                            sendStatus(id, SSH_FX_FAILURE, p.getAbsolutePath());
+                            sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, p.getAbsolutePath());
                         } else {
                             sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
                         }
@@ -885,6 +887,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                 log.debug("Received SSH_FXP_SYMLINK (linkpath={}, targetpath={})", linkpath, targetpath);
                 try {
                     SshFile link = resolveFile(linkpath);
+                    // TODO: resolving the file is wrong, we should keep it relative
                     SshFile target = resolveFile(targetpath);
                     link.createSymbolicLink(target);
                     sendStatus(id, SSH_FX_OK, "");

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/cc4d7877/sshd-core/src/test/java/org/apache/sshd/SftpFileSystemTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/SftpFileSystemTest.java b/sshd-core/src/test/java/org/apache/sshd/SftpFileSystemTest.java
new file mode 100644
index 0000000..42d1dcc
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/SftpFileSystemTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.command.ScpCommandFactory;
+import org.apache.sshd.server.sftp.SftpSubsystem;
+import org.apache.sshd.util.BaseTest;
+import org.apache.sshd.util.BogusPasswordAuthenticator;
+import org.apache.sshd.util.EchoShellFactory;
+import org.apache.sshd.util.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class SftpFileSystemTest extends BaseTest {
+
+    private SshServer sshd;
+    private int port;
+
+    @Before
+    public void setUp() throws Exception {
+        sshd = SshServer.setUpDefaultServer();
+        sshd.setKeyPairProvider(Utils.createTestHostKeyProvider());
+        sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystem.Factory()));
+        sshd.setCommandFactory(new ScpCommandFactory());
+        sshd.setShellFactory(new EchoShellFactory());
+        sshd.setPasswordAuthenticator(new BogusPasswordAuthenticator());
+        sshd.start();
+        port = sshd.getPort();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        sshd.stop(true);
+    }
+
+    @Test
+    public void testFileSystem() throws Exception {
+        Utils.deleteRecursive(new File("target/sftp"));
+
+        FileSystem fs = FileSystems.newFileSystem(URI.create("sftp://x:x@localhost:" + port + "/"), null);
+        Path root = fs.getRootDirectories().iterator().next();
+        try (DirectoryStream<Path> ds = Files.newDirectoryStream(root)) {
+            for (Path child : ds) {
+                System.out.println(child);
+            }
+        }
+        Path file = fs.getPath("target/sftp/client/test.txt");
+        Files.createDirectories(file.getParent());
+        Files.write(file, "Hello world\n".getBytes());
+        String buf = new String(Files.readAllBytes(file));
+        assertEquals("Hello world\n", buf);
+
+        Map<String, Object> attrs = Files.readAttributes(file, "*");
+        System.out.println(attrs);
+
+        // TODO: symbolic links only work for absolute files
+//        Path link = fs.getPath("target/sftp/client/test2.txt");
+//        Files.createSymbolicLink(link, link.relativize(file));
+//        assertTrue(Files.isSymbolicLink(link));
+//        assertEquals("test.txt", Files.readSymbolicLink(link).toString());
+
+        Path link = fs.getPath("target/sftp/client/test2.txt");
+        Files.createSymbolicLink(link, file);
+        assertTrue(Files.isSymbolicLink(link));
+        assertEquals(file.toAbsolutePath().toString(), Files.readSymbolicLink(link).toString());
+
+        attrs = Files.readAttributes(file, "*", LinkOption.NOFOLLOW_LINKS);
+        System.out.println(attrs);
+
+        buf = new String(Files.readAllBytes(file));
+        assertEquals("Hello world\n", buf);
+
+        Files.delete(file);
+
+        fs.close();
+    }
+
+}