All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sshd.sftp.client.fs.SftpFileSystemProvider Maven / Gradle / Ivy

There is a newer version: 2.14.0
Show newest version
/*
 * 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.sftp.client.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.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.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.logging.LoggingUtils;
import org.apache.sshd.sftp.SftpModuleProperties;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClient.Attributes;
import org.apache.sshd.sftp.client.SftpClient.OpenMode;
import org.apache.sshd.sftp.client.SftpClientFactory;
import org.apache.sshd.sftp.client.SftpErrorDataHandler;
import org.apache.sshd.sftp.client.SftpVersionSelector;
import org.apache.sshd.sftp.client.extensions.CopyFileExtension;
import org.apache.sshd.sftp.client.impl.SftpPathImpl;
import org.apache.sshd.sftp.client.impl.SftpRemotePathChannel;
import org.apache.sshd.sftp.common.SftpConstants;
import org.apache.sshd.sftp.common.SftpException;
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 Apache MINA SSHD Project
 */
public class SftpFileSystemProvider extends FileSystemProvider {

    /**
     * 

* URI parameter that can be used to specify a special version selection. Options are: *

*
    *
  • {@code max} - select maximum available version for the client
  • *
  • {@code min} - select minimum available version for the client
  • *
  • {@code current} - whatever version is reported by the server
  • *
  • {@code nnn} - select only the specified version
  • *
  • {@code a,b,c} - select one of the specified versions (if available) in preference order
  • *
*/ public static final String VERSION_PARAM = "version"; public static final Set> 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 SftpErrorDataHandler errorDataHandler; private final NavigableMap fileSystems = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); private SftpFileSystemClientSessionInitializer fsSessionInitializer = SftpFileSystemClientSessionInitializer.DEFAULT; public SftpFileSystemProvider() { this((SshClient) null); } public SftpFileSystemProvider(SftpVersionSelector selector) { this(null, selector); } public SftpFileSystemProvider(SshClient client) { this(client, SftpVersionSelector.CURRENT); } public SftpFileSystemProvider(SshClient client, SftpVersionSelector selector) { this(client, selector, SftpErrorDataHandler.EMPTY); } public SftpFileSystemProvider(SshClient client, SftpVersionSelector selector, SftpErrorDataHandler errorDataHandler) { this(client, null, selector, errorDataHandler); } public SftpFileSystemProvider(SshClient client, SftpClientFactory factory, SftpVersionSelector selector) { this(client, factory, selector, SftpErrorDataHandler.EMPTY); } /** * @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 * @param factory The {@link SftpClientFactory} to use to generate SFTP client instances * @param selector The {@link SftpVersionSelector} to use in order to negotiate the SFTP version * @param errorDataHandler The {@link SftpErrorDataHandler} to handle incoming data through the error stream - if * {@code null} the data is silently ignored * @see SshClient#setUpDefaultClient() */ public SftpFileSystemProvider(SshClient client, SftpClientFactory factory, SftpVersionSelector selector, SftpErrorDataHandler errorDataHandler) { this.log = LoggerFactory.getLogger(getClass()); this.factory = factory; this.versionSelector = selector; this.errorDataHandler = errorDataHandler; 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 SftpErrorDataHandler getSftpErrorDataHandler() { return errorDataHandler; } 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 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 params = resolveFileSystemParameters(env, parseURIParameters(uri)); PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(params); context.setPropertyResolver(resolver); context.setMaxConnectTime(SftpModuleProperties.CONNECT_TIME.getRequired(resolver)); context.setMaxAuthTime(SftpModuleProperties.AUTH_TIME.getRequired(resolver)); SftpVersionSelector selector = resolveSftpVersionSelector(uri, getSftpVersionSelector(), resolver); SftpErrorDataHandler errorHandler = resolveSftpErrorDataHandler(uri, getSftpErrorDataHandler(), resolver); Charset decodingCharset = SftpModuleProperties.NAME_DECODER_CHARSET.getRequired(resolver); 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 (MapEntryUtils.size(params) > 0) { // Cannot use forEach because the session is not effectively final for (Map.Entry pe : params.entrySet()) { String key = pe.getKey(); Object value = pe.getValue(); if (VERSION_PARAM.equalsIgnoreCase(key)) { continue; } PropertyResolverUtils.updateProperty(session, key, value); } SftpModuleProperties.NAME_DECODING_CHARSET.set(session, decodingCharset); } initializer.authenticateClientSession(this, context, session); fileSystem = initializer.createSftpFileSystem( this, context, session, selector, errorHandler); fileSystems.put(id, fileSystem); } catch (Exception e) { if (session != null) { try { session.close(); } catch (IOException t) { e.addSuppressed(t); LoggingUtils.debug(log, "Failed ({}) to close session for new file system on {}}:{} due to {}[{}]: {}", t.getClass().getSimpleName(), host, port, e.getClass().getSimpleName(), e.getMessage(), t.getMessage(), t); } } if (e instanceof IOException) { throw (IOException) e; } else if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new IOException(e); } } } Integer bs = SftpModuleProperties.READ_BUFFER_SIZE.getOrNull(resolver); if (bs != null) { fileSystem.setReadBufferSize(bs); } bs = SftpModuleProperties.WRITE_BUFFER_SIZE.getOrNull(resolver); if (bs != null) { fileSystem.setWriteBufferSize(bs); } 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); } // These are aliases for shorter parameters specification if ("max".equalsIgnoreCase(preference)) { return SftpVersionSelector.MAXIMUM; } else if ("min".equalsIgnoreCase(preference)) { return SftpVersionSelector.MINIMUM; } else { return SftpVersionSelector.resolveVersionSelector(preference); } } protected SftpErrorDataHandler resolveSftpErrorDataHandler( URI uri, SftpErrorDataHandler errorHandler, PropertyResolver resolver) { return errorHandler; } // NOTE: URI parameters override environment ones public static Map resolveFileSystemParameters(Map env, Map uriParams) { if (MapEntryUtils.isEmpty(env)) { return MapEntryUtils.isEmpty(uriParams) ? Collections.emptyMap() : uriParams; } else if (MapEntryUtils.isEmpty(uriParams)) { return Collections.unmodifiableMap(env); } Map 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; } int pos = userInfo.indexOf(':'); if (pos < 0) { return new BasicCredentialsImpl(userInfo, null); // assume password-less login } return new BasicCredentialsImpl(userInfo.substring(0, pos), userInfo.substring(pos + 1)); } public static Map parseURIParameters(URI uri) { return parseURIParameters((uri == null) ? "" : uri.getQuery()); } public static Map 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 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(), getSftpErrorDataHandler()); fileSystems.put(id, fileSystem); } Integer rbs = session.getInteger(SftpModuleProperties.READ_BUFFER_SIZE.getName()); if (rbs != null) { fileSystem.setReadBufferSize(rbs); } Integer wbs = session.getInteger(SftpModuleProperties.WRITE_BUFFER_SIZE.getName()); if (wbs != null) { fileSystem.setWriteBufferSize(wbs); } 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 options, FileAttribute... attrs) throws IOException { return newFileChannel(path, options, attrs); } @Override public FileChannel newFileChannel(Path path, Set options, FileAttribute... attrs) throws IOException { Collection modes = OpenMode.fromOpenOptions(options); if (modes.isEmpty()) { modes = EnumSet.of(OpenMode.Read, OpenMode.Write); } // TODO: process file attributes SftpPath p = toSftpPath(path); return new SftpRemotePathChannel(p.toString(), p.getFileSystem().getClient(), true, modes); } @Override public InputStream newInputStream(Path path, OpenOption... options) throws IOException { Collection modes = OpenMode.fromOpenOptions(Arrays.asList(options)); if (modes.isEmpty()) { modes = EnumSet.of(OpenMode.Read); } SftpPath p = toSftpPath(path); return p.getFileSystem().getClient().read(p.toString(), modes); } @Override public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException { Set modes = OpenMode.fromOpenOptions(Arrays.asList(options)); if (modes.contains(OpenMode.Read)) { throw new IllegalArgumentException("READ not allowed"); } if (modes.isEmpty()) { modes = EnumSet.of(OpenMode.Create, OpenMode.Truncate, OpenMode.Write); } else { modes.add(OpenMode.Write); } SftpPath p = toSftpPath(path); return p.getFileSystem().getClient().write(p.toString(), modes); } @Override public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { final SftpPath p = toSftpPath(dir); return new SftpDirectoryStream(p, filter); } @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 { CopyFileExtension copyFile = src.getFileSystem().getClient().getExtension(CopyFileExtension.class); if (copyFile.isSupported()) { copyFile.copyFile(source.toString(), target.toString(), false); } 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 getFileAttributeView(Path path, Class 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 type) { return isSupportedFileAttributeView(toSftpPath(path).getFileSystem(), type); } public boolean isSupportedFileAttributeView(SftpFileSystem fs, Class type) { Collection 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 readAttributes(Path path, Class 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 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 readAttributes(Path path, String view, String attrs, LinkOption... options) throws IOException { SftpPath p = toSftpPath(path); SftpFileSystem fs = p.getFileSystem(); Collection 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 readCustomViewAttributes(SftpPath path, String view, String attrs, LinkOption... options) throws IOException { throw new UnsupportedOperationException( "readCustomViewAttributes(" + path + ")[" + view + ":" + attrs + "] view not supported"); } protected NavigableMap 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 map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); String[] attrValues = GenericUtils.split(attrs, ','); boolean traceEnabled = log.isTraceEnabled(); for (String attr : attrValues) { switch (attr) { case IoUtils.ACL_VIEW_ATTR: { List acl = attributes.getAcl(); if (acl != null) { map.put(attr, acl); } break; } case IoUtils.OWNER_VIEW_ATTR: { 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 { // Use the cache here. The cache scope clears cached attributes at the beginning and at the end of the outermost // SftpPathImpl.withAttributeCache() invocation. So we ensure here that if we are already within a caching // scope, we do use the cached attributes, but if we are not, we clear any possibly cached attributes and // do actually read them from the remote. return SftpPathImpl.withAttributeCache(path, p -> resolveRemoteFileAttributes(path, options)); } protected SftpClient.Attributes resolveRemoteFileAttributes(SftpPath path, LinkOption... options) throws IOException { SftpClient.Attributes attributes = path.getAttributes(); if (attributes != null) { return attributes; } SftpFileSystem fs = path.getFileSystem(); try (SftpClient client = fs.getClient()) { SftpClient.Attributes attrs; if (IoUtils.followLinks(options)) { attrs = client.stat(path.toString()); } else { attrs = client.lstat(path.toString()); } if (log.isTraceEnabled()) { log.trace("resolveRemoteFileAttributes({})[{}]: {}", fs, path, attrs); } if (path instanceof SftpPathImpl) { ((SftpPathImpl) path).cacheAttributes(attrs); } return attrs; } catch (SftpException e) { if (e.getStatus() == SftpConstants.SSH_FX_NO_SUCH_FILE) { NoSuchFileException toThrow = new NoSuchFileException(path.toString()); toThrow.initCause(e); throw toThrow; } throw e; } } protected NavigableMap readPosixViewAttributes( SftpPath path, String view, String attrs, LinkOption... options) throws IOException { PosixFileAttributes v = readAttributes(path, PosixFileAttributes.class, options); if ("*".equals(attrs)) { attrs = IoUtils.LASTMOD_TIME_VIEW_ATTR + "," + IoUtils.LASTACC_TIME_VIEW_ATTR + "," + IoUtils.CREATE_TIME_VIEW_ATTR + "," + IoUtils.SIZE_VIEW_ATTR + "," + IoUtils.REGFILE_VIEW_ATTR + "," + IoUtils.DIRECTORY_VIEW_ATTR + "," + IoUtils.SYMLINK_VIEW_ATTR + "," + IoUtils.OTHERFILE_VIEW_ATTR + "," + IoUtils.FILEKEY_VIEW_ATTR + "," + IoUtils.OWNER_VIEW_ATTR + "," + IoUtils.GROUP_VIEW_ATTR + "," + IoUtils.PERMISSIONS_VIEW_ATTR + "," + IoUtils.FILEKEY_VIEW_ATTR; } NavigableMap map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); boolean traceEnabled = log.isTraceEnabled(); String[] attrValues = GenericUtils.split(attrs, ','); for (String attr : attrValues) { switch (attr) { case IoUtils.LASTMOD_TIME_VIEW_ATTR: map.put(attr, v.lastModifiedTime()); break; case IoUtils.LASTACC_TIME_VIEW_ATTR: map.put(attr, v.lastAccessTime()); break; case IoUtils.CREATE_TIME_VIEW_ATTR: map.put(attr, v.creationTime()); break; case IoUtils.SIZE_VIEW_ATTR: map.put(attr, v.size()); break; case IoUtils.REGFILE_VIEW_ATTR: map.put(attr, v.isRegularFile()); break; case IoUtils.DIRECTORY_VIEW_ATTR: map.put(attr, v.isDirectory()); break; case IoUtils.SYMLINK_VIEW_ATTR: map.put(attr, v.isSymbolicLink()); break; case IoUtils.OTHERFILE_VIEW_ATTR: map.put(attr, v.isOther()); break; case IoUtils.FILEKEY_VIEW_ATTR: map.put(attr, v.fileKey()); break; case IoUtils.OWNER_VIEW_ATTR: map.put(attr, v.owner()); break; case IoUtils.PERMISSIONS_VIEW_ATTR: map.put(attr, v.permissions()); break; case IoUtils.GROUP_VIEW_ATTR: 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 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 IoUtils.LASTMOD_TIME_VIEW_ATTR: attributes.modifyTime((int) ((FileTime) value).to(TimeUnit.SECONDS)); break; case IoUtils.LASTACC_TIME_VIEW_ATTR: attributes.accessTime((int) ((FileTime) value).to(TimeUnit.SECONDS)); break; case IoUtils.CREATE_TIME_VIEW_ATTR: attributes.createTime((int) ((FileTime) value).to(TimeUnit.SECONDS)); break; case IoUtils.SIZE_VIEW_ATTR: attributes.size(((Number) value).longValue()); break; case IoUtils.PERMISSIONS_VIEW_ATTR: { @SuppressWarnings("unchecked") Set attrSet = (Set) value; attributes.perms(attributesToPermissions(path, attrSet)); break; } case IoUtils.OWNER_VIEW_ATTR: attributes.owner(((UserPrincipal) value).getName()); break; case IoUtils.GROUP_VIEW_ATTR: attributes.group(((GroupPrincipal) value).getName()); break; case IoUtils.ACL_VIEW_ATTR: { ValidateUtils.checkTrue("acl".equalsIgnoreCase(view), "ACL cannot be set via view=%s", view); @SuppressWarnings("unchecked") List acl = (List) value; attributes.acl(acl); break; } case IoUtils.REGFILE_VIEW_ATTR: case IoUtils.DIRECTORY_VIEW_ATTR: case IoUtils.SYMLINK_VIEW_ATTR: case IoUtils.OTHERFILE_VIEW_ATTR: case IoUtils.FILEKEY_VIEW_ATTR: 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 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 attrs = permissionsToAttributes(perms); return getOctalPermissions(attrs); } public static Set permissionsToAttributes(int perms) { Set 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 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} - Note: 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) + ':' + SshConstants.TO_EFFECTIVE_PORT.applyAsInt(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 params) { ValidateUtils.checkNotNullAndNotEmpty(host, "No host provided"); String queryPart = null; int numParams = MapEntryUtils.size(params); if (numParams > 0) { StringBuilder sb = new StringBuilder(numParams * Short.SIZE); for (Map.Entry 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"); /* * 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 == null) || (password.indexOf(':') < 0)), "Reserved character used in credentials"); if (password == null) { return username; // assume password-less login required } else { return username + ":" + password; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy