http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java new file mode 100644 index 0000000..43cc619 --- /dev/null +++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java @@ -0,0 +1,1038 @@ +/* + * 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; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.Channel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.OpenOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.FileTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +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.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.client.subsystem.SubsystemClient; +import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension; +import org.apache.sshd.common.subsystem.sftp.SftpConstants; +import org.apache.sshd.common.subsystem.sftp.SftpHelper; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * @author <a href="http://mina.apache.org">Apache MINA Project</a> + */ +public interface SftpClient extends SubsystemClient { + /** + * Used to indicate the {@link Charset} (or its name) for decoding + * referenced files/folders names - extracted from the client session + * when 1st initialized. + * @see #DEFAULT_NAME_DECODING_CHARSET + * @see #getNameDecodingCharset() + * @see #setNameDecodingCharset(Charset) + */ + String NAME_DECODING_CHARSET = "sftp-name-decoding-charset"; + + /** + * Default value of {@value #NAME_DECODING_CHARSET} + */ + Charset DEFAULT_NAME_DECODING_CHARSET = StandardCharsets.UTF_8; + + enum OpenMode { + Read, + Write, + Append, + Create, + Truncate, + Exclusive; + + /** + * The {@link Set} of {@link OpenOption}-s supported by {@link #fromOpenOptions(Collection)} + */ + public static final Set<OpenOption> SUPPORTED_OPTIONS = + Collections.unmodifiableSet( + EnumSet.of( + StandardOpenOption.READ, StandardOpenOption.APPEND, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, + StandardOpenOption.SPARSE)); + + /** + * Converts {@link StandardOpenOption}-s into {@link OpenMode}-s + * + * @param options The original options - ignored if {@code null}/empty + * @return A {@link Set} of the equivalent modes + * @throws IllegalArgumentException If an unsupported option is requested + * @see #SUPPORTED_OPTIONS + */ + public static Set<OpenMode> fromOpenOptions(Collection<? extends OpenOption> options) { + if (GenericUtils.isEmpty(options)) { + return Collections.emptySet(); + } + + Set<OpenMode> modes = EnumSet.noneOf(OpenMode.class); + for (OpenOption option : options) { + if (option == StandardOpenOption.READ) { + modes.add(Read); + } else if (option == StandardOpenOption.APPEND) { + modes.add(Append); + } else if (option == StandardOpenOption.CREATE) { + modes.add(Create); + } else if (option == StandardOpenOption.TRUNCATE_EXISTING) { + modes.add(Truncate); + } else if (option == StandardOpenOption.WRITE) { + modes.add(Write); + } else if (option == StandardOpenOption.CREATE_NEW) { + modes.add(Create); + modes.add(Exclusive); + } else if (option == StandardOpenOption.SPARSE) { + /* + * As per the Javadoc: + * + * The option is ignored when the file system does not + * support the creation of sparse files + */ + continue; + } else { + throw new IllegalArgumentException("Unsupported open option: " + option); + } + } + + return modes; + } + } + + enum CopyMode { + Atomic, + Overwrite + } + + enum Attribute { + Size, + UidGid, + Perms, + OwnerGroup, + AccessTime, + ModifyTime, + CreateTime, + Acl, + Extensions + } + + class Handle { + private final String path; + private final byte[] id; + + Handle(String path, byte[] id) { + // clone the original so the handle is immutable + this.path = ValidateUtils.checkNotNullAndNotEmpty(path, "No remote path"); + this.id = ValidateUtils.checkNotNullAndNotEmpty(id, "No handle ID").clone(); + } + + /** + * @return The remote path represented by this handle + */ + public String getPath() { + return path; + } + + public int length() { + return id.length; + } + + /** + * @return A <U>cloned</U> instance of the identifier in order to + * avoid inadvertent modifications to the handle contents + */ + public byte[] getIdentifier() { + return id.clone(); + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (obj == this) { + return true; + } + + // we do not ask getClass() == obj.getClass() in order to allow for derived classes equality + if (!(obj instanceof Handle)) { + return false; + } + + return Arrays.equals(id, ((Handle) obj).id); + } + + @Override + public String toString() { + return getPath() + ": " + BufferUtils.toHex(BufferUtils.EMPTY_HEX_SEPARATOR, id); + } + } + + // CHECKSTYLE:OFF + abstract class CloseableHandle extends Handle implements Channel, Closeable { + protected CloseableHandle(String path, byte[] id) { + super(path, id); + } + } + // CHECKSTYLE:ON + + class Attributes { + private Set<Attribute> flags = EnumSet.noneOf(Attribute.class); + private int type = SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN; + private int perms; + private int uid; + private int gid; + private String owner; + private String group; + private long size; + private FileTime accessTime; + private FileTime createTime; + private FileTime modifyTime; + private List<AclEntry> acl; + private Map<String, byte[]> extensions = Collections.emptyMap(); + + public Attributes() { + super(); + } + + public Set<Attribute> getFlags() { + return flags; + } + + public Attributes addFlag(Attribute flag) { + flags.add(flag); + return this; + } + + public Attributes removeFlag(Attribute flag) { + flags.remove(flag); + return this; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public long getSize() { + return size; + } + + public Attributes size(long size) { + setSize(size); + return this; + } + + public void setSize(long size) { + this.size = size; + addFlag(Attribute.Size); + } + + public String getOwner() { + return owner; + } + + public Attributes owner(String owner) { + setOwner(owner); + return this; + } + + public void setOwner(String owner) { + this.owner = owner; + /* + * According to https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt + * section 7.5 + * + * If either the owner or group field is zero length, the field + * should be considered absent, and no change should be made to + * that specific field during a modification operation. + */ + if (GenericUtils.isEmpty(owner)) { + removeFlag(Attribute.OwnerGroup); + } else { + addFlag(Attribute.OwnerGroup); + } + } + + public String getGroup() { + return group; + } + + public Attributes group(String group) { + setGroup(group); + return this; + } + + public void setGroup(String group) { + this.group = group; + /* + * According to https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt + * section 7.5 + * + * If either the owner or group field is zero length, the field + * should be considered absent, and no change should be made to + * that specific field during a modification operation. + */ + if (GenericUtils.isEmpty(group)) { + removeFlag(Attribute.OwnerGroup); + } else { + addFlag(Attribute.OwnerGroup); + } + } + + public int getUserId() { + return uid; + } + + public int getGroupId() { + return gid; + } + + public Attributes owner(int uid, int gid) { + this.uid = uid; + this.gid = gid; + addFlag(Attribute.UidGid); + return this; + } + + public int getPermissions() { + return perms; + } + + public Attributes perms(int perms) { + setPermissions(perms); + return this; + } + + public void setPermissions(int perms) { + this.perms = perms; + addFlag(Attribute.Perms); + } + + public FileTime getAccessTime() { + return accessTime; + } + + public Attributes accessTime(long atime) { + return accessTime(atime, TimeUnit.SECONDS); + } + + public Attributes accessTime(long atime, TimeUnit unit) { + return accessTime(FileTime.from(atime, unit)); + } + + public Attributes accessTime(FileTime atime) { + setAccessTime(atime); + return this; + } + + public void setAccessTime(FileTime atime) { + accessTime = Objects.requireNonNull(atime, "No access time"); + addFlag(Attribute.AccessTime); + } + + public FileTime getCreateTime() { + return createTime; + } + + public Attributes createTime(long ctime) { + return createTime(ctime, TimeUnit.SECONDS); + } + + public Attributes createTime(long ctime, TimeUnit unit) { + return createTime(FileTime.from(ctime, unit)); + } + + public Attributes createTime(FileTime ctime) { + setCreateTime(ctime); + return this; + } + + public void setCreateTime(FileTime ctime) { + createTime = Objects.requireNonNull(ctime, "No create time"); + addFlag(Attribute.CreateTime); + } + + public FileTime getModifyTime() { + return modifyTime; + } + + public Attributes modifyTime(long mtime) { + return modifyTime(mtime, TimeUnit.SECONDS); + } + + public Attributes modifyTime(long mtime, TimeUnit unit) { + return modifyTime(FileTime.from(mtime, unit)); + } + + public Attributes modifyTime(FileTime mtime) { + setModifyTime(mtime); + return this; + } + + public void setModifyTime(FileTime mtime) { + modifyTime = Objects.requireNonNull(mtime, "No modify time"); + addFlag(Attribute.ModifyTime); + } + + public List<AclEntry> getAcl() { + return acl; + } + + public Attributes acl(List<AclEntry> acl) { + setAcl(acl); + return this; + } + + public void setAcl(List<AclEntry> acl) { + this.acl = Objects.requireNonNull(acl, "No ACLs"); + addFlag(Attribute.Acl); + } + + public Map<String, byte[]> getExtensions() { + return extensions; + } + + public Attributes extensions(Map<String, byte[]> extensions) { + setExtensions(extensions); + return this; + } + + public void setStringExtensions(Map<String, String> extensions) { + setExtensions(SftpHelper.toBinaryExtensions(extensions)); + } + + public void setExtensions(Map<String, byte[]> extensions) { + this.extensions = Objects.requireNonNull(extensions, "No extensions"); + addFlag(Attribute.Extensions); + } + + public boolean isRegularFile() { + return (getPermissions() & SftpConstants.S_IFMT) == SftpConstants.S_IFREG; + } + + public boolean isDirectory() { + return (getPermissions() & SftpConstants.S_IFMT) == SftpConstants.S_IFDIR; + } + + public boolean isSymbolicLink() { + return (getPermissions() & SftpConstants.S_IFMT) == SftpConstants.S_IFLNK; + } + + public boolean isOther() { + return !isRegularFile() && !isDirectory() && !isSymbolicLink(); + } + + @Override + public String toString() { + return "type=" + getType() + + ";size=" + getSize() + + ";uid=" + getUserId() + + ";gid=" + getGroupId() + + ";perms=0x" + Integer.toHexString(getPermissions()) + + ";flags=" + getFlags() + + ";owner=" + getOwner() + + ";group=" + getGroup() + + ";aTime=" + getAccessTime() + + ";cTime=" + getCreateTime() + + ";mTime=" + getModifyTime() + + ";extensions=" + getExtensions().keySet(); + } + } + + class DirEntry { + public static final Comparator<DirEntry> BY_CASE_SENSITIVE_FILENAME = new Comparator<DirEntry>() { + @Override + public int compare(DirEntry o1, DirEntry o2) { + if (o1 == o2) { + return 0; + } else if (o1 == null) { + return 1; + } else if (o2 == null) { + return -1; + } else { + return GenericUtils.safeCompare(o1.getFilename(), o2.getFilename(), true); + } + } + }; + + public static final Comparator<DirEntry> BY_CASE_INSENSITIVE_FILENAME = new Comparator<DirEntry>() { + @Override + public int compare(DirEntry o1, DirEntry o2) { + if (o1 == o2) { + return 0; + } else if (o1 == null) { + return 1; + } else if (o2 == null) { + return -1; + } else { + return GenericUtils.safeCompare(o1.getFilename(), o2.getFilename(), false); + } + } + }; + + private final String filename; + private final String longFilename; + private final Attributes attributes; + + public DirEntry(String filename, String longFilename, Attributes attributes) { + this.filename = filename; + this.longFilename = longFilename; + this.attributes = attributes; + } + + public String getFilename() { + return filename; + } + + public String getLongFilename() { + return longFilename; + } + + public Attributes getAttributes() { + return attributes; + } + + @Override + public String toString() { + return getFilename() + "[" + getLongFilename() + "]: " + getAttributes(); + } + } + + DirEntry[] EMPTY_DIR_ENTRIES = new DirEntry[0]; + + // default values used if none specified + int MIN_BUFFER_SIZE = Byte.MAX_VALUE; + int MIN_READ_BUFFER_SIZE = MIN_BUFFER_SIZE; + int MIN_WRITE_BUFFER_SIZE = MIN_BUFFER_SIZE; + int IO_BUFFER_SIZE = 32 * 1024; + int DEFAULT_READ_BUFFER_SIZE = IO_BUFFER_SIZE; + int DEFAULT_WRITE_BUFFER_SIZE = IO_BUFFER_SIZE; + long DEFAULT_WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(15L); + + /** + * Property that can be used on the {@link org.apache.sshd.common.FactoryManager} + * to control the internal timeout used by the client to open a channel. + * If not specified then {@link #DEFAULT_CHANNEL_OPEN_TIMEOUT} value + * is used + */ + String SFTP_CHANNEL_OPEN_TIMEOUT = "sftp-channel-open-timeout"; + long DEFAULT_CHANNEL_OPEN_TIMEOUT = DEFAULT_WAIT_TIMEOUT; + + /** + * Default modes for opening a channel if no specific modes specified + */ + Set<OpenMode> DEFAULT_CHANNEL_MODES = + Collections.unmodifiableSet(EnumSet.of(OpenMode.Read, OpenMode.Write)); + + /** + * @return The negotiated SFTP protocol version + */ + int getVersion(); + + @Override + default String getName() { + return SftpConstants.SFTP_SUBSYSTEM_NAME; + } + + /** + * @return The (never {@code null}) {@link Charset} used to decode referenced files/folders names + * @see #NAME_DECODING_CHARSET + */ + Charset getNameDecodingCharset(); + + void setNameDecodingCharset(Charset cs); + + /** + * @return An (unmodifiable) {@link NavigableMap} of the reported server extensions. + * where key=extension name (case <U>insensitive</U>) + */ + NavigableMap<String, byte[]> getServerExtensions(); + + boolean isClosing(); + + // + // Low level API + // + + /** + * Opens a remote file for read + * + * @param path The remote path + * @return The file's {@link CloseableHandle} + * @throws IOException If failed to open the remote file + * @see #open(String, Collection) + */ + default CloseableHandle open(String path) throws IOException { + return open(path, Collections.emptySet()); + } + + /** + * Opens a remote file with the specified mode(s) + * + * @param path The remote path + * @param options The desired mode - if none specified + * then {@link OpenMode#Read} is assumed + * @return The file's {@link CloseableHandle} + * @throws IOException If failed to open the remote file + * @see #open(String, Collection) + */ + default CloseableHandle open(String path, OpenMode... options) throws IOException { + return open(path, GenericUtils.of(options)); + } + + /** + * Opens a remote file with the specified mode(s) + * + * @param path The remote path + * @param options The desired mode - if none specified + * then {@link OpenMode#Read} is assumed + * @return The file's {@link CloseableHandle} + * @throws IOException If failed to open the remote file + */ + CloseableHandle open(String path, Collection<OpenMode> options) throws IOException; + + /** + * Close the handle obtained from one of the {@code open} methods + * + * @param handle The {@code Handle} to close + * @throws IOException If failed to execute + */ + void close(Handle handle) throws IOException; + + /** + * @param path The remote path to remove + * @throws IOException If failed to execute + */ + void remove(String path) throws IOException; + + default void rename(String oldPath, String newPath) throws IOException { + rename(oldPath, newPath, Collections.emptySet()); + } + + default void rename(String oldPath, String newPath, CopyMode... options) throws IOException { + rename(oldPath, newPath, GenericUtils.of(options)); + } + + void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException; + + /** + * Reads data from the open (file) handle + * + * @param handle The file {@link Handle} to read from + * @param fileOffset The file offset to read from + * @param dst The destination buffer + * @return Number of read bytes - {@code -1} if EOF reached + * @throws IOException If failed to read the data + * @see #read(Handle, long, byte[], int, int) + */ + default int read(Handle handle, long fileOffset, byte[] dst) throws IOException { + return read(handle, fileOffset, dst, null); + } + + /** + * Reads data from the open (file) handle + * + * @param handle The file {@link Handle} to read from + * @param fileOffset The file offset to read from + * @param dst The destination buffer + * @param eofSignalled If not {@code null} then upon return holds a value indicating + * whether EOF was reached due to the read. If {@code null} indicator + * value then this indication is not available + * @return Number of read bytes - {@code -1} if EOF reached + * @throws IOException If failed to read the data + * @see #read(Handle, long, byte[], int, int, AtomicReference) + * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A> + */ + default int read(Handle handle, long fileOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException { + return read(handle, fileOffset, dst, 0, dst.length, eofSignalled); + } + + default int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len) throws IOException { + return read(handle, fileOffset, dst, dstOffset, len, null); + } + + /** + * Reads data from the open (file) handle + * + * @param handle The file {@link Handle} to read from + * @param fileOffset The file offset to read from + * @param dst The destination buffer + * @param dstOffset Offset in destination buffer to place the read data + * @param len Available destination buffer size to read + * @param eofSignalled If not {@code null} then upon return holds a value indicating + * whether EOF was reached due to the read. If {@code null} indicator + * value then this indication is not available + * @return Number of read bytes - {@code -1} if EOF reached + * @throws IOException If failed to read the data + * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A> + */ + int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException; + + default void write(Handle handle, long fileOffset, byte[] src) throws IOException { + write(handle, fileOffset, src, 0, src.length); + } + + /** + * Write data to (open) file handle + * + * @param handle The file {@link Handle} + * @param fileOffset Zero-based offset to write in file + * @param src Data buffer + * @param srcOffset Offset of valid data in buffer + * @param len Number of bytes to write + * @throws IOException If failed to write the data + */ + void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException; + + /** + * Create remote directory + * + * @param path Remote directory path + * @throws IOException If failed to execute + */ + void mkdir(String path) throws IOException; + + /** + * Remove remote directory + * + * @param path Remote directory path + * @throws IOException If failed to execute + */ + void rmdir(String path) throws IOException; + + /** + * Obtain a handle for a directory + * + * @param path Remote directory path + * @return The associated directory {@link Handle} + * @throws IOException If failed to execute + */ + CloseableHandle openDir(String path) throws IOException; + + /** + * @param handle Directory {@link Handle} to read from + * @return A {@link List} of entries - {@code null} to indicate no more entries + * <B>Note:</B> the list may be <U>incomplete</U> since the client and + * server have some internal imposed limit on the number of entries they + * can process. Therefore several calls to this method may be required + * (until {@code null}). In order to iterate over all the entries use + * {@link #readDir(String)} + * @throws IOException If failed to access the remote site + */ + default List<DirEntry> readDir(Handle handle) throws IOException { + return readDir(handle, null); + } + + /** + * @param handle Directory {@link Handle} to read from + * @return A {@link List} of entries - {@code null} to indicate no more entries + * @param eolIndicator An indicator that can be used to get information + * whether end of list has been reached - ignored if {@code null}. Upon + * return, set value indicates whether all entries have been exhausted - a {@code null} + * value means that this information cannot be provided and another call to + * {@code readDir} is necessary in order to verify that no more entries are pending + * @throws IOException If failed to access the remote site + * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A> + */ + List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException; + + /** + * @param handle A directory {@link Handle} + * @return An {@link Iterable} that can be used to iterate over all the + * directory entries (like {@link #readDir(String)}). <B>Note:</B> the + * iterable instance is not re-usable - i.e., files can be iterated + * only <U>once</U> + * @throws IOException If failed to access the directory + */ + default Iterable<DirEntry> listDir(Handle handle) throws IOException { + if (!isOpen()) { + throw new IOException("listDir(" + handle + ") client is closed"); + } + + return new StfpIterableDirHandle(this, handle); + } + + /** + * The effective "normalized" remote path + * + * @param path The requested path - may be relative, and/or contain + * dots - e.g., ".", "..", "./foo", "../bar" + * + * @return The effective "normalized" remote path + * @throws IOException If failed to execute + */ + String canonicalPath(String path) throws IOException; + + /** + * Retrieve remote path meta-data - follow symbolic links if encountered + * + * @param path The remote path + * @return The associated {@link Attributes} + * @throws IOException If failed to execute + */ + Attributes stat(String path) throws IOException; + + /** + * Retrieve remote path meta-data - do <B>not</B> follow symbolic links + * + * @param path The remote path + * @return The associated {@link Attributes} + * @throws IOException If failed to execute + */ + Attributes lstat(String path) throws IOException; + + /** + * Retrieve file/directory handle meta-data + * + * @param handle The {@link Handle} obtained via one of the {@code open} calls + * @return The associated {@link Attributes} + * @throws IOException If failed to execute + */ + Attributes stat(Handle handle) throws IOException; + + /** + * Update remote node meta-data + * + * @param path The remote path + * @param attributes The {@link Attributes} to update + * @throws IOException If failed to execute + */ + void setStat(String path, Attributes attributes) throws IOException; + + /** + * Update remote node meta-data + * + * @param handle The {@link Handle} obtained via one of the {@code open} calls + * @param attributes The {@link Attributes} to update + * @throws IOException If failed to execute + */ + void setStat(Handle handle, Attributes attributes) throws IOException; + + /** + * Retrieve target of a link + * + * @param path Remote path that represents a link + * @return The link target + * @throws IOException If failed to execute + */ + String readLink(String path) throws IOException; + + /** + * Create symbolic link + * + * @param linkPath The link location + * @param targetPath The referenced target by the link + * @throws IOException If failed to execute + * @see #link(String, String, boolean) + */ + default void symLink(String linkPath, String targetPath) throws IOException { + link(linkPath, targetPath, true); + } + + /** + * Create a link + * + * @param linkPath The link location + * @param targetPath The referenced target by the link + * @param symbolic If {@code true} then make this a symbolic link, otherwise a hard one + * @throws IOException If failed to execute + */ + void link(String linkPath, String targetPath, boolean symbolic) throws IOException; + + // see SSH_FXP_BLOCK / SSH_FXP_UNBLOCK for byte range locks + void lock(Handle handle, long offset, long length, int mask) throws IOException; + + void unlock(Handle handle, long offset, long length) throws IOException; + + // + // High level API + // + + default SftpRemotePathChannel openRemotePathChannel(String path, OpenOption... options) throws IOException { + return openRemotePathChannel(path, GenericUtils.isEmpty(options) ? Collections.emptyList() : Arrays.asList(options)); + } + + default SftpRemotePathChannel openRemotePathChannel(String path, Collection<? extends OpenOption> options) throws IOException { + return openRemoteFileChannel(path, OpenMode.fromOpenOptions(options)); + } + + default SftpRemotePathChannel openRemoteFileChannel(String path, OpenMode... modes) throws IOException { + return openRemoteFileChannel(path, GenericUtils.isEmpty(modes) ? Collections.emptyList() : Arrays.asList(modes)); + } + + /** + * Opens an {@link SftpRemotePathChannel} on the specified remote path + * + * @param path The remote path + * @param modes The access mode(s) - if {@code null}/empty then the {@link #DEFAULT_CHANNEL_MODES} are used + * @return The open {@link SftpRemotePathChannel} - <B>Note:</B> do not close this + * owner client instance until the channel is no longer needed since it uses the client + * for providing the channel's functionality. + * @throws IOException If failed to open the channel + * @see java.nio.channels.Channels#newInputStream(java.nio.channels.ReadableByteChannel) + * @see java.nio.channels.Channels#newOutputStream(java.nio.channels.WritableByteChannel) + */ + default SftpRemotePathChannel openRemoteFileChannel(String path, Collection<OpenMode> modes) throws IOException { + return new SftpRemotePathChannel(path, this, false, GenericUtils.isEmpty(modes) ? DEFAULT_CHANNEL_MODES : modes); + } + + /** + * @param path The remote directory path + * @return An {@link Iterable} that can be used to iterate over all the + * directory entries (unlike {@link #readDir(Handle)}) + * @throws IOException If failed to access the remote site + * @see #readDir(Handle) + */ + default Iterable<DirEntry> readDir(String path) throws IOException { + if (!isOpen()) { + throw new IOException("readDir(" + path + ") client is closed"); + } + + return new SftpIterableDirEntry(this, path); + } + + default InputStream read(String path) throws IOException { + return read(path, DEFAULT_READ_BUFFER_SIZE); + } + + default InputStream read(String path, int bufferSize) throws IOException { + return read(path, bufferSize, EnumSet.of(OpenMode.Read)); + } + + default InputStream read(String path, OpenMode... mode) throws IOException { + return read(path, DEFAULT_READ_BUFFER_SIZE, mode); + } + + default InputStream read(String path, int bufferSize, OpenMode... mode) throws IOException { + return read(path, bufferSize, GenericUtils.of(mode)); + } + + default InputStream read(String path, Collection<OpenMode> mode) throws IOException { + return read(path, DEFAULT_READ_BUFFER_SIZE, mode); + } + + /** + * Read a remote file's data via an input stream + * + * @param path The remote file path + * @param bufferSize The internal read buffer size + * @param mode The remote file {@link OpenMode}s + * @return An {@link InputStream} for reading the remote file data + * @throws IOException If failed to execute + */ + default InputStream read(String path, int bufferSize, Collection<OpenMode> mode) throws IOException { + if (bufferSize < MIN_READ_BUFFER_SIZE) { + throw new IllegalArgumentException("Insufficient read buffer size: " + bufferSize + ", min.=" + MIN_READ_BUFFER_SIZE); + } + + if (!isOpen()) { + throw new IOException("read(" + path + ")[" + mode + "] size=" + bufferSize + ": client is closed"); + } + + return new SftpInputStreamWithChannel(this, bufferSize, path, mode); + } + + default OutputStream write(String path) throws IOException { + return write(path, DEFAULT_WRITE_BUFFER_SIZE); + } + + default OutputStream write(String path, int bufferSize) throws IOException { + return write(path, bufferSize, EnumSet.of(OpenMode.Write, OpenMode.Create, OpenMode.Truncate)); + } + + default OutputStream write(String path, OpenMode... mode) throws IOException { + return write(path, DEFAULT_WRITE_BUFFER_SIZE, mode); + } + + default OutputStream write(String path, int bufferSize, OpenMode... mode) throws IOException { + return write(path, bufferSize, GenericUtils.of(mode)); + } + + default OutputStream write(String path, Collection<OpenMode> mode) throws IOException { + return write(path, DEFAULT_WRITE_BUFFER_SIZE, mode); + } + + /** + * Write to a remote file via an output stream + * + * @param path The remote file path + * @param bufferSize The internal write buffer size + * @param mode The remote file {@link OpenMode}s + * @return An {@link OutputStream} for writing the data + * @throws IOException If failed to execute + */ + default OutputStream write(String path, int bufferSize, Collection<OpenMode> mode) throws IOException { + if (bufferSize < MIN_WRITE_BUFFER_SIZE) { + throw new IllegalArgumentException("Insufficient write buffer size: " + bufferSize + ", min.=" + MIN_WRITE_BUFFER_SIZE); + } + + if (!isOpen()) { + throw new IOException("write(" + path + ")[" + mode + "] size=" + bufferSize + ": client is closed"); + } + + return new SftpOutputStreamWithChannel(this, bufferSize, path, mode); + } + + /** + * @param <E> The generic extension type + * @param extensionType The extension type + * @return The extension instance - <B>Note:</B> it is up to the caller + * to invoke {@link SftpClientExtension#isSupported()} - {@code null} if + * this extension type is not implemented by the client + * @see #getServerExtensions() + */ + <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType); + + /** + * @param extensionName The extension name + * @return The extension instance - <B>Note:</B> it is up to the caller + * to invoke {@link SftpClientExtension#isSupported()} - {@code null} if + * this extension type is not implemented by the client + * @see #getServerExtensions() + */ + SftpClientExtension getExtension(String extensionName); +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java new file mode 100644 index 0000000..7f79b33 --- /dev/null +++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClientFactory.java @@ -0,0 +1,100 @@ +/* + * 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; + +import java.io.IOException; +import java.nio.file.FileSystem; + +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.subsystem.sftp.impl.DefaultSftpClientFactory; + +/** + * TODO Add javadoc + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public interface SftpClientFactory { + + static SftpClientFactory instance() { + return DefaultSftpClientFactory.INSTANCE; + } + + /** + * Create an SFTP client from this session. + * + * @return The created {@link SftpClient} + * @throws IOException if failed to create the client + */ + default SftpClient createSftpClient(ClientSession session) throws IOException { + return createSftpClient(session, SftpVersionSelector.CURRENT); + } + + /** + * Creates an SFTP client using the specified version + * + * @param version The version to use - <B>Note:</B> if the specified + * version is not supported by the server then an exception + * will occur + * @return The created {@link SftpClient} + * @throws IOException If failed to create the client or use the specified version + */ + default SftpClient createSftpClient(ClientSession session, int version) throws IOException { + return createSftpClient(session, SftpVersionSelector.fixedVersionSelector(version)); + } + + /** + * @param session The {@link ClientSession} to which the SFTP client should be attached + * @param selector The {@link SftpVersionSelector} to use in order to negotiate the SFTP version + * @return The created {@link SftpClient} instance + * @throws IOException If failed to create the client + */ + SftpClient createSftpClient(ClientSession session, SftpVersionSelector selector) throws IOException; + + default FileSystem createSftpFileSystem(ClientSession session) throws IOException { + return createSftpFileSystem(session, SftpVersionSelector.CURRENT); + } + + default FileSystem createSftpFileSystem(ClientSession session, int version) throws IOException { + return createSftpFileSystem(session, SftpVersionSelector.fixedVersionSelector(version)); + } + + default FileSystem createSftpFileSystem(ClientSession session, SftpVersionSelector selector) throws IOException { + return createSftpFileSystem(session, selector, SftpClient.DEFAULT_READ_BUFFER_SIZE, SftpClient.DEFAULT_WRITE_BUFFER_SIZE); + } + + default FileSystem createSftpFileSystem(ClientSession session, int version, int readBufferSize, int writeBufferSize) throws IOException { + return createSftpFileSystem(session, SftpVersionSelector.fixedVersionSelector(version), readBufferSize, writeBufferSize); + } + + default FileSystem createSftpFileSystem(ClientSession session, int readBufferSize, int writeBufferSize) throws IOException { + return createSftpFileSystem(session, SftpVersionSelector.CURRENT, readBufferSize, writeBufferSize); + } + + /** + * @param session The {@link ClientSession} to which the SFTP client backing the file system should be attached + * @param selector The {@link SftpVersionSelector} to use in order to negotiate the SFTP version + * @param readBufferSize Default I/O read buffer size + * @param writeBufferSize Default I/O write buffer size + * @return The created {@link FileSystem} instance + * @throws IOException If failed to create the instance + */ + FileSystem createSftpFileSystem( + ClientSession session, SftpVersionSelector selector, int readBufferSize, int writeBufferSize) + throws IOException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java new file mode 100644 index 0000000..61a83ec --- /dev/null +++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java @@ -0,0 +1,920 @@ +/* + * 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; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.channels.Channel; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.logging.Level; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes; +import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry; +import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo; +import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension; +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.kex.KexProposalOption; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.subsystem.sftp.SftpConstants; +import org.apache.sshd.common.subsystem.sftp.SftpException; +import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils; +import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.OsUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.io.NoCloseInputStream; + +/** + * Implements a simple command line SFTP client similar to the Linux one + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class SftpCommand implements Channel { + /** + * Command line option used to indicate a non-default port number + */ + public static final String SFTP_PORT_OPTION = "-P"; + + private final SftpClient client; + private final Map<String, CommandExecutor> commandsMap; + private String cwdRemote; + private String cwdLocal; + + @SuppressWarnings("synthetic-access") + public SftpCommand(SftpClient client) { + this.client = Objects.requireNonNull(client, "No client"); + + Map<String, CommandExecutor> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (CommandExecutor e : Arrays.asList( + new ExitCommandExecutor(), + new PwdCommandExecutor(), + new InfoCommandExecutor(), + new SessionCommandExecutor(), + new VersionCommandExecutor(), + new CdCommandExecutor(), + new LcdCommandExecutor(), + new MkdirCommandExecutor(), + new LsCommandExecutor(), + new LStatCommandExecutor(), + new ReadLinkCommandExecutor(), + new RmCommandExecutor(), + new RmdirCommandExecutor(), + new RenameCommandExecutor(), + new StatVfsCommandExecutor(), + new GetCommandExecutor(), + new PutCommandExecutor(), + new HelpCommandExecutor() + )) { + String name = e.getName(); + ValidateUtils.checkTrue(map.put(name, e) == null, "Multiple commands named '%s'", name); + } + commandsMap = Collections.unmodifiableMap(map); + cwdLocal = System.getProperty("user.dir"); + } + + public final SftpClient getClient() { + return client; + } + + public void doInteractive(BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + SftpClient sftp = getClient(); + setCurrentRemoteDirectory(sftp.canonicalPath(".")); + while (true) { + stdout.append(getCurrentRemoteDirectory()).append(" > ").flush(); + String line = stdin.readLine(); + if (line == null) { // EOF + break; + } + + line = GenericUtils.replaceWhitespaceAndTrim(line); + if (GenericUtils.isEmpty(line)) { + continue; + } + + String cmd; + String args; + int pos = line.indexOf(' '); + if (pos > 0) { + cmd = line.substring(0, pos); + args = line.substring(pos + 1).trim(); + } else { + cmd = line; + args = ""; + } + + CommandExecutor exec = commandsMap.get(cmd); + try { + if (exec == null) { + stderr.append("Unknown command: ").println(line); + } else { + try { + if (exec.executeCommand(args, stdin, stdout, stderr)) { + break; + } + } catch (Exception e) { + stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage()); + } finally { + stdout.flush(); + } + } + } finally { + stderr.flush(); // just makings sure + } + } + } + + protected String resolveLocalPath(String pathArg) { + String cwd = getCurrentLocalDirectory(); + if (GenericUtils.isEmpty(pathArg)) { + return cwd; + } + + if (OsUtils.isWin32()) { + if ((pathArg.length() >= 2) && (pathArg.charAt(1) == ':')) { + return pathArg; + } + } else { + if (pathArg.charAt(0) == '/') { + return pathArg; + } + } + + return cwd + File.separator + pathArg.replace('/', File.separatorChar); + } + + protected String resolveRemotePath(String pathArg) { + String cwd = getCurrentRemoteDirectory(); + if (GenericUtils.isEmpty(pathArg)) { + return cwd; + } + + if (pathArg.charAt(0) == '/') { + return pathArg; + } else { + return cwd + "/" + pathArg; + } + } + + protected <A extends Appendable> A appendFileAttributes(A stdout, SftpClient sftp, String path, Attributes attrs) throws IOException { + stdout.append('\t').append(Long.toString(attrs.getSize())) + .append('\t').append(SftpFileSystemProvider.getRWXPermissions(attrs.getPermissions())); + if (attrs.isSymbolicLink()) { + String linkValue = sftp.readLink(path); + stdout.append(" => ") + .append('(').append(attrs.isDirectory() ? "dir" : "file").append(')') + .append(' ').append(linkValue); + } + + return stdout; + } + + public String getCurrentRemoteDirectory() { + return cwdRemote; + } + + public void setCurrentRemoteDirectory(String path) { + cwdRemote = path; + } + + public String getCurrentLocalDirectory() { + return cwdLocal; + } + + public void setCurrentLocalDirectory(String path) { + cwdLocal = path; + } + + @Override + public boolean isOpen() { + return client.isOpen(); + } + + @Override + public void close() throws IOException { + if (isOpen()) { + client.close(); + } + } + + public interface CommandExecutor extends NamedResource { + // return value is whether to stop running + boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception; + } + + ////////////////////////////////////////////////////////////////////////// + + public static <A extends Appendable> A appendInfoValue(A sb, CharSequence name, Object value) throws IOException { + sb.append('\t').append(name).append(": ").append(Objects.toString(value)); + return sb; + } + + public static void main(String[] args) throws Exception { + PrintStream stdout = System.out; + PrintStream stderr = System.err; + OutputStream logStream = stderr; + try (BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) { + Level level = SshClient.resolveLoggingVerbosity(args); + logStream = SshClient.resolveLoggingTargetStream(stdout, stderr, args); + if (logStream != null) { + SshClient.setupLogging(level, stdout, stderr, logStream); + } + + ClientSession session = (logStream == null) ? null : SshClient.setupClientSession(SFTP_PORT_OPTION, stdin, stdout, stderr, args); + if (session == null) { + System.err.println("usage: sftp [-v[v][v]] [-E logoutput] [-i identity]" + + " [-l login] [" + SFTP_PORT_OPTION + " port] [-o option=value]" + + " [-w password] [-c cipherlist] [-m maclist] [-C] hostname/user@host"); + System.exit(-1); + return; + } + + try { + try (SftpCommand sftp = new SftpCommand(SftpClientFactory.instance().createSftpClient(session))) { + sftp.doInteractive(stdin, stdout, stderr); + } + } finally { + session.close(); + } + } finally { + if ((logStream != stdout) && (logStream != stderr)) { + logStream.close(); + } + } + } + + private static class ExitCommandExecutor implements CommandExecutor { + ExitCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "exit"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args); + stdout.println("Exiting"); + return true; + } + } + + private class PwdCommandExecutor implements CommandExecutor { + protected PwdCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "pwd"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args); + stdout.append('\t').append("Remote: ").println(getCurrentRemoteDirectory()); + stdout.append('\t').append("Local: ").println(getCurrentLocalDirectory()); + return false; + } + } + + private class SessionCommandExecutor implements CommandExecutor { + SessionCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "session"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args); + SftpClient sftp = getClient(); + ClientSession session = sftp.getSession(); + appendInfoValue(stdout, "Session ID", BufferUtils.toHex(session.getSessionId())).println(); + appendInfoValue(stdout, "Connect address", session.getConnectAddress()).println(); + + IoSession ioSession = session.getIoSession(); + appendInfoValue(stdout, "Local address", ioSession.getLocalAddress()).println(); + appendInfoValue(stdout, "Remote address", ioSession.getRemoteAddress()).println(); + + for (KexProposalOption option : KexProposalOption.VALUES) { + appendInfoValue(stdout, option.getDescription(), session.getNegotiatedKexParameter(option)).println(); + } + + return false; + } + } + + private class InfoCommandExecutor implements CommandExecutor { + InfoCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "info"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args); + SftpClient sftp = getClient(); + Session session = sftp.getSession(); + stdout.append('\t').println(session.getServerVersion()); + + Map<String, byte[]> extensions = sftp.getServerExtensions(); + Map<String, ?> parsed = ParserUtils.parse(extensions); + if (GenericUtils.size(extensions) > 0) { + stdout.println(); + } + + extensions.forEach((name, value) -> { + Object info = parsed.get(name); + + stdout.append('\t').append(name).append(": "); + if (info == null) { + stdout.println(BufferUtils.toHex(value)); + } else { + stdout.println(info); + } + }); + + return false; + } + } + + private class VersionCommandExecutor implements CommandExecutor { + VersionCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "version"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args); + SftpClient sftp = getClient(); + stdout.append('\t').println(sftp.getVersion()); + return false; + } + } + + private class CdCommandExecutor extends PwdCommandExecutor { + CdCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "cd"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified"); + + String newPath = resolveRemotePath(args); + SftpClient sftp = getClient(); + setCurrentRemoteDirectory(sftp.canonicalPath(newPath)); + return super.executeCommand("", stdin, stdout, stderr); + } + } + + private class LcdCommandExecutor extends PwdCommandExecutor { + LcdCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "lcd"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + if (GenericUtils.isEmpty(args)) { + setCurrentLocalDirectory(System.getProperty("user.home")); + } else { + Path path = Paths.get(resolveLocalPath(args)).normalize().toAbsolutePath(); + ValidateUtils.checkTrue(Files.exists(path), "No such local directory: %s", path); + ValidateUtils.checkTrue(Files.isDirectory(path), "Path is not a directory: %s", path); + setCurrentLocalDirectory(path.toString()); + } + + return super.executeCommand("", stdin, stdout, stderr); + } + } + + private class MkdirCommandExecutor implements CommandExecutor { + MkdirCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "mkdir"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified"); + + String path = resolveRemotePath(args); + SftpClient sftp = getClient(); + sftp.mkdir(path); + return false; + } + } + + private class LsCommandExecutor implements CommandExecutor { + LsCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "ls"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + String[] comps = GenericUtils.split(args, ' '); + int numComps = GenericUtils.length(comps); + String pathArg = (numComps <= 0) ? null : GenericUtils.trimToEmpty(comps[numComps - 1]); + String flags = (numComps >= 2) ? GenericUtils.trimToEmpty(comps[0]) : null; + // ignore all flags + if ((GenericUtils.length(pathArg) > 0) && (pathArg.charAt(0) == '-')) { + flags = pathArg; + pathArg = null; + } + + String path = resolveRemotePath(pathArg); + SftpClient sftp = getClient(); + int version = sftp.getVersion(); + boolean showLongName = (version == SftpConstants.SFTP_V3) && (GenericUtils.length(flags) > 1) && (flags.indexOf('l') > 0); + for (SftpClient.DirEntry entry : sftp.readDir(path)) { + String fileName = entry.getFilename(); + SftpClient.Attributes attrs = entry.getAttributes(); + appendFileAttributes(stdout.append('\t').append(fileName), sftp, path + "/" + fileName, attrs).println(); + if (showLongName) { + stdout.append("\t\tlong-name: ").println(entry.getLongFilename()); + } + } + + return false; + } + } + + private class RmCommandExecutor implements CommandExecutor { + RmCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "rm"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + String[] comps = GenericUtils.split(args, ' '); + int numArgs = GenericUtils.length(comps); + ValidateUtils.checkTrue(numArgs >= 1, "No arguments"); + ValidateUtils.checkTrue(numArgs <= 2, "Too many arguments: %s", args); + + String remotePath = comps[0]; + boolean recursive = false; + boolean verbose = false; + if (remotePath.charAt(0) == '-') { + ValidateUtils.checkTrue(remotePath.length() > 1, "Missing flags specification: %s", args); + ValidateUtils.checkTrue(numArgs == 2, "Missing remote directory: %s", args); + + for (int index = 1; index < remotePath.length(); index++) { + char ch = remotePath.charAt(index); + switch(ch) { + case 'r' : + recursive = true; + break; + case 'v': + verbose = true; + break; + default: + throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")"); + } + } + remotePath = comps[1]; + } + + String path = resolveRemotePath(remotePath); + SftpClient sftp = getClient(); + if (recursive) { + Attributes attrs = sftp.stat(path); + ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", args); + removeRecursive(sftp, path, attrs, stdout, verbose); + } else { + sftp.remove(path); + if (verbose) { + stdout.append('\t').append("Removed ").println(path); + } + } + + return false; + } + + private void removeRecursive(SftpClient sftp, String path, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException { + if (attrs.isDirectory()) { + for (DirEntry entry : sftp.readDir(path)) { + String name = entry.getFilename(); + if (".".equals(name) || "..".equals(name)) { + continue; + } + + removeRecursive(sftp, path + "/" + name, entry.getAttributes(), stdout, verbose); + } + + sftp.rmdir(path); + } else if (attrs.isRegularFile()) { + sftp.remove(path); + } else { + if (verbose) { + stdout.append('\t').append("Skip special file ").println(path); + return; + } + } + + if (verbose) { + stdout.append('\t').append("Removed ").println(path); + } + } + } + + private class RmdirCommandExecutor implements CommandExecutor { + RmdirCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "rmdir"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified"); + + String path = resolveRemotePath(args); + SftpClient sftp = getClient(); + sftp.rmdir(path); + return false; + } + } + + private class RenameCommandExecutor implements CommandExecutor { + RenameCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "rename"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + String[] comps = GenericUtils.split(args, ' '); + ValidateUtils.checkTrue(GenericUtils.length(comps) == 2, "Invalid number of arguments: %s", args); + + String oldPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[0])); + String newPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[1])); + SftpClient sftp = getClient(); + sftp.rename(oldPath, newPath); + return false; + } + } + + private class StatVfsCommandExecutor implements CommandExecutor { + StatVfsCommandExecutor() { + super(); + } + + @Override + public String getName() { + return StatVfsExtensionParser.NAME; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + String[] comps = GenericUtils.split(args, ' '); + int numArgs = GenericUtils.length(comps); + ValidateUtils.checkTrue(numArgs <= 1, "Invalid number of arguments: %s", args); + + SftpClient sftp = getClient(); + OpenSSHStatPathExtension ext = sftp.getExtension(OpenSSHStatPathExtension.class); + ValidateUtils.checkTrue(ext.isSupported(), "Extension not supported by server: %s", ext.getName()); + + String remPath = resolveRemotePath((numArgs >= 1) ? GenericUtils.trimToEmpty(comps[0]) : GenericUtils.trimToEmpty(args)); + OpenSSHStatExtensionInfo info = ext.stat(remPath); + Field[] fields = info.getClass().getFields(); + for (Field f : fields) { + String name = f.getName(); + int mod = f.getModifiers(); + if (Modifier.isStatic(mod)) { + continue; + } + + Object value = f.get(info); + stdout.append('\t').append(name).append(": ").println(value); + } + + return false; + } + } + + private class LStatCommandExecutor implements CommandExecutor { + LStatCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "lstat"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + String[] comps = GenericUtils.split(args, ' '); + ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args); + + String path = GenericUtils.trimToEmpty(resolveRemotePath(args)); + SftpClient client = getClient(); + Attributes attrs = client.lstat(path); + appendFileAttributes(stdout, client, path, attrs).println(); + return false; + } + } + + private class ReadLinkCommandExecutor implements CommandExecutor { + ReadLinkCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "readlink"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + String[] comps = GenericUtils.split(args, ' '); + ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args); + + String path = GenericUtils.trimToEmpty(resolveRemotePath(args)); + SftpClient client = getClient(); + String linkData = client.readLink(path); + stdout.append('\t').println(linkData); + return false; + } + } + + private class HelpCommandExecutor implements CommandExecutor { + HelpCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "help"; + } + + @Override + @SuppressWarnings("synthetic-access") + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args); + for (String cmd : commandsMap.keySet()) { + stdout.append('\t').println(cmd); + } + return false; + } + } + + private abstract class TransferCommandExecutor implements CommandExecutor { + protected TransferCommandExecutor() { + super(); + } + + protected void createDirectories(SftpClient sftp, String remotePath) throws IOException { + try { + Attributes attrs = sftp.stat(remotePath); + ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path already exists but is not a directory: %s", remotePath); + return; + } catch (SftpException e) { + int status = e.getStatus(); + ValidateUtils.checkTrue(status == SftpConstants.SSH_FX_NO_SUCH_FILE, "Failed to get status of %s: %s", remotePath, e.getMessage()); + } + + int pos = remotePath.lastIndexOf('/'); + ValidateUtils.checkTrue(pos > 0, "No more parents for %s", remotePath); + createDirectories(sftp, remotePath.substring(0, pos)); + } + + protected void transferFile(SftpClient sftp, Path localPath, String remotePath, boolean upload, PrintStream stdout, boolean verbose) throws IOException { + // Create the file's hierarchy + if (upload) { + int pos = remotePath.lastIndexOf('/'); + ValidateUtils.checkTrue(pos > 0, "Missing full remote file path: %s", remotePath); + createDirectories(sftp, remotePath.substring(0, pos)); + } else { + Files.createDirectories(localPath.getParent()); + } + + try (InputStream input = upload ? Files.newInputStream(localPath) : sftp.read(remotePath); + OutputStream output = upload ? sftp.write(remotePath) : Files.newOutputStream(localPath)) { + IoUtils.copy(input, output, SftpClient.IO_BUFFER_SIZE); + } + + if (verbose) { + stdout.append('\t') + .append("Copied ").append(upload ? localPath.toString() : remotePath) + .append(" to ").println(upload ? remotePath : localPath.toString()); + } + } + + protected void transferRemoteDir(SftpClient sftp, Path localPath, String remotePath, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException { + if (attrs.isDirectory()) { + for (DirEntry entry : sftp.readDir(remotePath)) { + String name = entry.getFilename(); + if (".".equals(name) || "..".equals(name)) { + continue; + } + + transferRemoteDir(sftp, localPath.resolve(name), remotePath + "/" + name, entry.getAttributes(), stdout, verbose); + } + } else if (attrs.isRegularFile()) { + transferFile(sftp, localPath, remotePath, false, stdout, verbose); + } else { + if (verbose) { + stdout.append('\t').append("Skip remote special file ").println(remotePath); + } + } + } + + protected void transferLocalDir(SftpClient sftp, Path localPath, String remotePath, PrintStream stdout, boolean verbose) throws IOException { + if (Files.isDirectory(localPath)) { + try (DirectoryStream<Path> ds = Files.newDirectoryStream(localPath)) { + for (Path entry : ds) { + String name = entry.getFileName().toString(); + transferLocalDir(sftp, localPath.resolve(name), remotePath + "/" + name, stdout, verbose); + } + } + } else if (Files.isRegularFile(localPath)) { + transferFile(sftp, localPath, remotePath, true, stdout, verbose); + } else { + if (verbose) { + stdout.append('\t').append("Skip local special file ").println(localPath); + } + } + } + + protected void executeCommand(String args, boolean upload, PrintStream stdout) throws IOException { + String[] comps = GenericUtils.split(args, ' '); + int numArgs = GenericUtils.length(comps); + ValidateUtils.checkTrue((numArgs >= 1) && (numArgs <= 3), "Invalid number of arguments: %s", args); + + String src = comps[0]; + boolean recursive = false; + boolean verbose = false; + int tgtIndex = 1; + if (src.charAt(0) == '-') { + ValidateUtils.checkTrue(src.length() > 1, "Missing flags specification: %s", args); + ValidateUtils.checkTrue(numArgs >= 2, "Missing source specification: %s", args); + + for (int index = 1; index < src.length(); index++) { + char ch = src.charAt(index); + switch(ch) { + case 'r' : + recursive = true; + break; + case 'v': + verbose = true; + break; + default: + throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")"); + } + } + src = comps[1]; + tgtIndex++; + } + + String tgt = (tgtIndex < numArgs) ? comps[tgtIndex] : null; + String localPath; + String remotePath; + if (upload) { + localPath = src; + remotePath = ValidateUtils.checkNotNullAndNotEmpty(tgt, "No remote target specified: %s", args); + } else { + localPath = GenericUtils.isEmpty(tgt) ? getCurrentLocalDirectory() : tgt; + remotePath = src; + } + + SftpClient sftp = getClient(); + Path local = Paths.get(resolveLocalPath(localPath)).normalize().toAbsolutePath(); + String remote = resolveRemotePath(remotePath); + if (recursive) { + if (upload) { + ValidateUtils.checkTrue(Files.isDirectory(local), "Local path not a directory or does not exist: %s", local); + transferLocalDir(sftp, local, remote, stdout, verbose); + } else { + Attributes attrs = sftp.stat(remote); + ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", remote); + transferRemoteDir(sftp, local, remote, attrs, stdout, verbose); + } + } else { + if (Files.exists(local) && Files.isDirectory(local)) { + int pos = remote.lastIndexOf('/'); + String name = (pos >= 0) ? remote.substring(pos + 1) : remote; + local = local.resolve(name); + } + + transferFile(sftp, local, remote, upload, stdout, verbose); + } + } + } + + private class GetCommandExecutor extends TransferCommandExecutor { + GetCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "get"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + executeCommand(args, false, stdout); + return false; + } + } + + private class PutCommandExecutor extends TransferCommandExecutor { + PutCommandExecutor() { + super(); + } + + @Override + public String getName() { + return "put"; + } + + @Override + public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception { + executeCommand(args, true, stdout); + return false; + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java new file mode 100644 index 0000000..abf3a1d --- /dev/null +++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java @@ -0,0 +1,194 @@ +/* + * 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; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.Channel; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry; +import org.apache.sshd.client.subsystem.sftp.SftpClient.Handle; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; + +/** + * Iterates over the available directory entries for a given path. <B>Note:</B> + * if the iteration is carried out until no more entries are available, then + * no need to close the iterator. Otherwise, it is recommended to close it so + * as to release the internal handle. + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class SftpDirEntryIterator extends AbstractLoggingBean implements Iterator<DirEntry>, Channel { + private final AtomicReference<Boolean> eolIndicator = new AtomicReference<>(); + private final AtomicBoolean open = new AtomicBoolean(true); + private final SftpClient client; + private final String dirPath; + private final boolean closeOnFinished; + private Handle dirHandle; + private List<DirEntry> dirEntries; + private int index; + + /** + * @param client The {@link SftpClient} instance to use for the iteration + * @param path The remote directory path + * @throws IOException If failed to gain access to the remote directory path + */ + public SftpDirEntryIterator(SftpClient client, String path) throws IOException { + this(client, path, client.openDir(path), true); + } + + /** + * @param client The {@link SftpClient} instance to use for the iteration + * @param dirHandle The directory {@link Handle} to use for listing the entries + */ + public SftpDirEntryIterator(SftpClient client, Handle dirHandle) { + this(client, Objects.toString(dirHandle, null), dirHandle, false); + } + + /** + * @param client The {@link SftpClient} instance to use for the iteration + * @param path A hint as to the remote directory path - used only for logging + * @param dirHandle The directory {@link Handle} to use for listing the entries + * @param closeOnFinished If {@code true} then close the directory handle when + * all entries have been exhausted + */ + public SftpDirEntryIterator(SftpClient client, String path, Handle dirHandle, boolean closeOnFinished) { + this.client = Objects.requireNonNull(client, "No SFTP client instance"); + this.dirPath = ValidateUtils.checkNotNullAndNotEmpty(path, "No path"); + this.dirHandle = Objects.requireNonNull(dirHandle, "No directory handle"); + this.closeOnFinished = closeOnFinished; + this.dirEntries = load(dirHandle); + } + + /** + * The client instance + * + * @return {@link SftpClient} instance used to access the remote folder + */ + public final SftpClient getClient() { + return client; + } + + /** + * The remotely accessed directory path + * + * @return Remote directory hint - may be the handle's value if accessed directly + * via a {@link Handle} instead of via a path - used only for logging + */ + public final String getPath() { + return dirPath; + } + + /** + * @return The directory {@link Handle} used to access the remote directory + */ + public final Handle getHandle() { + return dirHandle; + } + + @Override + public boolean hasNext() { + return (dirEntries != null) && (index < dirEntries.size()); + } + + @Override + public DirEntry next() { + DirEntry entry = dirEntries.get(index++); + if (index >= dirEntries.size()) { + index = 0; + + try { + dirEntries = load(getHandle()); + } catch (RuntimeException e) { + dirEntries = null; + throw e; + } + } + + return entry; + } + + @Override + public boolean isOpen() { + return open.get(); + } + + public boolean isCloseOnFinished() { + return closeOnFinished; + } + + @Override + public void close() throws IOException { + if (open.getAndSet(false)) { + Handle handle = getHandle(); + if ((handle instanceof Closeable) && isCloseOnFinished()) { + if (log.isDebugEnabled()) { + log.debug("close(" + getPath() + ") handle=" + handle); + } + ((Closeable) handle).close(); + } + } + } + + protected List<DirEntry> load(Handle handle) { + try { + // check if previous call yielded an end-of-list indication + Boolean eolReached = eolIndicator.getAndSet(null); + if ((eolReached != null) && eolReached) { + if (log.isTraceEnabled()) { + log.trace("load({})[{}] exhausted all entries on previous call", getPath(), handle); + } + return null; + } + + List<DirEntry> entries = client.readDir(handle, eolIndicator); + eolReached = eolIndicator.get(); + if ((entries == null) || ((eolReached != null) && eolReached)) { + if (log.isTraceEnabled()) { + log.trace("load({})[{}] exhausted all entries - EOL={}", getPath(), handle, eolReached); + } + close(); + } + + return entries; + } catch (IOException e) { + try { + close(); + } catch (IOException t) { + if (log.isTraceEnabled()) { + log.trace(t.getClass().getSimpleName() + " while close handle=" + handle + + " due to " + e.getClass().getSimpleName() + " [" + e.getMessage() + "]" + + ": " + t.getMessage()); + } + } + throw new RuntimeException(e); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("readDir(" + getPath() + ")[" + getHandle() + "] Iterator#remove() N/A"); + } +} \ No newline at end of file