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();
+ }
+
+}