/* * SFTPFileSystem.java * Copyright 2016 Rob Spoor * * Licensed 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 com.github.robtimus.filesystems.sftp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.nio.channels.SeekableByteChannel; import java.nio.file.AccessDeniedException; import java.nio.file.AccessMode; import java.nio.file.CopyOption; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileStore; import java.nio.file.FileSystem; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.StandardOpenOption; import java.nio.file.WatchService; import java.nio.file.attribute.FileAttribute; 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.nio.file.attribute.UserPrincipalLookupService; import java.nio.file.spi.FileSystemProvider; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import com.github.robtimus.filesystems.AbstractDirectoryStream; import com.github.robtimus.filesystems.FileSystemProviderSupport; import com.github.robtimus.filesystems.LinkOptionSupport; import com.github.robtimus.filesystems.Messages; import com.github.robtimus.filesystems.PathMatcherSupport; import com.github.robtimus.filesystems.URISupport; import com.github.robtimus.filesystems.attribute.PosixFilePermissionSupport; import com.github.robtimus.filesystems.attribute.SimpleGroupPrincipal; import com.github.robtimus.filesystems.attribute.SimpleUserPrincipal; import com.github.robtimus.filesystems.sftp.SSHChannelPool.Channel; import com.jcraft.jsch.ChannelSftp.LsEntry; import com.jcraft.jsch.SftpATTRS; import com.jcraft.jsch.SftpStatVFS; /** * An SFTP file system. * * @author Rob Spoor */ class SFTPFileSystem extends FileSystem { private static final String CURRENT_DIR = "."; //$NON-NLS-1$ private static final String PARENT_DIR = ".."; //$NON-NLS-1$ @SuppressWarnings("nls") private static final Set<String> SUPPORTED_FILE_ATTRIBUTE_VIEWS = Collections .unmodifiableSet(new HashSet<>(Arrays.asList("basic", "owner", "posix"))); private final SFTPFileSystemProvider provider; private final Iterable<Path> rootDirectories; private final Iterable<FileStore> fileStores; private final SSHChannelPool channelPool; private final URI uri; private final String defaultDirectory; private final AtomicBoolean open = new AtomicBoolean(true); SFTPFileSystem(SFTPFileSystemProvider provider, URI uri, SFTPEnvironment env) throws IOException { this.provider = Objects.requireNonNull(provider); SFTPPath rootPath = new SFTPPath(this, "/"); //$NON-NLS-1$ this.rootDirectories = Collections.<Path>singleton(rootPath); this.fileStores = Collections.<FileStore>singleton(new SFTPFileStore(rootPath)); this.channelPool = new SSHChannelPool(uri.getHost(), uri.getPort(), env); this.uri = Objects.requireNonNull(uri); try (Channel channel = channelPool.get()) { this.defaultDirectory = channel.pwd(); } } @Override public FileSystemProvider provider() { return provider; } @Override public void close() throws IOException { if (open.getAndSet(false)) { provider.removeFileSystem(uri); channelPool.close(); } } @Override public boolean isOpen() { return open.get(); } @Override public boolean isReadOnly() { return false; } @Override public String getSeparator() { return "/"; //$NON-NLS-1$ } @Override public Iterable<Path> getRootDirectories() { return rootDirectories; } @Override public Iterable<FileStore> getFileStores() { // TODO: get the actual file stores, instead of only returning the root file store return fileStores; } @Override public Set<String> supportedFileAttributeViews() { return SUPPORTED_FILE_ATTRIBUTE_VIEWS; } @Override public Path getPath(String first, String... more) { StringBuilder sb = new StringBuilder(first); for (String s : more) { sb.append("/").append(s); //$NON-NLS-1$ } return new SFTPPath(this, sb.toString()); } @Override public PathMatcher getPathMatcher(String syntaxAndPattern) { final Pattern pattern = PathMatcherSupport.toPattern(syntaxAndPattern); return path -> pattern.matcher(path.toString()).matches(); } @Override public UserPrincipalLookupService getUserPrincipalLookupService() { throw Messages.unsupportedOperation(FileSystem.class, "getUserPrincipalLookupService"); //$NON-NLS-1$ } @Override public WatchService newWatchService() throws IOException { throw Messages.unsupportedOperation(FileSystem.class, "newWatchService"); //$NON-NLS-1$ } void keepAlive() throws IOException { channelPool.keepAlive(); } URI toUri(SFTPPath path) { SFTPPath absPath = toAbsolutePath(path).normalize(); return toUri(absPath.path()); } URI toUri(String path) { return URISupport.create(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), path, null, null); } SFTPPath toAbsolutePath(SFTPPath path) { if (path.isAbsolute()) { return path; } return new SFTPPath(this, defaultDirectory + "/" + path.path()); //$NON-NLS-1$ } SFTPPath toRealPath(SFTPPath path, LinkOption... options) throws IOException { boolean followLinks = LinkOptionSupport.followLinks(options); try (Channel channel = channelPool.get()) { return toRealPath(channel, path, followLinks).path; } } private SFTPPathAndAttributesPair toRealPath(Channel channel, SFTPPath path, boolean followLinks) throws IOException { SFTPPath absPath = toAbsolutePath(path).normalize(); SftpATTRS attributes = getAttributes(channel, absPath, false); if (followLinks && attributes.isLink()) { SFTPPath link = readSymbolicLink(channel, absPath); return toRealPath(channel, link, followLinks); } return new SFTPPathAndAttributesPair(absPath, attributes); } private static final class SFTPPathAndAttributesPair { private final SFTPPath path; private final SftpATTRS attributes; private SFTPPathAndAttributesPair(SFTPPath path, SftpATTRS attributes) { this.path = path; this.attributes = attributes; } } String toString(SFTPPath path) { return path.path(); } InputStream newInputStream(SFTPPath path, OpenOption... options) throws IOException { OpenOptions openOptions = OpenOptions.forNewInputStream(options); try (Channel channel = channelPool.get()) { return newInputStream(channel, path, openOptions); } } private InputStream newInputStream(Channel channel, SFTPPath path, OpenOptions options) throws IOException { assert options.read; return channel.newInputStream(path.path(), options); } OutputStream newOutputStream(SFTPPath path, OpenOption... options) throws IOException { OpenOptions openOptions = OpenOptions.forNewOutputStream(options); try (Channel channel = channelPool.get()) { return newOutputStream(channel, path, false, openOptions).out; } } @SuppressWarnings("resource") private SFTPAttributesAndOutputStreamPair newOutputStream(Channel channel, SFTPPath path, boolean requireAttributes, OpenOptions options) throws IOException { // retrieve the attributes unless create is true and createNew is false, because then the file can be created SftpATTRS attributes = null; if (!options.create || options.createNew) { attributes = findAttributes(channel, path, false); if (attributes != null && attributes.isDir()) { throw Messages.fileSystemProvider().isDirectory(path.path()); } if (!options.createNew && attributes == null) { throw new NoSuchFileException(path.path()); } else if (options.createNew && attributes != null) { throw new FileAlreadyExistsException(path.path()); } } // else the file can be created if necessary if (attributes == null && requireAttributes) { attributes = findAttributes(channel, path, false); } OutputStream out = channel.newOutputStream(path.path(), options); return new SFTPAttributesAndOutputStreamPair(attributes, out); } private static final class SFTPAttributesAndOutputStreamPair { private final SftpATTRS attributes; private final OutputStream out; private SFTPAttributesAndOutputStreamPair(SftpATTRS attributes, OutputStream out) { this.attributes = attributes; this.out = out; } } @SuppressWarnings("resource") SeekableByteChannel newByteChannel(SFTPPath path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { if (attrs.length > 0) { throw Messages.fileSystemProvider().unsupportedCreateFileAttribute(attrs[0].name()); } OpenOptions openOptions = OpenOptions.forNewByteChannel(options); try (Channel channel = channelPool.get()) { if (openOptions.read) { // use findAttributes instead of getAttributes, to let the opening of the stream provide the correct error message SftpATTRS attributes = findAttributes(channel, path, false); InputStream in = newInputStream(channel, path, openOptions); long size = attributes == null ? 0 : attributes.getSize(); return FileSystemProviderSupport.createSeekableByteChannel(in, size); } // if append then we need the attributes, to find the initial position of the channel boolean requireAttributes = openOptions.append; SFTPAttributesAndOutputStreamPair outPair = newOutputStream(channel, path, requireAttributes, openOptions); long initialPosition = outPair.attributes == null ? 0 : outPair.attributes.getSize(); return FileSystemProviderSupport.createSeekableByteChannel(outPair.out, initialPosition); } } DirectoryStream<Path> newDirectoryStream(final SFTPPath path, Filter<? super Path> filter) throws IOException { try (Channel channel = channelPool.get()) { List<LsEntry> entries = channel.listFiles(path.path()); boolean isDirectory = false; for (Iterator<LsEntry> i = entries.iterator(); i.hasNext(); ) { LsEntry entry = i.next(); String filename = entry.getFilename(); if (CURRENT_DIR.equals(filename)) { isDirectory = true; i.remove(); } else if (PARENT_DIR.equals(filename)) { i.remove(); } } if (!isDirectory) { // https://github.com/robtimus/sftp-fs/issues/4: don't fail immediately but check the attributes // Follow links to ensure the directory attribute can be read correctly SftpATTRS attributes = channel.readAttributes(path.path(), true); if (!attributes.isDir()) { throw new NotDirectoryException(path.path()); } } return new SFTPPathDirectoryStream(path, entries, filter); } } private static final class SFTPPathDirectoryStream extends AbstractDirectoryStream<Path> { private final SFTPPath path; private final List<LsEntry> entries; private Iterator<LsEntry> iterator; private SFTPPathDirectoryStream(SFTPPath path, List<LsEntry> entries, Filter<? super Path> filter) { super(filter); this.path = path; this.entries = entries; } @Override protected void setupIteration() { iterator = entries.iterator(); } @Override protected Path getNext() throws IOException { return iterator.hasNext() ? path.resolve(iterator.next().getFilename()) : null; } } void createDirectory(SFTPPath path, FileAttribute<?>... attrs) throws IOException { if (attrs.length > 0) { throw Messages.fileSystemProvider().unsupportedCreateFileAttribute(attrs[0].name()); } try (Channel channel = channelPool.get()) { channel.mkdir(path.path()); } } void delete(SFTPPath path) throws IOException { try (Channel channel = channelPool.get()) { SftpATTRS attributes = getAttributes(channel, path, false); boolean isDirectory = attributes.isDir(); channel.delete(path.path(), isDirectory); } } SFTPPath readSymbolicLink(SFTPPath path) throws IOException { try (Channel channel = channelPool.get()) { return readSymbolicLink(channel, path); } } private SFTPPath readSymbolicLink(Channel channel, SFTPPath path) throws IOException { String link = channel.readSymbolicLink(path.path()); return path.resolveSibling(link); } void copy(SFTPPath source, SFTPPath target, CopyOption... options) throws IOException { boolean sameFileSystem = haveSameFileSystem(source, target); CopyOptions copyOptions = CopyOptions.forCopy(options); try (Channel channel = channelPool.get()) { // get the attributes to determine whether a directory needs to be created or a file needs to be copied // Files.copy specifies that for links, the final target must be copied SFTPPathAndAttributesPair sourcePair = toRealPath(channel, source, true); if (!sameFileSystem) { copyAcrossFileSystems(channel, source, sourcePair.attributes, target, copyOptions); return; } try { if (sourcePair.path.path().equals(toRealPath(channel, target, true).path.path())) { // non-op, don't do a thing as specified by Files.copy return; } } catch (@SuppressWarnings("unused") NoSuchFileException e) { // the target does not exist or either path is an invalid link, ignore the error and continue } SftpATTRS targetAttributes = findAttributes(channel, target, false); if (targetAttributes != null) { if (copyOptions.replaceExisting) { channel.delete(target.path(), targetAttributes.isDir()); } else { throw new FileAlreadyExistsException(target.path()); } } if (sourcePair.attributes.isDir()) { channel.mkdir(target.path()); } else { try (Channel channel2 = channelPool.getOrCreate()) { copyFile(channel, source, channel2, target, copyOptions); } } } } private void copyAcrossFileSystems(Channel sourceChannel, SFTPPath source, SftpATTRS sourceAttributes, SFTPPath target, CopyOptions options) throws IOException { @SuppressWarnings("resource") SFTPFileSystem targetFileSystem = target.getFileSystem(); try (Channel targetChannel = targetFileSystem.channelPool.getOrCreate()) { SftpATTRS targetAttributes = findAttributes(targetChannel, target, false); if (targetAttributes != null) { if (options.replaceExisting) { targetChannel.delete(target.path(), targetAttributes.isDir()); } else { throw new FileAlreadyExistsException(target.path()); } } if (sourceAttributes.isDir()) { targetChannel.mkdir(target.path()); } else { copyFile(sourceChannel, source, targetChannel, target, options); } } } private void copyFile(Channel sourceChannel, SFTPPath source, Channel targetChannel, SFTPPath target, CopyOptions options) throws IOException { OpenOptions inOptions = OpenOptions.forNewInputStream(options.toOpenOptions(StandardOpenOption.READ)); OpenOptions outOptions = OpenOptions .forNewOutputStream(options.toOpenOptions(StandardOpenOption.WRITE, StandardOpenOption.CREATE)); try (InputStream in = sourceChannel.newInputStream(source.path(), inOptions)) { targetChannel.storeFile(target.path(), in, outOptions.options); } } void move(SFTPPath source, SFTPPath target, CopyOption... options) throws IOException { boolean sameFileSystem = haveSameFileSystem(source, target); CopyOptions copyOptions = CopyOptions.forMove(sameFileSystem, options); try (Channel channel = channelPool.get()) { if (!sameFileSystem) { SftpATTRS attributes = getAttributes(channel, source, false); if (attributes.isLink()) { throw new IOException(SFTPMessages.copyOfSymbolicLinksAcrossFileSystemsNotSupported()); } copyAcrossFileSystems(channel, source, attributes, target, copyOptions); channel.delete(source.path(), attributes.isDir()); return; } try { if (isSameFile(channel, source, target)) { // non-op, don't do a thing as specified by Files.move return; } } catch (@SuppressWarnings("unused") NoSuchFileException e) { // the source or target does not exist or either path is an invalid link // call getAttributes to ensure the source file exists // ignore any error to target or if the source link is invalid getAttributes(channel, source, false); } if (toAbsolutePath(source).parentPath() == null) { // cannot move or rename the root throw new DirectoryNotEmptyException(source.path()); } SftpATTRS targetAttributes = findAttributes(channel, target, false); if (copyOptions.replaceExisting && targetAttributes != null) { channel.delete(target.path(), targetAttributes.isDir()); } channel.rename(source.path(), target.path()); } } boolean isSameFile(SFTPPath path, SFTPPath path2) throws IOException { if (!haveSameFileSystem(path, path2)) { return false; } if (path.equals(path2)) { return true; } try (Channel channel = channelPool.get()) { return isSameFile(channel, path, path2); } } @SuppressWarnings("resource") private boolean haveSameFileSystem(SFTPPath path, SFTPPath path2) { return path.getFileSystem() == path2.getFileSystem(); } private boolean isSameFile(Channel channel, SFTPPath path, SFTPPath path2) throws IOException { if (path.equals(path2)) { return true; } return toRealPath(channel, path, true).path.path().equals(toRealPath(channel, path2, true).path.path()); } boolean isHidden(SFTPPath path) throws IOException { // call getAttributes to check for existence try (Channel channel = channelPool.get()) { getAttributes(channel, path, false); } String fileName = path.fileName(); return !CURRENT_DIR.equals(fileName) && !PARENT_DIR.equals(fileName) && fileName.startsWith("."); //$NON-NLS-1$ } FileStore getFileStore(SFTPPath path) throws IOException { // call getAttributes to check for existence try (Channel channel = channelPool.get()) { getAttributes(channel, path, false); } return new SFTPFileStore(path); } void checkAccess(SFTPPath path, AccessMode... modes) throws IOException { try (Channel channel = channelPool.get()) { SftpATTRS attributes = getAttributes(channel, path, true); for (AccessMode mode : modes) { if (!hasAccess(attributes, mode)) { throw new AccessDeniedException(path.path()); } } } } private boolean hasAccess(SftpATTRS attrs, AccessMode mode) { switch (mode) { case READ: return PosixFilePermissionSupport.hasPermission(attrs.getPermissions(), PosixFilePermission.OWNER_READ); case WRITE: return PosixFilePermissionSupport.hasPermission(attrs.getPermissions(), PosixFilePermission.OWNER_WRITE); case EXECUTE: return PosixFilePermissionSupport.hasPermission(attrs.getPermissions(), PosixFilePermission.OWNER_EXECUTE); default: return false; } } PosixFileAttributes readAttributes(SFTPPath path, LinkOption... options) throws IOException { boolean followLinks = LinkOptionSupport.followLinks(options); try (Channel channel = channelPool.get()) { SftpATTRS attributes = getAttributes(channel, path, followLinks); return new SFTPPathFileAttributes(attributes); } } private static final class SFTPPathFileAttributes implements PosixFileAttributes { private final SftpATTRS attributes; private SFTPPathFileAttributes(SftpATTRS attributes) { this.attributes = attributes; } @Override public UserPrincipal owner() { String user = Integer.toString(attributes.getUId()); return new SimpleUserPrincipal(user); } @Override public GroupPrincipal group() { String group = Integer.toString(attributes.getGId()); return new SimpleGroupPrincipal(group); } @Override public Set<PosixFilePermission> permissions() { return PosixFilePermissionSupport.fromMask(attributes.getPermissions()); } @Override public FileTime lastModifiedTime() { // times are in seconds return FileTime.from(attributes.getMTime(), TimeUnit.SECONDS); } @Override public FileTime lastAccessTime() { // times are in seconds return FileTime.from(attributes.getATime(), TimeUnit.SECONDS); } @Override public FileTime creationTime() { return lastModifiedTime(); } @Override public boolean isRegularFile() { return attributes.isReg(); } @Override public boolean isDirectory() { return attributes.isDir(); } @Override public boolean isSymbolicLink() { return attributes.isLink(); } @Override public boolean isOther() { return !(isRegularFile() || isDirectory() || isSymbolicLink()); } @Override public long size() { return attributes.getSize(); } @Override public Object fileKey() { return null; } } @SuppressWarnings("nls") private static final Set<String> BASIC_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "basic:lastModifiedTime", "basic:lastAccessTime", "basic:creationTime", "basic:size", "basic:isRegularFile", "basic:isDirectory", "basic:isSymbolicLink", "basic:isOther", "basic:fileKey"))); @SuppressWarnings("nls") private static final Set<String> OWNER_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "owner:owner"))); @SuppressWarnings("nls") private static final Set<String> POSIX_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "posix:lastModifiedTime", "posix:lastAccessTime", "posix:creationTime", "posix:size", "posix:isRegularFile", "posix:isDirectory", "posix:isSymbolicLink", "posix:isOther", "posix:fileKey", "posix:owner", "posix:group", "posix:permissions"))); Map<String, Object> readAttributes(SFTPPath path, String attributes, LinkOption... options) throws IOException { String view; int pos = attributes.indexOf(':'); if (pos == -1) { view = "basic"; //$NON-NLS-1$ attributes = "basic:" + attributes; //$NON-NLS-1$ } else { view = attributes.substring(0, pos); } if (!SUPPORTED_FILE_ATTRIBUTE_VIEWS.contains(view)) { throw Messages.fileSystemProvider().unsupportedFileAttributeView(view); } Set<String> allowedAttributes; if (attributes.startsWith("basic:")) { //$NON-NLS-1$ allowedAttributes = BASIC_ATTRIBUTES; } else if (attributes.startsWith("owner:")) { //$NON-NLS-1$ allowedAttributes = OWNER_ATTRIBUTES; } else if (attributes.startsWith("posix:")) { //$NON-NLS-1$ allowedAttributes = POSIX_ATTRIBUTES; } else { // should not occur throw Messages.fileSystemProvider().unsupportedFileAttributeView(attributes.substring(0, attributes.indexOf(':'))); } Map<String, Object> result = getAttributeMap(attributes, allowedAttributes); PosixFileAttributes posixAttributes = readAttributes(path, options); for (Map.Entry<String, Object> entry : result.entrySet()) { switch (entry.getKey()) { case "basic:lastModifiedTime": //$NON-NLS-1$ case "posix:lastModifiedTime": //$NON-NLS-1$ entry.setValue(posixAttributes.lastModifiedTime()); break; case "basic:lastAccessTime": //$NON-NLS-1$ case "posix:lastAccessTime": //$NON-NLS-1$ entry.setValue(posixAttributes.lastAccessTime()); break; case "basic:creationTime": //$NON-NLS-1$ case "posix:creationTime": //$NON-NLS-1$ entry.setValue(posixAttributes.creationTime()); break; case "basic:size": //$NON-NLS-1$ case "posix:size": //$NON-NLS-1$ entry.setValue(posixAttributes.size()); break; case "basic:isRegularFile": //$NON-NLS-1$ case "posix:isRegularFile": //$NON-NLS-1$ entry.setValue(posixAttributes.isRegularFile()); break; case "basic:isDirectory": //$NON-NLS-1$ case "posix:isDirectory": //$NON-NLS-1$ entry.setValue(posixAttributes.isDirectory()); break; case "basic:isSymbolicLink": //$NON-NLS-1$ case "posix:isSymbolicLink": //$NON-NLS-1$ entry.setValue(posixAttributes.isSymbolicLink()); break; case "basic:isOther": //$NON-NLS-1$ case "posix:isOther": //$NON-NLS-1$ entry.setValue(posixAttributes.isOther()); break; case "basic:fileKey": //$NON-NLS-1$ case "posix:fileKey": //$NON-NLS-1$ entry.setValue(posixAttributes.fileKey()); break; case "owner:owner": //$NON-NLS-1$ case "posix:owner": //$NON-NLS-1$ entry.setValue(posixAttributes.owner()); break; case "posix:group": //$NON-NLS-1$ entry.setValue(posixAttributes.group()); break; case "posix:permissions": //$NON-NLS-1$ entry.setValue(posixAttributes.permissions()); break; default: // should not occur throw new IllegalStateException("unexpected attribute name: " + entry.getKey()); //$NON-NLS-1$ } } return result; } private Map<String, Object> getAttributeMap(String attributes, Set<String> allowedAttributes) { int indexOfColon = attributes.indexOf(':'); String prefix = attributes.substring(0, indexOfColon + 1); attributes = attributes.substring(indexOfColon + 1); String[] attributeList = attributes.split(","); //$NON-NLS-1$ Map<String, Object> result = new HashMap<>(allowedAttributes.size()); for (String attribute : attributeList) { String prefixedAttribute = prefix + attribute; if (allowedAttributes.contains(prefixedAttribute)) { result.put(prefixedAttribute, null); } else if ("*".equals(attribute)) { //$NON-NLS-1$ for (String s : allowedAttributes) { result.put(s, null); } } else { throw Messages.fileSystemProvider().unsupportedFileAttribute(attribute); } } return result; } void setOwner(SFTPPath path, UserPrincipal owner) throws IOException { setOwner(path, owner, false); } private void setOwner(SFTPPath path, UserPrincipal owner, boolean followLinks) throws IOException { try { int uid = Integer.parseInt(owner.getName()); try (Channel channel = channelPool.get()) { if (followLinks) { path = toRealPath(channel, path, followLinks).path; } channel.chown(path.path(), uid); } } catch (NumberFormatException e) { throw new IOException(e); } } void setGroup(SFTPPath path, GroupPrincipal group) throws IOException { setGroup(path, group, false); } private void setGroup(SFTPPath path, GroupPrincipal group, boolean followLinks) throws IOException { try { int gid = Integer.parseInt(group.getName()); try (Channel channel = channelPool.get()) { if (followLinks) { path = toRealPath(channel, path, followLinks).path; } channel.chgrp(path.path(), gid); } } catch (NumberFormatException e) { throw new IOException(e); } } void setPermissions(SFTPPath path, Set<PosixFilePermission> permissions) throws IOException { setPermissions(path, permissions, false); } private void setPermissions(SFTPPath path, Set<PosixFilePermission> permissions, boolean followLinks) throws IOException { try (Channel channel = channelPool.get()) { if (followLinks) { path = toRealPath(channel, path, followLinks).path; } channel.chmod(path.path(), PosixFilePermissionSupport.toMask(permissions)); } } void setTimes(SFTPPath path, FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { if (lastAccessTime != null) { throw new IOException(Messages.fileSystemProvider().unsupportedFileAttribute("lastAccessTime")); //$NON-NLS-1$ } if (createTime != null) { throw new IOException(Messages.fileSystemProvider().unsupportedFileAttribute("createAccessTime")); //$NON-NLS-1$ } if (lastModifiedTime != null) { setLastModifiedTime(path, lastModifiedTime, false); } } void setLastModifiedTime(SFTPPath path, FileTime lastModifiedTime, boolean followLinks) throws IOException { try (Channel channel = channelPool.get()) { if (followLinks) { path = toRealPath(channel, path, followLinks).path; } // times are in seconds channel.setMtime(path.path(), lastModifiedTime.to(TimeUnit.SECONDS)); } } void setAttribute(SFTPPath path, String attribute, Object value, LinkOption... options) throws IOException { String view; int pos = attribute.indexOf(':'); if (pos == -1) { view = "basic"; //$NON-NLS-1$ attribute = "basic:" + attribute; //$NON-NLS-1$ } else { view = attribute.substring(0, pos); } if (!"basic".equals(view) && !"owner".equals(view) && !"posix".equals(view)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ throw Messages.fileSystemProvider().unsupportedFileAttributeView(view); } boolean followLinks = LinkOptionSupport.followLinks(options); switch (attribute) { case "basic:lastModifiedTime": //$NON-NLS-1$ case "posix:lastModifiedTime": //$NON-NLS-1$ setLastModifiedTime(path, (FileTime) value, followLinks); break; case "owner:owner": //$NON-NLS-1$ case "posix:owner": //$NON-NLS-1$ setOwner(path, (UserPrincipal) value, followLinks); break; case "posix:group": //$NON-NLS-1$ setGroup(path, (GroupPrincipal) value, followLinks); break; case "posix:permissions": //$NON-NLS-1$ @SuppressWarnings("unchecked") Set<PosixFilePermission> permissions = (Set<PosixFilePermission>) value; setPermissions(path, permissions, followLinks); break; default: throw Messages.fileSystemProvider().unsupportedFileAttribute(attribute); } } private SftpATTRS getAttributes(Channel channel, SFTPPath path, boolean followLinks) throws IOException { return channel.readAttributes(path.path(), followLinks); } private SftpATTRS findAttributes(Channel channel, SFTPPath path, boolean followLinks) throws IOException { try { return getAttributes(channel, path, followLinks); } catch (@SuppressWarnings("unused") NoSuchFileException e) { return null; } } long getTotalSpace(SFTPPath path) throws IOException { try (Channel channel = channelPool.get()) { SftpStatVFS stat = channel.statVFS(path.path()); // don't use stat.getSize because that uses kilobyte precision return stat.getFragmentSize() * stat.getBlocks(); } catch (@SuppressWarnings("unused") UnsupportedOperationException e) { // statVFS is not available return Long.MAX_VALUE; } } long getUsableSpace(SFTPPath path) throws IOException { try (Channel channel = channelPool.get()) { SftpStatVFS stat = channel.statVFS(path.path()); // don't use stat.getAvailForNonRoot because that uses kilobyte precision return stat.getFragmentSize() * stat.getAvailBlocks(); } catch (@SuppressWarnings("unused") UnsupportedOperationException e) { // statVFS is not available return Long.MAX_VALUE; } } long getUnallocatedSpace(SFTPPath path) throws IOException { try (Channel channel = channelPool.get()) { SftpStatVFS stat = channel.statVFS(path.path()); // don't use stat.getAvail because that uses kilobyte precision return stat.getFragmentSize() * stat.getFreeBlocks(); } catch (@SuppressWarnings("unused") UnsupportedOperationException e) { // statVFS is not available return Long.MAX_VALUE; } } }