You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2018/04/16 11:48:02 UTC
[15/30] mina-sshd git commit: [SSHD-815] Extract SFTP in its own
module
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:dev@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:dev@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:dev@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