You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/11/11 16:50:52 UTC
[4/8] mina-sshd git commit: [SSHD-861] Added
SftpFileSystemClientSessionInitializer hook in SftpFileSystemProvider
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e3b8acd7/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpFileSystemProvider.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpFileSystemProvider.java
new file mode 100644
index 0000000..fdec2f3
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpFileSystemProvider.java
@@ -0,0 +1,1340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.fs;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.FileSystemException;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.ProviderMismatchException;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
+import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.auth.BasicCredentialsImpl;
+import org.apache.sshd.common.auth.BasicCredentialsProvider;
+import org.apache.sshd.common.auth.MutableBasicCredentials;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A registered {@link FileSystemProvider} that registers the "sftp://"
+ * scheme so that URLs with this protocol are handled as remote SFTP {@link Path}-s
+ * - e.g., "{@code sftp://user:password@host/remote/file/path}"
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpFileSystemProvider extends FileSystemProvider {
+ public static final String READ_BUFFER_PROP_NAME = "sftp-fs-read-buffer-size";
+ public static final int DEFAULT_READ_BUFFER_SIZE = SftpClient.DEFAULT_READ_BUFFER_SIZE;
+ public static final String WRITE_BUFFER_PROP_NAME = "sftp-fs-write-buffer-size";
+ public static final int DEFAULT_WRITE_BUFFER_SIZE = SftpClient.DEFAULT_WRITE_BUFFER_SIZE;
+ public static final String CONNECT_TIME_PROP_NAME = "sftp-fs-connect-time";
+ public static final long DEFAULT_CONNECT_TIME = SftpClient.DEFAULT_WAIT_TIMEOUT;
+ public static final String AUTH_TIME_PROP_NAME = "sftp-fs-auth-time";
+ public static final long DEFAULT_AUTH_TIME = SftpClient.DEFAULT_WAIT_TIMEOUT;
+ public static final String NAME_DECORDER_CHARSET_PROP_NAME = "sftp-fs-name-decoder-charset";
+ public static final Charset DEFAULT_NAME_DECODER_CHARSET = SftpClient.DEFAULT_NAME_DECODING_CHARSET;
+
+ /**
+ * <P>
+ * URI parameter that can be used to specify a special version selection. Options are:
+ * </P>
+ * <UL>
+ * <LI>{@code max} - select maximum available version for the client</LI>
+ * <LI>{@code min} - select minimum available version for the client</LI>
+ * <LI>{@code current} - whatever version is reported by the server</LI>
+ * <LI>{@code nnn} - select <U>only</U> the specified version</LI>
+ * <LI>{@code a,b,c} - select one of the specified versions (if available) in preference order</LI>
+ * </UL>
+ */
+ public static final String VERSION_PARAM = "version";
+
+ public static final Set<Class<? extends FileAttributeView>> UNIVERSAL_SUPPORTED_VIEWS =
+ Collections.unmodifiableSet(
+ GenericUtils.asSet(
+ PosixFileAttributeView.class,
+ FileOwnerAttributeView.class,
+ BasicFileAttributeView.class
+ ));
+
+ protected final Logger log;
+
+ private final SshClient clientInstance;
+ private final SftpClientFactory factory;
+ private final SftpVersionSelector versionSelector;
+ private final NavigableMap<String, SftpFileSystem> fileSystems = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ private SftpFileSystemClientSessionInitializer fsSessionInitializer = SftpFileSystemClientSessionInitializer.DEFAULT;
+
+ public SftpFileSystemProvider() {
+ this((SshClient) null);
+ }
+
+ public SftpFileSystemProvider(SftpVersionSelector selector) {
+ this(null, selector);
+ }
+
+ /**
+ * @param client The {@link SshClient} to use - if {@code null} then a
+ * default one will be setup and started. Otherwise, it is assumed that
+ * the client has already been started
+ * @see SshClient#setUpDefaultClient()
+ */
+ public SftpFileSystemProvider(SshClient client) {
+ this(client, SftpVersionSelector.CURRENT);
+ }
+
+ public SftpFileSystemProvider(SshClient client, SftpVersionSelector selector) {
+ this(client, null, selector);
+ }
+
+ public SftpFileSystemProvider(SshClient client, SftpClientFactory factory, SftpVersionSelector selector) {
+ this.log = LoggerFactory.getLogger(getClass());
+ this.factory = factory;
+ this.versionSelector = selector;
+ if (client == null) {
+ // TODO: make this configurable using system properties
+ client = SshClient.setUpDefaultClient();
+ client.start();
+ }
+ this.clientInstance = client;
+ }
+
+ @Override
+ public String getScheme() {
+ return SftpConstants.SFTP_SUBSYSTEM_NAME;
+ }
+
+ public final SftpVersionSelector getSftpVersionSelector() {
+ return versionSelector;
+ }
+
+ public final SshClient getClientInstance() {
+ return clientInstance;
+ }
+
+ public SftpClientFactory getSftpClientFactory() {
+ return factory;
+ }
+
+ public SftpFileSystemClientSessionInitializer getSftpFileSystemClientSessionInitializer() {
+ return fsSessionInitializer;
+ }
+
+ public void setSftpFileSystemClientSessionInitializer(SftpFileSystemClientSessionInitializer initializer) {
+ fsSessionInitializer = Objects.requireNonNull(initializer, "No initializer provided");
+ }
+
+ @Override // NOTE: co-variant return
+ public SftpFileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+ String host = ValidateUtils.checkNotNullAndNotEmpty(uri.getHost(), "Host not provided");
+ int port = uri.getPort();
+ if (port <= 0) {
+ port = SshConstants.DEFAULT_PORT;
+ }
+
+ BasicCredentialsProvider credentials = parseCredentials(uri);
+ ValidateUtils.checkState(credentials != null, "No credentials provided");
+
+ String username = credentials.getUsername();
+ String id = getFileSystemIdentifier(host, port, username);
+ SftpFileSystemInitializationContext context = new SftpFileSystemInitializationContext(id, uri, env);
+ context.setHost(host);
+ context.setPort(port);
+ context.setCredentials(credentials);
+
+ Map<String, Object> params = resolveFileSystemParameters(env, parseURIParameters(uri));
+ PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(params);
+ context.setPropertyResolver(resolver);
+ context.setMaxConnectTime(resolver.getLongProperty(CONNECT_TIME_PROP_NAME, DEFAULT_CONNECT_TIME));
+ context.setMaxAuthTime(resolver.getLongProperty(AUTH_TIME_PROP_NAME, DEFAULT_AUTH_TIME));
+
+ SftpVersionSelector selector = resolveSftpVersionSelector(uri, getSftpVersionSelector(), resolver);
+ Charset decodingCharset = PropertyResolverUtils.getCharset(
+ resolver, NAME_DECORDER_CHARSET_PROP_NAME, DEFAULT_NAME_DECODER_CHARSET);
+
+ SftpFileSystemClientSessionInitializer initializer = getSftpFileSystemClientSessionInitializer();
+ SftpFileSystem fileSystem;
+ synchronized (fileSystems) {
+ if (fileSystems.containsKey(id)) {
+ throw new FileSystemAlreadyExistsException(id);
+ }
+
+ // TODO try and find a way to avoid doing this while locking the file systems cache
+ ClientSession session = null;
+ try {
+ session = initializer.createClientSession(this, context);
+
+ // Make any extra configuration parameters available to the session
+ if (GenericUtils.size(params) > 0) {
+ // Cannot use forEach because the session is not effectively final
+ for (Map.Entry<String, ?> pe : params.entrySet()) {
+ String key = pe.getKey();
+ Object value = pe.getValue();
+ if (VERSION_PARAM.equalsIgnoreCase(key)) {
+ continue;
+ }
+
+ PropertyResolverUtils.updateProperty(session, key, value);
+ }
+
+ PropertyResolverUtils.updateProperty(session, SftpClient.NAME_DECODING_CHARSET, decodingCharset);
+ }
+
+ initializer.authenticateClientSession(this, context, session);
+
+ fileSystem = initializer.createSftpFileSystem(this, context, session, selector);
+ fileSystems.put(id, fileSystem);
+ } catch (Exception e) {
+ if (session != null) {
+ try {
+ session.close();
+ } catch (IOException t) {
+ if (log.isDebugEnabled()) {
+ log.debug("Failed (" + t.getClass().getSimpleName() + ")"
+ + " to close session for new file system on " + host + ":" + port
+ + " due to " + e.getClass().getSimpleName() + "[" + e.getMessage() + "]"
+ + ": " + t.getMessage());
+ }
+ }
+ }
+
+ if (e instanceof IOException) {
+ throw (IOException) e;
+ } else if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+
+ fileSystem.setReadBufferSize(resolver.getIntProperty(READ_BUFFER_PROP_NAME, DEFAULT_READ_BUFFER_SIZE));
+ fileSystem.setWriteBufferSize(resolver.getIntProperty(WRITE_BUFFER_PROP_NAME, DEFAULT_WRITE_BUFFER_SIZE));
+ if (log.isDebugEnabled()) {
+ log.debug("newFileSystem({}): {}", uri.toASCIIString(), fileSystem);
+ }
+ return fileSystem;
+ }
+
+ protected SftpVersionSelector resolveSftpVersionSelector(URI uri, SftpVersionSelector defaultSelector, PropertyResolver resolver) {
+ String preference = resolver.getString(VERSION_PARAM);
+ if (GenericUtils.isEmpty(preference)) {
+ return defaultSelector;
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("resolveSftpVersionSelector({}) preference={}", uri, preference);
+ }
+
+ if ("max".equalsIgnoreCase(preference)) {
+ return SftpVersionSelector.MAXIMUM;
+ } else if ("min".equalsIgnoreCase(preference)) {
+ return SftpVersionSelector.MINIMUM;
+ } else if ("current".equalsIgnoreCase(preference)) {
+ return SftpVersionSelector.CURRENT;
+ }
+
+ String[] values = GenericUtils.split(preference, ',');
+ if (values.length == 1) {
+ return SftpVersionSelector.fixedVersionSelector(Integer.parseInt(values[0]));
+ }
+
+ int[] preferred = new int[values.length];
+ for (int index = 0; index < values.length; index++) {
+ preferred[index] = Integer.parseInt(values[index]);
+ }
+
+ return SftpVersionSelector.preferredVersionSelector(preferred);
+ }
+
+ // NOTE: URI parameters override environment ones
+ public static Map<String, Object> resolveFileSystemParameters(Map<String, ?> env, Map<String, Object> uriParams) {
+ if (GenericUtils.isEmpty(env)) {
+ return GenericUtils.isEmpty(uriParams) ? Collections.emptyMap() : uriParams;
+ } else if (GenericUtils.isEmpty(uriParams)) {
+ return Collections.unmodifiableMap(env);
+ }
+
+ Map<String, Object> resolved = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ resolved.putAll(env);
+ resolved.putAll(uriParams);
+ return resolved;
+ }
+
+ /**
+ * Attempts to parse the user information from the URI
+ *
+ * @param uri The {@link URI} value - ignored if {@code null} or does not
+ * contain any {@link URI#getUserInfo() user info}.
+ * @return The parsed credentials - {@code null} if none available
+ */
+ public static MutableBasicCredentials parseCredentials(URI uri) {
+ return parseCredentials((uri == null) ? "" : uri.getUserInfo());
+ }
+
+ public static MutableBasicCredentials parseCredentials(String userInfo) {
+ if (GenericUtils.isEmpty(userInfo)) {
+ return null;
+ }
+
+ String[] ui = GenericUtils.split(userInfo, ':');
+ ValidateUtils.checkTrue(GenericUtils.length(ui) == 2, "Invalid user info: %s", userInfo);
+ return new BasicCredentialsImpl(ui[0], ui[1]);
+ }
+
+ public static Map<String, Object> parseURIParameters(URI uri) {
+ return parseURIParameters((uri == null) ? "" : uri.getQuery());
+ }
+
+ public static Map<String, Object> parseURIParameters(String params) {
+ if (GenericUtils.isEmpty(params)) {
+ return Collections.emptyMap();
+ }
+
+ if (params.charAt(0) == '?') {
+ if (params.length() == 1) {
+ return Collections.emptyMap();
+ }
+ params = params.substring(1);
+ }
+
+ String[] pairs = GenericUtils.split(params, '&');
+ Map<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (String p : pairs) {
+ int pos = p.indexOf('=');
+ if (pos < 0) {
+ map.put(p, Boolean.TRUE);
+ continue;
+ }
+
+ String key = p.substring(0, pos);
+ String value = p.substring(pos + 1);
+ if (NumberUtils.isIntegerNumber(value)) {
+ map.put(key, Long.valueOf(value));
+ } else if ("true".equals(value) || "false".equals("value")) {
+ map.put(key, Boolean.valueOf(value));
+ } else {
+ map.put(key, value);
+ }
+ }
+
+ return map;
+ }
+
+ public SftpFileSystem newFileSystem(ClientSession session) throws IOException {
+ String id = getFileSystemIdentifier(session);
+ SftpFileSystem fileSystem;
+ synchronized (fileSystems) {
+ if (fileSystems.containsKey(id)) {
+ throw new FileSystemAlreadyExistsException(id);
+ }
+ fileSystem = new SftpFileSystem(this, id, session, factory, getSftpVersionSelector());
+ fileSystems.put(id, fileSystem);
+ }
+
+ fileSystem.setReadBufferSize(session.getIntProperty(READ_BUFFER_PROP_NAME, DEFAULT_READ_BUFFER_SIZE));
+ fileSystem.setWriteBufferSize(session.getIntProperty(WRITE_BUFFER_PROP_NAME, DEFAULT_WRITE_BUFFER_SIZE));
+ if (log.isDebugEnabled()) {
+ log.debug("newFileSystem: {}", fileSystem);
+ }
+
+ return fileSystem;
+ }
+
+ @Override
+ public FileSystem getFileSystem(URI uri) {
+ String id = getFileSystemIdentifier(uri);
+ SftpFileSystem fs = getFileSystem(id);
+ if (fs == null) {
+ throw new FileSystemNotFoundException(id);
+ }
+ return fs;
+ }
+
+ /**
+ * @param id File system identifier - ignored if {@code null}/empty
+ * @return The removed {@link SftpFileSystem} - {@code null} if no match
+ */
+ public SftpFileSystem removeFileSystem(String id) {
+ if (GenericUtils.isEmpty(id)) {
+ return null;
+ }
+
+ SftpFileSystem removed;
+ synchronized (fileSystems) {
+ removed = fileSystems.remove(id);
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("removeFileSystem({}): {}", id, removed);
+ }
+ return removed;
+ }
+
+ /**
+ * @param id File system identifier - ignored if {@code null}/empty
+ * @return The cached {@link SftpFileSystem} - {@code null} if no match
+ */
+ public SftpFileSystem getFileSystem(String id) {
+ if (GenericUtils.isEmpty(id)) {
+ return null;
+ }
+
+ synchronized (fileSystems) {
+ return fileSystems.get(id);
+ }
+ }
+
+ @Override
+ public Path getPath(URI uri) {
+ FileSystem fs = getFileSystem(uri);
+ return fs.getPath(uri.getPath());
+ }
+
+ @Override
+ public FileChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+ return newFileChannel(path, options, attrs);
+ }
+
+ @Override
+ public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+ Collection<SftpClient.OpenMode> modes = SftpClient.OpenMode.fromOpenOptions(options);
+ if (modes.isEmpty()) {
+ modes = EnumSet.of(SftpClient.OpenMode.Read, SftpClient.OpenMode.Write);
+ }
+ // TODO: process file attributes
+ return new SftpFileSystemChannel(toSftpPath(path), modes);
+ }
+
+ @Override
+ public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
+ final SftpPath p = toSftpPath(dir);
+ return new SftpDirectoryStream(p);
+ }
+
+ @Override
+ public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+ SftpPath p = toSftpPath(dir);
+ SftpFileSystem fs = p.getFileSystem();
+ if (log.isDebugEnabled()) {
+ log.debug("createDirectory({}) {} ({})", fs, dir, Arrays.asList(attrs));
+ }
+ try (SftpClient sftp = fs.getClient()) {
+ try {
+ sftp.mkdir(dir.toString());
+ } catch (SftpException e) {
+ int sftpStatus = e.getStatus();
+ if ((sftp.getVersion() == SftpConstants.SFTP_V3) && (sftpStatus == SftpConstants.SSH_FX_FAILURE)) {
+ try {
+ Attributes attributes = sftp.stat(dir.toString());
+ if (attributes != null) {
+ throw new FileAlreadyExistsException(p.toString());
+ }
+ } catch (SshException e2) {
+ e.addSuppressed(e2);
+ }
+ }
+ if (sftpStatus == SftpConstants.SSH_FX_FILE_ALREADY_EXISTS) {
+ throw new FileAlreadyExistsException(p.toString());
+ }
+ throw e;
+ }
+ for (FileAttribute<?> attr : attrs) {
+ setAttribute(p, attr.name(), attr.value());
+ }
+ }
+ }
+
+ @Override
+ public void delete(Path path) throws IOException {
+ SftpPath p = toSftpPath(path);
+ checkAccess(p, AccessMode.WRITE);
+
+ SftpFileSystem fs = p.getFileSystem();
+ if (log.isDebugEnabled()) {
+ log.debug("delete({}) {}", fs, path);
+ }
+
+ try (SftpClient sftp = fs.getClient()) {
+ BasicFileAttributes attributes = readAttributes(path, BasicFileAttributes.class);
+ if (attributes.isDirectory()) {
+ sftp.rmdir(path.toString());
+ } else {
+ sftp.remove(path.toString());
+ }
+ }
+ }
+
+ @Override
+ public void copy(Path source, Path target, CopyOption... options) throws IOException {
+ SftpPath src = toSftpPath(source);
+ SftpPath dst = toSftpPath(target);
+ if (src.getFileSystem() != dst.getFileSystem()) {
+ throw new ProviderMismatchException("Mismatched file system providers for " + src + " vs. " + dst);
+ }
+ checkAccess(src);
+
+ boolean replaceExisting = false;
+ boolean copyAttributes = false;
+ boolean noFollowLinks = false;
+ for (CopyOption opt : options) {
+ replaceExisting |= opt == StandardCopyOption.REPLACE_EXISTING;
+ copyAttributes |= opt == StandardCopyOption.COPY_ATTRIBUTES;
+ noFollowLinks |= opt == LinkOption.NOFOLLOW_LINKS;
+ }
+ LinkOption[] linkOptions = IoUtils.getLinkOptions(!noFollowLinks);
+
+ // attributes of source file
+ BasicFileAttributes attrs = readAttributes(source, BasicFileAttributes.class, linkOptions);
+ if (attrs.isSymbolicLink()) {
+ throw new IOException("Copying of symbolic links not supported");
+ }
+
+ // delete target if it exists and REPLACE_EXISTING is specified
+ Boolean status = IoUtils.checkFileExists(target, linkOptions);
+ if (status == null) {
+ throw new AccessDeniedException("Existence cannot be determined for copy target: " + target);
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("copy({})[{}] {} => {}", src.getFileSystem(), Arrays.asList(options), src, dst);
+ }
+
+ if (replaceExisting) {
+ deleteIfExists(target);
+ } else {
+ if (status) {
+ throw new FileAlreadyExistsException(target.toString());
+ }
+ }
+
+ // create directory or copy file
+ if (attrs.isDirectory()) {
+ createDirectory(target);
+ } else {
+ try (InputStream in = newInputStream(source);
+ OutputStream os = newOutputStream(target)) {
+ IoUtils.copy(in, os);
+ }
+ }
+
+ // copy basic attributes to target
+ if (copyAttributes) {
+ BasicFileAttributeView view = getFileAttributeView(target, BasicFileAttributeView.class, linkOptions);
+ try {
+ view.setTimes(attrs.lastModifiedTime(), attrs.lastAccessTime(), attrs.creationTime());
+ } catch (Throwable x) {
+ // rollback
+ try {
+ delete(target);
+ } catch (Throwable suppressed) {
+ x.addSuppressed(suppressed);
+ }
+ throw x;
+ }
+ }
+ }
+
+ @Override
+ public void move(Path source, Path target, CopyOption... options) throws IOException {
+ SftpPath src = toSftpPath(source);
+ SftpFileSystem fsSrc = src.getFileSystem();
+ SftpPath dst = toSftpPath(target);
+
+ if (src.getFileSystem() != dst.getFileSystem()) {
+ throw new ProviderMismatchException("Mismatched file system providers for " + src + " vs. " + dst);
+ }
+ checkAccess(src);
+
+ boolean replaceExisting = false;
+ boolean copyAttributes = false;
+ boolean noFollowLinks = false;
+ for (CopyOption opt : options) {
+ replaceExisting |= opt == StandardCopyOption.REPLACE_EXISTING;
+ copyAttributes |= opt == StandardCopyOption.COPY_ATTRIBUTES;
+ noFollowLinks |= opt == LinkOption.NOFOLLOW_LINKS;
+ }
+ LinkOption[] linkOptions = IoUtils.getLinkOptions(noFollowLinks);
+
+ // attributes of source file
+ BasicFileAttributes attrs = readAttributes(source, BasicFileAttributes.class, linkOptions);
+ if (attrs.isSymbolicLink()) {
+ throw new IOException("Moving of source symbolic link (" + source + ") to " + target + " not supported");
+ }
+
+ // delete target if it exists and REPLACE_EXISTING is specified
+ Boolean status = IoUtils.checkFileExists(target, linkOptions);
+ if (status == null) {
+ throw new AccessDeniedException("Existence cannot be determined for move target " + target);
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("move({})[{}] {} => {}", src.getFileSystem(), Arrays.asList(options), src, dst);
+ }
+
+ if (replaceExisting) {
+ deleteIfExists(target);
+ } else if (status) {
+ throw new FileAlreadyExistsException(target.toString());
+ }
+
+ try (SftpClient sftp = fsSrc.getClient()) {
+ sftp.rename(src.toString(), dst.toString());
+ }
+
+ // copy basic attributes to target
+ if (copyAttributes) {
+ BasicFileAttributeView view = getFileAttributeView(target, BasicFileAttributeView.class, linkOptions);
+ try {
+ view.setTimes(attrs.lastModifiedTime(), attrs.lastAccessTime(), attrs.creationTime());
+ } catch (Throwable x) {
+ // rollback
+ try {
+ delete(target);
+ } catch (Throwable suppressed) {
+ x.addSuppressed(suppressed);
+ }
+ throw x;
+ }
+ }
+ }
+
+ @Override
+ public boolean isSameFile(Path path1, Path path2) throws IOException {
+ SftpPath p1 = toSftpPath(path1);
+ SftpPath p2 = toSftpPath(path2);
+ if (p1.getFileSystem() != p2.getFileSystem()) {
+ throw new ProviderMismatchException("Mismatched file system providers for " + p1 + " vs. " + p2);
+ }
+ checkAccess(p1);
+ checkAccess(p2);
+ return p1.equals(p2);
+ }
+
+ @Override
+ public boolean isHidden(Path path) throws IOException {
+ return false;
+ }
+
+ @Override
+ public FileStore getFileStore(Path path) throws IOException {
+ FileSystem fs = path.getFileSystem();
+ if (!(fs instanceof SftpFileSystem)) {
+ throw new FileSystemException(path.toString(), path.toString(), "getFileStore(" + path + ") path not attached to an SFTP file system");
+ }
+
+ SftpFileSystem sftpFs = (SftpFileSystem) fs;
+ String id = sftpFs.getId();
+ SftpFileSystem cached = getFileSystem(id);
+ if (cached != sftpFs) {
+ throw new FileSystemException(path.toString(), path.toString(), "Mismatched file system instance for id=" + id);
+ }
+
+ return sftpFs.getFileStores().get(0);
+ }
+
+ @Override
+ public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
+ SftpPath l = toSftpPath(link);
+ SftpFileSystem fsLink = l.getFileSystem();
+ SftpPath t = toSftpPath(target);
+ if (fsLink != t.getFileSystem()) {
+ throw new ProviderMismatchException("Mismatched file system providers for " + l + " vs. " + t);
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("createSymbolicLink({})[{}] {} => {}", fsLink, Arrays.asList(attrs), link, target);
+ }
+
+ try (SftpClient client = fsLink.getClient()) {
+ client.symLink(l.toString(), t.toString());
+ }
+ }
+
+ @Override
+ public Path readSymbolicLink(Path link) throws IOException {
+ SftpPath l = toSftpPath(link);
+ SftpFileSystem fsLink = l.getFileSystem();
+ try (SftpClient client = fsLink.getClient()) {
+ String linkPath = client.readLink(l.toString());
+ if (log.isDebugEnabled()) {
+ log.debug("readSymbolicLink({})[{}] {} => {}", fsLink, link, linkPath);
+ }
+
+ return fsLink.getPath(linkPath);
+ }
+ }
+
+ @Override
+ public void checkAccess(Path path, AccessMode... modes) throws IOException {
+ SftpPath p = toSftpPath(path);
+ boolean w = false;
+ boolean x = false;
+ if (GenericUtils.length(modes) > 0) {
+ for (AccessMode mode : modes) {
+ switch (mode) {
+ case READ:
+ break;
+ case WRITE:
+ w = true;
+ break;
+ case EXECUTE:
+ x = true;
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported mode: " + mode);
+ }
+ }
+ }
+
+ BasicFileAttributes attrs = getFileAttributeView(p, BasicFileAttributeView.class).readAttributes();
+ if ((attrs == null) && !(p.isAbsolute() && p.getNameCount() == 0)) {
+ throw new NoSuchFileException(path.toString());
+ }
+
+ SftpFileSystem fs = p.getFileSystem();
+ if (x || (w && fs.isReadOnly())) {
+ throw new AccessDeniedException("Filesystem is read-only: " + path.toString());
+ }
+ }
+
+ @Override
+ public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, final LinkOption... options) {
+ if (isSupportedFileAttributeView(path, type)) {
+ if (AclFileAttributeView.class.isAssignableFrom(type)) {
+ return type.cast(new SftpAclFileAttributeView(this, path, options));
+ } else if (BasicFileAttributeView.class.isAssignableFrom(type)) {
+ return type.cast(new SftpPosixFileAttributeView(this, path, options));
+ }
+ }
+
+ throw new UnsupportedOperationException("getFileAttributeView(" + path + ") view not supported: " + type.getSimpleName());
+ }
+
+ public boolean isSupportedFileAttributeView(Path path, Class<? extends FileAttributeView> type) {
+ return isSupportedFileAttributeView(toSftpPath(path).getFileSystem(), type);
+ }
+
+ public boolean isSupportedFileAttributeView(SftpFileSystem fs, Class<? extends FileAttributeView> type) {
+ Collection<String> views = fs.supportedFileAttributeViews();
+ if ((type == null) || GenericUtils.isEmpty(views)) {
+ return false;
+ } else if (PosixFileAttributeView.class.isAssignableFrom(type)) {
+ return views.contains("posix");
+ } else if (AclFileAttributeView.class.isAssignableFrom(type)) {
+ return views.contains("acl"); // must come before owner view
+ } else if (FileOwnerAttributeView.class.isAssignableFrom(type)) {
+ return views.contains("owner");
+ } else if (BasicFileAttributeView.class.isAssignableFrom(type)) {
+ return views.contains("basic"); // must be last
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
+ if (type.isAssignableFrom(PosixFileAttributes.class)) {
+ return type.cast(getFileAttributeView(path, PosixFileAttributeView.class, options).readAttributes());
+ }
+
+ throw new UnsupportedOperationException("readAttributes(" + path + ")[" + type.getSimpleName() + "] N/A");
+ }
+
+ @Override
+ public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
+ String view;
+ String attrs;
+ int i = attributes.indexOf(':');
+ if (i == -1) {
+ view = "basic";
+ attrs = attributes;
+ } else {
+ view = attributes.substring(0, i++);
+ attrs = attributes.substring(i);
+ }
+
+ return readAttributes(path, view, attrs, options);
+ }
+
+ public Map<String, Object> readAttributes(Path path, String view, String attrs, LinkOption... options) throws IOException {
+ SftpPath p = toSftpPath(path);
+ SftpFileSystem fs = p.getFileSystem();
+ Collection<String> views = fs.supportedFileAttributeViews();
+ if (GenericUtils.isEmpty(views) || (!views.contains(view))) {
+ throw new UnsupportedOperationException("readAttributes(" + path + ")[" + view + ":" + attrs + "] view not supported: " + views);
+ }
+
+ if ("basic".equalsIgnoreCase(view) || "posix".equalsIgnoreCase(view) || "owner".equalsIgnoreCase(view)) {
+ return readPosixViewAttributes(p, view, attrs, options);
+ } else if ("acl".equalsIgnoreCase(view)) {
+ return readAclViewAttributes(p, view, attrs, options);
+ } else {
+ return readCustomViewAttributes(p, view, attrs, options);
+ }
+ }
+
+ protected Map<String, Object> readCustomViewAttributes(SftpPath path, String view, String attrs, LinkOption... options) throws IOException {
+ throw new UnsupportedOperationException("readCustomViewAttributes(" + path + ")[" + view + ":" + attrs + "] view not supported");
+ }
+
+ protected NavigableMap<String, Object> readAclViewAttributes(SftpPath path, String view, String attrs, LinkOption... options) throws IOException {
+ if ("*".equals(attrs)) {
+ attrs = "acl,owner";
+ }
+
+ SftpFileSystem fs = path.getFileSystem();
+ SftpClient.Attributes attributes;
+ try (SftpClient client = fs.getClient()) {
+ attributes = readRemoteAttributes(path, options);
+ }
+
+ NavigableMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ String[] attrValues = GenericUtils.split(attrs, ',');
+ boolean traceEnabled = log.isTraceEnabled();
+ for (String attr : attrValues) {
+ switch (attr) {
+ case "acl":
+ List<AclEntry> acl = attributes.getAcl();
+ if (acl != null) {
+ map.put(attr, acl);
+ }
+ break;
+ case "owner":
+ String owner = attributes.getOwner();
+ if (GenericUtils.length(owner) > 0) {
+ map.put(attr, new SftpFileSystem.DefaultUserPrincipal(owner));
+ }
+ break;
+ default:
+ if (traceEnabled) {
+ log.trace("readAclViewAttributes({})[{}] unknown attribute: {}", fs, attrs, attr);
+ }
+ }
+ }
+
+ return map;
+ }
+
+ public SftpClient.Attributes readRemoteAttributes(SftpPath path, LinkOption... options) throws IOException {
+ SftpFileSystem fs = path.getFileSystem();
+ try (SftpClient client = fs.getClient()) {
+ try {
+ SftpClient.Attributes attrs;
+ if (IoUtils.followLinks(options)) {
+ attrs = client.stat(path.toString());
+ } else {
+ attrs = client.lstat(path.toString());
+ }
+ if (log.isTraceEnabled()) {
+ log.trace("readRemoteAttributes({})[{}]: {}", fs, path, attrs);
+ }
+ return attrs;
+ } catch (SftpException e) {
+ if (e.getStatus() == SftpConstants.SSH_FX_NO_SUCH_FILE) {
+ throw new NoSuchFileException(path.toString());
+ }
+ throw e;
+ }
+ }
+ }
+
+ protected NavigableMap<String, Object> readPosixViewAttributes(
+ SftpPath path, String view, String attrs, LinkOption... options)
+ throws IOException {
+ PosixFileAttributes v = readAttributes(path, PosixFileAttributes.class, options);
+ if ("*".equals(attrs)) {
+ attrs = "lastModifiedTime,lastAccessTime,creationTime,size,isRegularFile,isDirectory,isSymbolicLink,isOther,fileKey,owner,permissions,group";
+ }
+
+ NavigableMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ boolean traceEnabled = log.isTraceEnabled();
+ String[] attrValues = GenericUtils.split(attrs, ',');
+ for (String attr : attrValues) {
+ switch (attr) {
+ case "lastModifiedTime":
+ map.put(attr, v.lastModifiedTime());
+ break;
+ case "lastAccessTime":
+ map.put(attr, v.lastAccessTime());
+ break;
+ case "creationTime":
+ map.put(attr, v.creationTime());
+ break;
+ case "size":
+ map.put(attr, v.size());
+ break;
+ case "isRegularFile":
+ map.put(attr, v.isRegularFile());
+ break;
+ case "isDirectory":
+ map.put(attr, v.isDirectory());
+ break;
+ case "isSymbolicLink":
+ map.put(attr, v.isSymbolicLink());
+ break;
+ case "isOther":
+ map.put(attr, v.isOther());
+ break;
+ case "fileKey":
+ map.put(attr, v.fileKey());
+ break;
+ case "owner":
+ map.put(attr, v.owner());
+ break;
+ case "permissions":
+ map.put(attr, v.permissions());
+ break;
+ case "group":
+ map.put(attr, v.group());
+ break;
+ default:
+ if (traceEnabled) {
+ log.trace("readPosixViewAttributes({})[{}:{}] ignored for {}", path, view, attr, attrs);
+ }
+ }
+ }
+ return map;
+ }
+
+ @Override
+ public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
+ String view;
+ String attr;
+ int i = attribute.indexOf(':');
+ if (i == -1) {
+ view = "basic";
+ attr = attribute;
+ } else {
+ view = attribute.substring(0, i++);
+ attr = attribute.substring(i);
+ }
+
+ setAttribute(path, view, attr, value, options);
+ }
+
+ public void setAttribute(Path path, String view, String attr, Object value, LinkOption... options) throws IOException {
+ SftpPath p = toSftpPath(path);
+ SftpFileSystem fs = p.getFileSystem();
+ Collection<String> views = fs.supportedFileAttributeViews();
+ if (GenericUtils.isEmpty(views) || (!views.contains(view))) {
+ throw new UnsupportedOperationException("setAttribute(" + path + ")[" + view + ":" + attr + "=" + value + "] view " + view + " not supported: " + views);
+ }
+
+ SftpClient.Attributes attributes = new SftpClient.Attributes();
+ switch (attr) {
+ case "lastModifiedTime":
+ attributes.modifyTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+ break;
+ case "lastAccessTime":
+ attributes.accessTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+ break;
+ case "creationTime":
+ attributes.createTime((int) ((FileTime) value).to(TimeUnit.SECONDS));
+ break;
+ case "size":
+ attributes.size(((Number) value).longValue());
+ break;
+ case "permissions": {
+ @SuppressWarnings("unchecked")
+ Set<PosixFilePermission> attrSet = (Set<PosixFilePermission>) value;
+ attributes.perms(attributesToPermissions(path, attrSet));
+ break;
+ }
+ case "owner":
+ attributes.owner(((UserPrincipal) value).getName());
+ break;
+ case "group":
+ attributes.group(((GroupPrincipal) value).getName());
+ break;
+ case "acl": {
+ ValidateUtils.checkTrue("acl".equalsIgnoreCase(view), "ACL cannot be set via view=%s", view);
+ @SuppressWarnings("unchecked")
+ List<AclEntry> acl = (List<AclEntry>) value;
+ attributes.acl(acl);
+ break;
+ }
+ case "isRegularFile":
+ case "isDirectory":
+ case "isSymbolicLink":
+ case "isOther":
+ case "fileKey":
+ throw new UnsupportedOperationException("setAttribute(" + path + ")[" + view + ":" + attr + "=" + value + "] modification N/A");
+ default:
+ if (log.isTraceEnabled()) {
+ log.trace("setAttribute({})[{}] ignore {}:{}={}", fs, path, view, attr, value);
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("setAttribute({}) {}: {}", fs, path, attributes);
+ }
+
+ try (SftpClient client = fs.getClient()) {
+ client.setStat(p.toString(), attributes);
+ }
+ }
+
+ public SftpPath toSftpPath(Path path) {
+ Objects.requireNonNull(path, "No path provided");
+ if (!(path instanceof SftpPath)) {
+ throw new ProviderMismatchException("Path is not SFTP: " + path);
+ }
+ return (SftpPath) path;
+ }
+
+ protected int attributesToPermissions(Path path, Collection<PosixFilePermission> perms) {
+ if (GenericUtils.isEmpty(perms)) {
+ return 0;
+ }
+
+ int pf = 0;
+ boolean traceEnabled = log.isTraceEnabled();
+ for (PosixFilePermission p : perms) {
+ switch (p) {
+ case OWNER_READ:
+ pf |= SftpConstants.S_IRUSR;
+ break;
+ case OWNER_WRITE:
+ pf |= SftpConstants.S_IWUSR;
+ break;
+ case OWNER_EXECUTE:
+ pf |= SftpConstants.S_IXUSR;
+ break;
+ case GROUP_READ:
+ pf |= SftpConstants.S_IRGRP;
+ break;
+ case GROUP_WRITE:
+ pf |= SftpConstants.S_IWGRP;
+ break;
+ case GROUP_EXECUTE:
+ pf |= SftpConstants.S_IXGRP;
+ break;
+ case OTHERS_READ:
+ pf |= SftpConstants.S_IROTH;
+ break;
+ case OTHERS_WRITE:
+ pf |= SftpConstants.S_IWOTH;
+ break;
+ case OTHERS_EXECUTE:
+ pf |= SftpConstants.S_IXOTH;
+ break;
+ default:
+ if (traceEnabled) {
+ log.trace("attributesToPermissions(" + path + ") ignored " + p);
+ }
+ }
+ }
+
+ return pf;
+ }
+
+ public static String getRWXPermissions(int perms) {
+ StringBuilder sb = new StringBuilder(10 /* 3 * rwx + (d)irectory */);
+ if ((perms & SftpConstants.S_IFLNK) == SftpConstants.S_IFLNK) {
+ sb.append('l');
+ } else if ((perms & SftpConstants.S_IFDIR) == SftpConstants.S_IFDIR) {
+ sb.append('d');
+ } else {
+ sb.append('-');
+ }
+
+ if ((perms & SftpConstants.S_IRUSR) == SftpConstants.S_IRUSR) {
+ sb.append('r');
+ } else {
+ sb.append('-');
+ }
+ if ((perms & SftpConstants.S_IWUSR) == SftpConstants.S_IWUSR) {
+ sb.append('w');
+ } else {
+ sb.append('-');
+ }
+ if ((perms & SftpConstants.S_IXUSR) == SftpConstants.S_IXUSR) {
+ sb.append('x');
+ } else {
+ sb.append('-');
+ }
+
+ if ((perms & SftpConstants.S_IRGRP) == SftpConstants.S_IRGRP) {
+ sb.append('r');
+ } else {
+ sb.append('-');
+ }
+ if ((perms & SftpConstants.S_IWGRP) == SftpConstants.S_IWGRP) {
+ sb.append('w');
+ } else {
+ sb.append('-');
+ }
+ if ((perms & SftpConstants.S_IXGRP) == SftpConstants.S_IXGRP) {
+ sb.append('x');
+ } else {
+ sb.append('-');
+ }
+
+ if ((perms & SftpConstants.S_IROTH) == SftpConstants.S_IROTH) {
+ sb.append('r');
+ } else {
+ sb.append('-');
+ }
+ if ((perms & SftpConstants.S_IWOTH) == SftpConstants.S_IWOTH) {
+ sb.append('w');
+ } else {
+ sb.append('-');
+ }
+ if ((perms & SftpConstants.S_IXOTH) == SftpConstants.S_IXOTH) {
+ sb.append('x');
+ } else {
+ sb.append('-');
+ }
+
+ return sb.toString();
+ }
+
+ public static String getOctalPermissions(int perms) {
+ Collection<PosixFilePermission> attrs = permissionsToAttributes(perms);
+ return getOctalPermissions(attrs);
+ }
+
+ public static Set<PosixFilePermission> permissionsToAttributes(int perms) {
+ Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class);
+ if ((perms & SftpConstants.S_IRUSR) == SftpConstants.S_IRUSR) {
+ p.add(PosixFilePermission.OWNER_READ);
+ }
+ if ((perms & SftpConstants.S_IWUSR) == SftpConstants.S_IWUSR) {
+ p.add(PosixFilePermission.OWNER_WRITE);
+ }
+ if ((perms & SftpConstants.S_IXUSR) == SftpConstants.S_IXUSR) {
+ p.add(PosixFilePermission.OWNER_EXECUTE);
+ }
+ if ((perms & SftpConstants.S_IRGRP) == SftpConstants.S_IRGRP) {
+ p.add(PosixFilePermission.GROUP_READ);
+ }
+ if ((perms & SftpConstants.S_IWGRP) == SftpConstants.S_IWGRP) {
+ p.add(PosixFilePermission.GROUP_WRITE);
+ }
+ if ((perms & SftpConstants.S_IXGRP) == SftpConstants.S_IXGRP) {
+ p.add(PosixFilePermission.GROUP_EXECUTE);
+ }
+ if ((perms & SftpConstants.S_IROTH) == SftpConstants.S_IROTH) {
+ p.add(PosixFilePermission.OTHERS_READ);
+ }
+ if ((perms & SftpConstants.S_IWOTH) == SftpConstants.S_IWOTH) {
+ p.add(PosixFilePermission.OTHERS_WRITE);
+ }
+ if ((perms & SftpConstants.S_IXOTH) == SftpConstants.S_IXOTH) {
+ p.add(PosixFilePermission.OTHERS_EXECUTE);
+ }
+ return p;
+ }
+
+ public static String getOctalPermissions(Collection<PosixFilePermission> perms) {
+ int pf = 0;
+
+ for (PosixFilePermission p : perms) {
+ switch (p) {
+ case OWNER_READ:
+ pf |= SftpConstants.S_IRUSR;
+ break;
+ case OWNER_WRITE:
+ pf |= SftpConstants.S_IWUSR;
+ break;
+ case OWNER_EXECUTE:
+ pf |= SftpConstants.S_IXUSR;
+ break;
+ case GROUP_READ:
+ pf |= SftpConstants.S_IRGRP;
+ break;
+ case GROUP_WRITE:
+ pf |= SftpConstants.S_IWGRP;
+ break;
+ case GROUP_EXECUTE:
+ pf |= SftpConstants.S_IXGRP;
+ break;
+ case OTHERS_READ:
+ pf |= SftpConstants.S_IROTH;
+ break;
+ case OTHERS_WRITE:
+ pf |= SftpConstants.S_IWOTH;
+ break;
+ case OTHERS_EXECUTE:
+ pf |= SftpConstants.S_IXOTH;
+ break;
+ default: // ignored
+ }
+ }
+
+ return String.format("%04o", pf);
+ }
+
+ /**
+ * Uses the host, port and username to create a unique identifier
+ *
+ * @param uri The {@link URI} - <B>Note:</B> not checked to make sure
+ * that the scheme is {@code sftp://}
+ * @return The unique identifier
+ * @see #getFileSystemIdentifier(String, int, String)
+ */
+ public static String getFileSystemIdentifier(URI uri) {
+ String userInfo = ValidateUtils.checkNotNullAndNotEmpty(uri.getUserInfo(), "UserInfo not provided");
+ String[] ui = GenericUtils.split(userInfo, ':');
+ ValidateUtils.checkTrue(GenericUtils.length(ui) == 2, "Invalid user info: %s", userInfo);
+ return getFileSystemIdentifier(uri.getHost(), uri.getPort(), ui[0]);
+ }
+
+ /**
+ * Uses the remote host address, port and current username to create a unique identifier
+ *
+ * @param session The {@link ClientSession}
+ * @return The unique identifier
+ * @see #getFileSystemIdentifier(String, int, String)
+ */
+ public static String getFileSystemIdentifier(ClientSession session) {
+ IoSession ioSession = session.getIoSession();
+ SocketAddress addr = ioSession.getRemoteAddress();
+ String username = session.getUsername();
+ if (addr instanceof InetSocketAddress) {
+ InetSocketAddress inetAddr = (InetSocketAddress) addr;
+ return getFileSystemIdentifier(inetAddr.getHostString(), inetAddr.getPort(), username);
+ } else {
+ return getFileSystemIdentifier(addr.toString(), SshConstants.DEFAULT_PORT, username);
+ }
+ }
+
+ public static String getFileSystemIdentifier(String host, int port, String username) {
+ return GenericUtils.trimToEmpty(host) + ':'
+ + ((port <= 0) ? SshConstants.DEFAULT_PORT : port) + ':'
+ + GenericUtils.trimToEmpty(username);
+ }
+
+ public static URI createFileSystemURI(String host, int port, String username, String password) {
+ return createFileSystemURI(host, port, username, password, Collections.emptyMap());
+ }
+
+ public static URI createFileSystemURI(String host, int port, String username, String password, Map<String, ?> params) {
+ ValidateUtils.checkNotNullAndNotEmpty(host, "No host provided");
+
+ String queryPart = null;
+ int numParams = GenericUtils.size(params);
+ if (numParams > 0) {
+ StringBuilder sb = new StringBuilder(numParams * Short.SIZE);
+ for (Map.Entry<String, ?> pe : params.entrySet()) {
+ String key = pe.getKey();
+ Object value = pe.getValue();
+ if (sb.length() > 0) {
+ sb.append('&');
+ }
+ sb.append(key);
+ if (value != null) {
+ sb.append('=').append(Objects.toString(value, null));
+ }
+ }
+
+ queryPart = sb.toString();
+ }
+
+ try {
+ String userAuth = encodeCredentials(username, password);
+ return new URI(SftpConstants.SFTP_SUBSYSTEM_NAME, userAuth, host, port, "/", queryPart, null);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Failed (" + e.getClass().getSimpleName() + ")"
+ + " to create access URI: " + e.getMessage(), e);
+ }
+ }
+
+ public static String encodeCredentials(String username, String password) {
+ ValidateUtils.checkNotNullAndNotEmpty(username, "No username provided");
+ ValidateUtils.checkNotNullAndNotEmpty(password, "No password provided");
+ /*
+ * There is no way to properly encode/decode credentials that already contain
+ * colon. See also https://tools.ietf.org/html/rfc3986#section-3.2.1:
+ *
+ *
+ * Use of the format "user:password" in the userinfo field is
+ * deprecated. Applications should not render as clear text any data
+ * after the first colon (":") character found within a userinfo
+ * subcomponent unless the data after the colon is the empty string
+ * (indicating no password). Applications may choose to ignore or
+ * reject such data when it is received as part of a reference and
+ * should reject the storage of such data in unencrypted form.
+ */
+ ValidateUtils.checkTrue((username.indexOf(':') < 0) && (password.indexOf(':') < 0), "Reserved character used in credentials");
+ return username + ":" + password;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e3b8acd7/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPath.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPath.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPath.java
new file mode 100644
index 0000000..381e272
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPath.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.subsystem.sftp.fs;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.LinkOption;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.List;
+
+import org.apache.sshd.common.file.util.BasePath;
+
+public class SftpPath extends BasePath<SftpPath, SftpFileSystem> {
+ public SftpPath(SftpFileSystem fileSystem, String root, List<String> names) {
+ super(fileSystem, root, names);
+ }
+
+ @Override
+ public SftpPath toRealPath(LinkOption... options) throws IOException {
+ // TODO: handle links
+ SftpPath absolute = toAbsolutePath();
+ FileSystem fs = getFileSystem();
+ FileSystemProvider provider = fs.provider();
+ provider.checkAccess(absolute);
+ return absolute;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e3b8acd7/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPathIterator.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPathIterator.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPathIterator.java
new file mode 100644
index 0000000..8c4f89e
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPathIterator.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp.fs;
+
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpPathIterator implements Iterator<Path> {
+ private final SftpPath p;
+ private final Iterator<? extends SftpClient.DirEntry> it;
+ private boolean dotIgnored;
+ private boolean dotdotIgnored;
+ private SftpClient.DirEntry curEntry;
+
+ public SftpPathIterator(SftpPath path, Iterable<? extends SftpClient.DirEntry> iter) {
+ this(path, (iter == null) ? null : iter.iterator());
+ }
+
+ public SftpPathIterator(SftpPath path, Iterator<? extends SftpClient.DirEntry> iter) {
+ p = path;
+ it = iter;
+ curEntry = nextEntry();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return curEntry != null;
+ }
+
+ @Override
+ public Path next() {
+ if (curEntry == null) {
+ throw new NoSuchElementException("No next entry");
+ }
+
+ SftpClient.DirEntry entry = curEntry;
+ curEntry = nextEntry();
+ return p.resolve(entry.getFilename());
+ }
+
+ private SftpClient.DirEntry nextEntry() {
+ while ((it != null) && it.hasNext()) {
+ SftpClient.DirEntry entry = it.next();
+ String name = entry.getFilename();
+ if (".".equals(name) && (!dotIgnored)) {
+ dotIgnored = true;
+ } else if ("..".equals(name) && (!dotdotIgnored)) {
+ dotdotIgnored = true;
+ } else {
+ return entry;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("newDirectoryStream(" + p + ") Iterator#remove() N/A");
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e3b8acd7/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributeView.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributeView.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributeView.java
new file mode 100644
index 0000000..276c4aa
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributeView.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.subsystem.sftp.fs;
+
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+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.util.Set;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.impl.AbstractSftpFileAttributeView;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpPosixFileAttributeView extends AbstractSftpFileAttributeView implements PosixFileAttributeView {
+ public SftpPosixFileAttributeView(SftpFileSystemProvider provider, Path path, LinkOption... options) {
+ super(provider, path, options);
+ }
+
+ @Override
+ public String name() {
+ return "posix";
+ }
+
+ @Override
+ public PosixFileAttributes readAttributes() throws IOException {
+ return new SftpPosixFileAttributes(path, readRemoteAttributes());
+ }
+
+ @Override
+ public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException {
+ SftpClient.Attributes attrs = new SftpClient.Attributes();
+ if (lastModifiedTime != null) {
+ attrs.modifyTime(lastModifiedTime);
+ }
+ if (lastAccessTime != null) {
+ attrs.accessTime(lastAccessTime);
+ }
+ if (createTime != null) {
+ attrs.createTime(createTime);
+ }
+
+ if (GenericUtils.isEmpty(attrs.getFlags())) {
+ if (log.isDebugEnabled()) {
+ log.debug("setTimes({}) no changes", path);
+ }
+ } else {
+ writeRemoteAttributes(attrs);
+ }
+ }
+
+ @Override
+ public void setPermissions(Set<PosixFilePermission> perms) throws IOException {
+ provider.setAttribute(path, "permissions", perms, options);
+ }
+
+ @Override
+ public void setGroup(GroupPrincipal group) throws IOException {
+ provider.setAttribute(path, "group", group, options);
+ }
+
+ @Override
+ public UserPrincipal getOwner() throws IOException {
+ return readAttributes().owner();
+ }
+
+ @Override
+ public void setOwner(UserPrincipal owner) throws IOException {
+ provider.setAttribute(path, "owner", owner, options);
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e3b8acd7/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributes.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributes.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributes.java
new file mode 100644
index 0000000..0c6c5fe
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/fs/SftpPosixFileAttributes.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.subsystem.sftp.fs;
+
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Set;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpPosixFileAttributes implements PosixFileAttributes {
+ private final Path path;
+ private final Attributes attributes;
+
+ public SftpPosixFileAttributes(Path path, Attributes attributes) {
+ this.path = path;
+ this.attributes = attributes;
+ }
+
+ /**
+ * @return The referenced attributes file {@link Path}
+ */
+ public final Path getPath() {
+ return path;
+ }
+
+ @Override
+ public UserPrincipal owner() {
+ String owner = attributes.getOwner();
+ return GenericUtils.isEmpty(owner) ? null : new SftpFileSystem.DefaultUserPrincipal(owner);
+ }
+
+ @Override
+ public GroupPrincipal group() {
+ String group = attributes.getGroup();
+ return GenericUtils.isEmpty(group) ? null : new SftpFileSystem.DefaultGroupPrincipal(group);
+ }
+
+ @Override
+ public Set<PosixFilePermission> permissions() {
+ return SftpFileSystemProvider.permissionsToAttributes(attributes.getPermissions());
+ }
+
+ @Override
+ public FileTime lastModifiedTime() {
+ return attributes.getModifyTime();
+ }
+
+ @Override
+ public FileTime lastAccessTime() {
+ return attributes.getAccessTime();
+ }
+
+ @Override
+ public FileTime creationTime() {
+ return attributes.getCreateTime();
+ }
+
+ @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.getSize();
+ }
+
+ @Override
+ public Object fileKey() {
+ // TODO consider implementing this
+ return null;
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e3b8acd7/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java
index 0fce423..8892e72 100644
--- a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/AbstractSftpFileAttributeView.java
@@ -27,9 +27,9 @@ import java.nio.file.attribute.FileAttributeView;
import java.util.Objects;
import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpFileSystem;
-import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
-import org.apache.sshd.client.subsystem.sftp.SftpPath;
+import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystem;
+import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystemProvider;
+import org.apache.sshd.client.subsystem.sftp.fs.SftpPath;
import org.apache.sshd.common.subsystem.sftp.SftpConstants;
import org.apache.sshd.common.subsystem.sftp.SftpException;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e3b8acd7/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java
index c6702f8..ec65752 100644
--- a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/DefaultSftpClientFactory.java
@@ -25,9 +25,9 @@ import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.subsystem.sftp.SftpClient;
import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
-import org.apache.sshd.client.subsystem.sftp.SftpFileSystem;
-import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
+import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystem;
+import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystemProvider;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
/**