aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/ch/ethz/ssh2/SFTPv3Client.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/ch/ethz/ssh2/SFTPv3Client.java')
-rw-r--r--src/main/java/ch/ethz/ssh2/SFTPv3Client.java1622
1 files changed, 1622 insertions, 0 deletions
diff --git a/src/main/java/ch/ethz/ssh2/SFTPv3Client.java b/src/main/java/ch/ethz/ssh2/SFTPv3Client.java
new file mode 100644
index 0000000..f3f1843
--- /dev/null
+++ b/src/main/java/ch/ethz/ssh2/SFTPv3Client.java
@@ -0,0 +1,1622 @@
+/*
+ * Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
+ * Please refer to the LICENSE.txt for licensing details.
+ */
+package ch.ethz.ssh2;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import ch.ethz.ssh2.channel.Channel;
+import ch.ethz.ssh2.log.Logger;
+import ch.ethz.ssh2.packets.TypesReader;
+import ch.ethz.ssh2.packets.TypesWriter;
+import ch.ethz.ssh2.sftp.AttribFlags;
+import ch.ethz.ssh2.sftp.ErrorCodes;
+import ch.ethz.ssh2.sftp.Packet;
+
+/**
+ * A <code>SFTPv3Client</code> represents a SFTP (protocol version 3)
+ * client connection tunnelled over a SSH-2 connection. This is a very simple
+ * (synchronous) implementation.
+ * <p/>
+ * Basically, most methods in this class map directly to one of
+ * the packet types described in draft-ietf-secsh-filexfer-02.txt.
+ * <p/>
+ * Note: this is experimental code.
+ * <p/>
+ * Error handling: the methods of this class throw IOExceptions. However, unless
+ * there is catastrophic failure, exceptions of the type {@link SFTPv3Client} will
+ * be thrown (a subclass of IOException). Therefore, you can implement more verbose
+ * behavior by checking if a thrown exception if of this type. If yes, then you
+ * can cast the exception and access detailed information about the failure.
+ * <p/>
+ * Notes about file names, directory names and paths, copy-pasted
+ * from the specs:
+ * <ul>
+ * <li>SFTP v3 represents file names as strings. File names are
+ * assumed to use the slash ('/') character as a directory separator.</li>
+ * <li>File names starting with a slash are "absolute", and are relative to
+ * the root of the file system. Names starting with any other character
+ * are relative to the user's default directory (home directory).</li>
+ * <li>Servers SHOULD interpret a path name component ".." as referring to
+ * the parent directory, and "." as referring to the current directory.
+ * If the server implementation limits access to certain parts of the
+ * file system, it must be extra careful in parsing file names when
+ * enforcing such restrictions. There have been numerous reported
+ * security bugs where a ".." in a path name has allowed access outside
+ * the intended area.</li>
+ * <li>An empty path name is valid, and it refers to the user's default
+ * directory (usually the user's home directory).</li>
+ * </ul>
+ * <p/>
+ * If you are still not tired then please go on and read the comment for
+ * {@link #setCharset(String)}.
+ *
+ * @author Christian Plattner, plattner@inf.ethz.ch
+ * @version $Id: SFTPv3Client.java 46 2011-07-06 08:40:29Z dkocher@sudo.ch $
+ */
+public class SFTPv3Client
+{
+ private static final Logger log = Logger.getLogger(SFTPv3Client.class);
+
+ private Session sess;
+
+ private InputStream is;
+ private OutputStream os;
+
+ private int protocol_version = 0;
+
+ private int next_request_id = 1000;
+
+ private String charsetName = null;
+
+ /**
+ *
+ */
+ private PacketListener listener;
+
+ /**
+ * Create a SFTP v3 client.
+ *
+ * @param conn The underlying SSH-2 connection to be used.
+ * @throws IOException
+ */
+ public SFTPv3Client(Connection conn, PacketListener listener) throws IOException
+ {
+ if (conn == null)
+ {
+ throw new IllegalArgumentException("Cannot accept null argument!");
+ }
+
+ this.listener = listener;
+
+ log.debug("Opening session and starting SFTP subsystem.");
+ sess = conn.openSession();
+ sess.startSubSystem("sftp");
+
+ is = sess.getStdout();
+ os = new BufferedOutputStream(sess.getStdin(), 2048);
+
+ if (is == null)
+ {
+ throw new IOException("There is a problem with the streams of the underlying channel.");
+ }
+
+ init();
+ }
+
+ /**
+ * Create a SFTP v3 client.
+ *
+ * @param conn The underlying SSH-2 connection to be used.
+ * @throws IOException
+ */
+ public SFTPv3Client(Connection conn) throws IOException
+ {
+ this(conn, new PacketListener()
+ {
+ public void read(String packet)
+ {
+ log.debug("Read packet " + packet);
+ }
+
+ public void write(String packet)
+ {
+ log.debug("Write packet " + packet);
+ }
+ });
+ }
+
+ /**
+ * Set the charset used to convert between Java Unicode Strings and byte encodings
+ * used by the server for paths and file names. Unfortunately, the SFTP v3 draft
+ * says NOTHING about such conversions (well, with the exception of error messages
+ * which have to be in UTF-8). Newer drafts specify to use UTF-8 for file names
+ * (if I remember correctly). However, a quick test using OpenSSH serving a EXT-3
+ * filesystem has shown that UTF-8 seems to be a bad choice for SFTP v3 (tested with
+ * filenames containing german umlauts). "windows-1252" seems to work better for Europe.
+ * Luckily, "windows-1252" is the platform default in my case =).
+ * <p/>
+ * If you don't set anything, then the platform default will be used (this is the default
+ * behavior).
+ *
+ * @param charset the name of the charset to be used or <code>null</code> to use the platform's
+ * default encoding.
+ * @throws IOException
+ * @see #getCharset()
+ */
+ public void setCharset(String charset) throws IOException
+ {
+ if (charset == null)
+ {
+ charsetName = charset;
+ return;
+ }
+
+ try
+ {
+ Charset.forName(charset);
+ }
+ catch (UnsupportedCharsetException e)
+ {
+ throw (IOException) new IOException("This charset is not supported").initCause(e);
+ }
+ charsetName = charset;
+ }
+
+ /**
+ * The currently used charset for filename encoding/decoding.
+ *
+ * @return The name of the charset (<code>null</code> if the platform's default charset is being used)
+ * @see #setCharset(String)
+ */
+ public String getCharset()
+ {
+ return charsetName;
+ }
+
+ private void checkHandleValidAndOpen(SFTPv3FileHandle handle) throws IOException
+ {
+ if (handle.client != this)
+ {
+ throw new IOException("The file handle was created with another SFTPv3FileHandle instance.");
+ }
+
+ if (handle.isClosed)
+ {
+ throw new IOException("The file handle is closed.");
+ }
+ }
+
+ private void sendMessage(int type, int requestId, byte[] msg, int off, int len) throws IOException
+ {
+ listener.write(Packet.forName(type));
+
+ int msglen = len + 1;
+
+ if (type != Packet.SSH_FXP_INIT)
+ {
+ msglen += 4;
+ }
+
+ os.write(msglen >> 24);
+ os.write(msglen >> 16);
+ os.write(msglen >> 8);
+ os.write(msglen);
+ os.write(type);
+
+ if (type != Packet.SSH_FXP_INIT)
+ {
+ os.write(requestId >> 24);
+ os.write(requestId >> 16);
+ os.write(requestId >> 8);
+ os.write(requestId);
+ }
+
+ os.write(msg, off, len);
+ os.flush();
+ }
+
+ private void sendMessage(int type, int requestId, byte[] msg) throws IOException
+ {
+ sendMessage(type, requestId, msg, 0, msg.length);
+ }
+
+ private void readBytes(byte[] buff, int pos, int len) throws IOException
+ {
+ while (len > 0)
+ {
+ int count = is.read(buff, pos, len);
+ if (count < 0)
+ {
+ throw new IOException("Unexpected end of sftp stream.");
+ }
+ if ((count == 0) || (count > len))
+ {
+ throw new IOException("Underlying stream implementation is bogus!");
+ }
+ len -= count;
+ pos += count;
+ }
+ }
+
+ /**
+ * Read a message and guarantee that the <b>contents</b> is not larger than
+ * <code>maxlen</code> bytes.
+ * <p/>
+ * Note: receiveMessage(34000) actually means that the message may be up to 34004
+ * bytes (the length attribute preceeding the contents is 4 bytes).
+ *
+ * @param maxlen
+ * @return the message contents
+ * @throws IOException
+ */
+ private byte[] receiveMessage(int maxlen) throws IOException
+ {
+ byte[] msglen = new byte[4];
+
+ readBytes(msglen, 0, 4);
+
+ int len = (((msglen[0] & 0xff) << 24) | ((msglen[1] & 0xff) << 16) | ((msglen[2] & 0xff) << 8) | (msglen[3] & 0xff));
+
+ if ((len > maxlen) || (len <= 0))
+ {
+ throw new IOException("Illegal sftp packet len: " + len);
+ }
+
+ byte[] msg = new byte[len];
+
+ readBytes(msg, 0, len);
+
+ return msg;
+ }
+
+ private int generateNextRequestID()
+ {
+ synchronized (this)
+ {
+ return next_request_id++;
+ }
+ }
+
+ private void closeHandle(byte[] handle) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(handle, 0, handle.length);
+
+ sendMessage(Packet.SSH_FXP_CLOSE, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ private SFTPv3FileAttributes readAttrs(TypesReader tr) throws IOException
+ {
+ /*
+ * uint32 flags
+ * uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE
+ * uint32 uid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID
+ * uint32 gid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID
+ * uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS
+ * uint32 atime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME
+ * uint32 mtime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME
+ * uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED
+ * string extended_type
+ * string extended_data
+ * ... more extended data (extended_type - extended_data pairs),
+ * so that number of pairs equals extended_count
+ */
+
+ SFTPv3FileAttributes fa = new SFTPv3FileAttributes();
+
+ int flags = tr.readUINT32();
+
+ if ((flags & AttribFlags.SSH_FILEXFER_ATTR_SIZE) != 0)
+ {
+ log.debug("SSH_FILEXFER_ATTR_SIZE");
+ fa.size = tr.readUINT64();
+ }
+
+ if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID) != 0)
+ {
+ log.debug("SSH_FILEXFER_ATTR_V3_UIDGID");
+ fa.uid = tr.readUINT32();
+ fa.gid = tr.readUINT32();
+ }
+
+ if ((flags & AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS) != 0)
+ {
+ log.debug("SSH_FILEXFER_ATTR_PERMISSIONS");
+ fa.permissions = tr.readUINT32();
+ }
+
+ if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME) != 0)
+ {
+ log.debug("SSH_FILEXFER_ATTR_V3_ACMODTIME");
+ fa.atime = tr.readUINT32();
+ fa.mtime = tr.readUINT32();
+
+ }
+
+ if ((flags & AttribFlags.SSH_FILEXFER_ATTR_EXTENDED) != 0)
+ {
+ int count = tr.readUINT32();
+
+ log.debug("SSH_FILEXFER_ATTR_EXTENDED (" + count + ")");
+ /* Read it anyway to detect corrupt packets */
+
+ while (count > 0)
+ {
+ tr.readByteString();
+ tr.readByteString();
+ count--;
+ }
+ }
+
+ return fa;
+ }
+
+ /**
+ * Retrieve the file attributes of an open file.
+ *
+ * @param handle a SFTPv3FileHandle handle.
+ * @return a SFTPv3FileAttributes object.
+ * @throws IOException
+ */
+ public SFTPv3FileAttributes fstat(SFTPv3FileHandle handle) throws IOException
+ {
+ checkHandleValidAndOpen(handle);
+
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(handle.fileHandle, 0, handle.fileHandle.length);
+
+ log.debug("Sending SSH_FXP_FSTAT...");
+ sendMessage(Packet.SSH_FXP_FSTAT, req_id, tw.getBytes());
+
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != req_id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t == Packet.SSH_FXP_ATTRS)
+ {
+ return readAttrs(tr);
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+
+ private SFTPv3FileAttributes statBoth(String path, int statMethod) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(path, charsetName);
+
+ log.debug("Sending SSH_FXP_STAT/SSH_FXP_LSTAT...");
+ sendMessage(statMethod, req_id, tw.getBytes());
+
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != req_id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t == Packet.SSH_FXP_ATTRS)
+ {
+ return readAttrs(tr);
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+
+ /**
+ * Retrieve the file attributes of a file. This method
+ * follows symbolic links on the server.
+ *
+ * @param path See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileAttributes object.
+ * @throws IOException
+ * @see #lstat(String)
+ */
+ public SFTPv3FileAttributes stat(String path) throws IOException
+ {
+ return statBoth(path, Packet.SSH_FXP_STAT);
+ }
+
+ /**
+ * Retrieve the file attributes of a file. This method
+ * does NOT follow symbolic links on the server.
+ *
+ * @param path See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileAttributes object.
+ * @throws IOException
+ * @see #stat(String)
+ */
+ public SFTPv3FileAttributes lstat(String path) throws IOException
+ {
+ return statBoth(path, Packet.SSH_FXP_LSTAT);
+ }
+
+ /**
+ * Read the target of a symbolic link. Note: OpenSSH (as of version 4.4) gets very upset
+ * (SSH_FX_BAD_MESSAGE error) if you want to read the target of a file that is not a
+ * symbolic link. Better check first with {@link #lstat(String)}.
+ *
+ * @param path See the {@link SFTPv3Client comment} for the class for more details.
+ * @return The target of the link.
+ * @throws IOException
+ */
+ public String readLink(String path) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(path, charsetName);
+
+ log.debug("Sending SSH_FXP_READLINK...");
+ sendMessage(Packet.SSH_FXP_READLINK, req_id, tw.getBytes());
+
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != req_id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t == Packet.SSH_FXP_NAME)
+ {
+ int count = tr.readUINT32();
+
+ if (count != 1)
+ {
+ throw new IOException("The server sent an invalid SSH_FXP_NAME packet.");
+ }
+
+ return tr.readString(charsetName);
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+
+ private void expectStatusOKMessage(int id) throws IOException
+ {
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+
+ if (errorCode == ErrorCodes.SSH_FX_OK)
+ {
+ return;
+ }
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+
+ /**
+ * Modify the attributes of a file. Used for operations such as changing
+ * the ownership, permissions or access times, as well as for truncating a file.
+ *
+ * @param path See the {@link SFTPv3Client comment} for the class for more details.
+ * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be
+ * made to the attributes of the file. Empty fields will be ignored.
+ * @throws IOException
+ */
+ public void setstat(String path, SFTPv3FileAttributes attr) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(path, charsetName);
+ tw.writeBytes(createAttrs(attr));
+
+ log.debug("Sending SSH_FXP_SETSTAT...");
+ sendMessage(Packet.SSH_FXP_SETSTAT, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ /**
+ * Modify the attributes of a file. Used for operations such as changing
+ * the ownership, permissions or access times, as well as for truncating a file.
+ *
+ * @param handle a SFTPv3FileHandle handle
+ * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be
+ * made to the attributes of the file. Empty fields will be ignored.
+ * @throws IOException
+ */
+ public void fsetstat(SFTPv3FileHandle handle, SFTPv3FileAttributes attr) throws IOException
+ {
+ checkHandleValidAndOpen(handle);
+
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(handle.fileHandle, 0, handle.fileHandle.length);
+ tw.writeBytes(createAttrs(attr));
+
+ log.debug("Sending SSH_FXP_FSETSTAT...");
+ sendMessage(Packet.SSH_FXP_FSETSTAT, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ /**
+ * Create a symbolic link on the server. Creates a link "src" that points
+ * to "target".
+ *
+ * @param src See the {@link SFTPv3Client comment} for the class for more details.
+ * @param target See the {@link SFTPv3Client comment} for the class for more details.
+ * @throws IOException
+ */
+ public void createSymlink(String src, String target) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ /* Either I am too stupid to understand the SFTP draft
+ * or the OpenSSH guys changed the semantics of src and target.
+ */
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(target, charsetName);
+ tw.writeString(src, charsetName);
+
+ log.debug("Sending SSH_FXP_SYMLINK...");
+ sendMessage(Packet.SSH_FXP_SYMLINK, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ /**
+ * Have the server canonicalize any given path name to an absolute path.
+ * This is useful for converting path names containing ".." components or
+ * relative pathnames without a leading slash into absolute paths.
+ *
+ * @param path See the {@link SFTPv3Client comment} for the class for more details.
+ * @return An absolute path.
+ * @throws IOException
+ */
+ public String canonicalPath(String path) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(path, charsetName);
+
+ log.debug("Sending SSH_FXP_REALPATH...");
+ sendMessage(Packet.SSH_FXP_REALPATH, req_id, tw.getBytes());
+
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != req_id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t == Packet.SSH_FXP_NAME)
+ {
+ int count = tr.readUINT32();
+
+ if (count != 1)
+ {
+ throw new IOException("The server sent an invalid SSH_FXP_NAME packet.");
+ }
+
+ final String name = tr.readString(charsetName);
+ listener.read(name);
+ return name;
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+
+ private List<SFTPv3DirectoryEntry> scanDirectory(byte[] handle) throws IOException
+ {
+ List<SFTPv3DirectoryEntry> files = new Vector<SFTPv3DirectoryEntry>();
+
+ while (true)
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(handle, 0, handle.length);
+
+ log.debug("Sending SSH_FXP_READDIR...");
+ sendMessage(Packet.SSH_FXP_READDIR, req_id, tw.getBytes());
+
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != req_id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t == Packet.SSH_FXP_NAME)
+ {
+ int count = tr.readUINT32();
+
+ log.debug("Parsing " + count + " name entries...");
+ while (count > 0)
+ {
+ SFTPv3DirectoryEntry dirEnt = new SFTPv3DirectoryEntry();
+
+ dirEnt.filename = tr.readString(charsetName);
+ dirEnt.longEntry = tr.readString(charsetName);
+ listener.read(dirEnt.longEntry);
+
+ dirEnt.attributes = readAttrs(tr);
+ files.add(dirEnt);
+
+ log.debug("File: '" + dirEnt.filename + "'");
+ count--;
+ }
+ continue;
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+
+ if (errorCode == ErrorCodes.SSH_FX_EOF)
+ {
+ return files;
+ }
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+ }
+
+ public final SFTPv3FileHandle openDirectory(String path) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(path, charsetName);
+
+ log.debug("Sending SSH_FXP_OPENDIR...");
+ sendMessage(Packet.SSH_FXP_OPENDIR, req_id, tw.getBytes());
+
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != req_id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t == Packet.SSH_FXP_HANDLE)
+ {
+ log.debug("Got SSH_FXP_HANDLE.");
+ return new SFTPv3FileHandle(this, tr.readByteString());
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+
+ private String expandString(byte[] b, int off, int len)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < len; i++)
+ {
+ int c = b[off + i] & 0xff;
+
+ if ((c >= 32) && (c <= 126))
+ {
+ sb.append((char) c);
+ }
+ else
+ {
+ sb.append("{0x" + Integer.toHexString(c) + "}");
+ }
+ }
+
+ return sb.toString();
+ }
+
+ private void init() throws IOException
+ {
+ /* Send SSH_FXP_INIT (version 3) */
+
+ final int client_version = 3;
+
+ log.debug("Sending SSH_FXP_INIT (" + client_version + ")...");
+ TypesWriter tw = new TypesWriter();
+ tw.writeUINT32(client_version);
+ sendMessage(Packet.SSH_FXP_INIT, 0, tw.getBytes());
+
+ /* Receive SSH_FXP_VERSION */
+
+ log.debug("Waiting for SSH_FXP_VERSION...");
+ TypesReader tr = new TypesReader(receiveMessage(34000)); /* Should be enough for any reasonable server */
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ if (t != Packet.SSH_FXP_VERSION)
+ {
+ throw new IOException("The server did not send a SSH_FXP_VERSION packet (got " + t + ")");
+ }
+
+ protocol_version = tr.readUINT32();
+
+ log.debug("SSH_FXP_VERSION: protocol_version = " + protocol_version);
+ if (protocol_version != 3)
+ {
+ throw new IOException("Server version " + protocol_version + " is currently not supported");
+ }
+
+ /* Read and save extensions (if any) for later use */
+
+ while (tr.remain() != 0)
+ {
+ String name = tr.readString();
+ listener.read(name);
+ byte[] value = tr.readByteString();
+ log.debug("SSH_FXP_VERSION: extension: " + name + " = '" + expandString(value, 0, value.length) + "'");
+ }
+ }
+
+ /**
+ * Returns the negotiated SFTP protocol version between the client and the server.
+ *
+ * @return SFTP protocol version, i.e., "3".
+ */
+ public int getProtocolVersion()
+ {
+ return protocol_version;
+ }
+
+ /**
+ * Queries the channel state
+ * @return True if the underlying session is in open state
+ */
+ public boolean isConnected() {
+ return sess.getState() == Channel.STATE_OPEN;
+ }
+
+ /**
+ * Close this SFTP session. NEVER forget to call this method to free up
+ * resources - even if you got an exception from one of the other methods.
+ * Sometimes these other methods may throw an exception, saying that the
+ * underlying channel is closed (this can happen, e.g., if the other server
+ * sent a close message.) However, as long as you have not called the
+ * <code>close()</code> method, you are likely wasting resources.
+ */
+ public void close()
+ {
+ sess.close();
+ }
+
+ /**
+ * List the contents of a directory.
+ *
+ * @param dirName See the {@link SFTPv3Client comment} for the class for more details.
+ * @return A Vector containing {@link SFTPv3DirectoryEntry} objects.
+ * @throws IOException
+ */
+ public List<SFTPv3DirectoryEntry> ls(String dirName) throws IOException
+ {
+ SFTPv3FileHandle handle = openDirectory(dirName);
+ List<SFTPv3DirectoryEntry> result = scanDirectory(handle.fileHandle);
+ closeFile(handle);
+ return result;
+ }
+
+ /**
+ * Create a new directory.
+ *
+ * @param dirName See the {@link SFTPv3Client comment} for the class for more details.
+ * @param posixPermissions the permissions for this directory, e.g., "0700" (remember that
+ * this is octal noation). The server will likely apply a umask.
+ * @throws IOException
+ */
+ public void mkdir(String dirName, int posixPermissions) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(dirName, charsetName);
+ tw.writeUINT32(AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS);
+ tw.writeUINT32(posixPermissions);
+
+ sendMessage(Packet.SSH_FXP_MKDIR, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ /**
+ * Remove a file.
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @throws IOException
+ */
+ public void rm(String fileName) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(fileName, charsetName);
+
+ sendMessage(Packet.SSH_FXP_REMOVE, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ /**
+ * Remove an empty directory.
+ *
+ * @param dirName See the {@link SFTPv3Client comment} for the class for more details.
+ * @throws IOException
+ */
+ public void rmdir(String dirName) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(dirName, charsetName);
+
+ sendMessage(Packet.SSH_FXP_RMDIR, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ /**
+ * Move a file or directory.
+ *
+ * @param oldPath See the {@link SFTPv3Client comment} for the class for more details.
+ * @param newPath See the {@link SFTPv3Client comment} for the class for more details.
+ * @throws IOException
+ */
+ public void mv(String oldPath, String newPath) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(oldPath, charsetName);
+ tw.writeString(newPath, charsetName);
+
+ sendMessage(Packet.SSH_FXP_RENAME, req_id, tw.getBytes());
+
+ expectStatusOKMessage(req_id);
+ }
+
+ /**
+ * Open the file for reading.
+ */
+ public static final int SSH_FXF_READ = 0x00000001;
+ /**
+ * Open the file for writing. If both this and SSH_FXF_READ are
+ * specified, the file is opened for both reading and writing.
+ */
+ public static final int SSH_FXF_WRITE = 0x00000002;
+ /**
+ * Force all writes to append data at the end of the file.
+ */
+ public static final int SSH_FXF_APPEND = 0x00000004;
+ /**
+ * If this flag is specified, then a new file will be created if one
+ * does not alread exist (if O_TRUNC is specified, the new file will
+ * be truncated to zero length if it previously exists).
+ */
+ public static final int SSH_FXF_CREAT = 0x00000008;
+ /**
+ * Forces an existing file with the same name to be truncated to zero
+ * length when creating a file by specifying SSH_FXF_CREAT.
+ * SSH_FXF_CREAT MUST also be specified if this flag is used.
+ */
+ public static final int SSH_FXF_TRUNC = 0x00000010;
+ /**
+ * Causes the request to fail if the named file already exists.
+ */
+ public static final int SSH_FXF_EXCL = 0x00000020;
+
+ /**
+ * Open a file for reading.
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle openFileRO(String fileName) throws IOException
+ {
+ return openFile(fileName, SSH_FXF_READ, null);
+ }
+
+ /**
+ * Open a file for reading and writing.
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle openFileRW(String fileName) throws IOException
+ {
+ return openFile(fileName, SSH_FXF_READ | SSH_FXF_WRITE, null);
+ }
+
+ /**
+ * Open a file in append mode. The SFTP v3 draft says nothing but assuming normal POSIX
+ * behavior, all writes will be appendend to the end of the file, no matter which offset
+ * one specifies.
+ * <p/>
+ * A side note for the curious: OpenSSH does an lseek() to the specified writing offset before each write(),
+ * even for writes to files opened in O_APPEND mode. However, bear in mind that when working
+ * in the O_APPEND mode, each write() includes an implicit lseek() to the end of the file
+ * (well, this is what the newsgroups say).
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle openFileRWAppend(String fileName) throws IOException
+ {
+ return openFile(fileName, SSH_FXF_READ | SSH_FXF_WRITE | SSH_FXF_APPEND, null);
+ }
+
+ /**
+ * Open a file in append mode. The SFTP v3 draft says nothing but assuming normal POSIX
+ * behavior, all writes will be appendend to the end of the file, no matter which offset
+ * one specifies.
+ * <p/>
+ * A side note for the curious: OpenSSH does an lseek() to the specified writing offset before each write(),
+ * even for writes to files opened in O_APPEND mode. However, bear in mind that when working
+ * in the O_APPEND mode, each write() includes an implicit lseek() to the end of the file
+ * (well, this is what the newsgroups say).
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle openFileWAppend(String fileName) throws IOException
+ {
+ return openFile(fileName, SSH_FXF_WRITE | SSH_FXF_APPEND, null);
+ }
+
+ /**
+ * Create a file and open it for reading and writing.
+ * Same as {@link #createFile(String, SFTPv3FileAttributes) createFile(fileName, null)}.
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle createFile(String fileName) throws IOException
+ {
+ return createFile(fileName, null);
+ }
+
+ /**
+ * Create a file and open it for reading and writing.
+ * You can specify the default attributes of the file (the server may or may
+ * not respect your wishes).
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @param attr may be <code>null</code> to use server defaults. Probably only
+ * the <code>uid</code>, <code>gid</code> and <code>permissions</code>
+ * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle}
+ * structure make sense. You need only to set those fields where you want
+ * to override the server's defaults.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle createFile(String fileName, SFTPv3FileAttributes attr) throws IOException
+ {
+ return openFile(fileName, SSH_FXF_CREAT | SSH_FXF_READ | SSH_FXF_WRITE, attr);
+ }
+
+ /**
+ * Create a file (truncate it if it already exists) and open it for writing.
+ * Same as {@link #createFileTruncate(String, SFTPv3FileAttributes) createFileTruncate(fileName, null)}.
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle createFileTruncate(String fileName) throws IOException
+ {
+ return createFileTruncate(fileName, null);
+ }
+
+ /**
+ * reate a file (truncate it if it already exists) and open it for writing.
+ * You can specify the default attributes of the file (the server may or may
+ * not respect your wishes).
+ *
+ * @param fileName See the {@link SFTPv3Client comment} for the class for more details.
+ * @param attr may be <code>null</code> to use server defaults. Probably only
+ * the <code>uid</code>, <code>gid</code> and <code>permissions</code>
+ * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle}
+ * structure make sense. You need only to set those fields where you want
+ * to override the server's defaults.
+ * @return a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public SFTPv3FileHandle createFileTruncate(String fileName, SFTPv3FileAttributes attr) throws IOException
+ {
+ return openFile(fileName, SSH_FXF_CREAT | SSH_FXF_TRUNC | SSH_FXF_WRITE, attr);
+ }
+
+ private byte[] createAttrs(SFTPv3FileAttributes attr)
+ {
+ TypesWriter tw = new TypesWriter();
+
+ int attrFlags = 0;
+
+ if (attr == null)
+ {
+ tw.writeUINT32(0);
+ }
+ else
+ {
+ if (attr.size != null)
+ {
+ attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_SIZE;
+ }
+
+ if ((attr.uid != null) && (attr.gid != null))
+ {
+ attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID;
+ }
+
+ if (attr.permissions != null)
+ {
+ attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS;
+ }
+
+ if ((attr.atime != null) && (attr.mtime != null))
+ {
+ attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME;
+ }
+
+ tw.writeUINT32(attrFlags);
+
+ if (attr.size != null)
+ {
+ tw.writeUINT64(attr.size);
+ }
+
+ if ((attr.uid != null) && (attr.gid != null))
+ {
+ tw.writeUINT32(attr.uid);
+ tw.writeUINT32(attr.gid);
+ }
+
+ if (attr.permissions != null)
+ {
+ tw.writeUINT32(attr.permissions);
+ }
+
+ if ((attr.atime != null) && (attr.mtime != null))
+ {
+ tw.writeUINT32(attr.atime);
+ tw.writeUINT32(attr.mtime);
+ }
+ }
+
+ return tw.getBytes();
+ }
+
+ public SFTPv3FileHandle openFile(String fileName, int flags, SFTPv3FileAttributes attr) throws IOException
+ {
+ int req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(fileName, charsetName);
+ tw.writeUINT32(flags);
+ tw.writeBytes(createAttrs(attr));
+
+ log.debug("Sending SSH_FXP_OPEN...");
+ sendMessage(Packet.SSH_FXP_OPEN, req_id, tw.getBytes());
+
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ int rep_id = tr.readUINT32();
+ if (rep_id != req_id)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ if (t == Packet.SSH_FXP_HANDLE)
+ {
+ log.debug("Got SSH_FXP_HANDLE.");
+ return new SFTPv3FileHandle(this, tr.readByteString());
+ }
+
+ if (t != Packet.SSH_FXP_STATUS)
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ int errorCode = tr.readUINT32();
+ String errorMessage = tr.readString();
+ listener.read(errorMessage);
+ throw new SFTPException(errorMessage, errorCode);
+ }
+
+ /**
+ * A read is divided into multiple requests sent sequentially before
+ * reading any status from the server
+ */
+ private static class OutstandingReadRequest
+ {
+ int req_id;
+ /**
+ * Read offset to request on server starting at the file offset for the first request.
+ */
+ long serverOffset;
+ /**
+ * Length of requested data
+ */
+ int len;
+ /**
+ * Offset in destination buffer
+ */
+ int dstOffset;
+ /**
+ * Temporary buffer
+ */
+ byte[] buffer;
+ }
+
+ private void sendReadRequest(int id, SFTPv3FileHandle handle, long offset, int len) throws IOException
+ {
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(handle.fileHandle, 0, handle.fileHandle.length);
+ tw.writeUINT64(offset);
+ tw.writeUINT32(len);
+
+ log.debug("Sending SSH_FXP_READ (" + id + ") " + offset + "/" + len);
+ sendMessage(Packet.SSH_FXP_READ, id, tw.getBytes());
+ }
+
+ /**
+ * Parallel read requests maximum size.
+ */
+ private static final int DEFAULT_MAX_PARALLELISM = 64;
+
+ /**
+ * Parallel read requests.
+ */
+ private int parallelism = DEFAULT_MAX_PARALLELISM;
+
+ /**
+ * @param parallelism
+ */
+ public void setRequestParallelism(int parallelism)
+ {
+ this.parallelism = Math.min(parallelism, DEFAULT_MAX_PARALLELISM);
+ }
+
+ /**
+ * Mapping request ID to request.
+ */
+ Map<Integer, OutstandingReadRequest> pendingReadQueue
+ = new HashMap<Integer, OutstandingReadRequest>();
+
+ /**
+ * Read bytes from a file in a parallel fashion. As many bytes as you want will be read.
+ * <p/>
+ * <ul>
+ * <li>The server will read as many bytes as it can from the file (up to <code>len</code>),
+ * and return them.</li>
+ * <li>If EOF is encountered before reading any data, <code>-1</code> is returned.
+ * <li>If an error occurs, an exception is thrown</li>.
+ * <li>For normal disk files, it is guaranteed that the server will return the specified
+ * number of bytes, or up to end of file. For, e.g., device files this may return
+ * fewer bytes than requested.</li>
+ * </ul>
+ *
+ * @param handle a SFTPv3FileHandle handle
+ * @param fileOffset offset (in bytes) in the file
+ * @param dst the destination byte array
+ * @param dstoff offset in the destination byte array
+ * @param len how many bytes to read, 0 &lt; len
+ * @return the number of bytes that could be read, may be less than requested if
+ * the end of the file is reached, -1 is returned in case of <code>EOF</code>
+ * @throws IOException
+ */
+ public int read(SFTPv3FileHandle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException
+ {
+ boolean errorOccured = false;
+
+ checkHandleValidAndOpen(handle);
+
+ int remaining = len * parallelism;
+ int clientOffset = dstoff;
+
+ long serverOffset = fileOffset;
+ for (OutstandingReadRequest r : pendingReadQueue.values())
+ {
+ // Server offset should take pending requests into account.
+ serverOffset += r.len;
+ }
+
+ while (true)
+ {
+ // Stop if there was an error and no outstanding request
+ if ((pendingReadQueue.size() == 0) && errorOccured)
+ {
+ break;
+ }
+
+ // Send as many requests as we are allowed to
+ while (pendingReadQueue.size() < parallelism)
+ {
+ if (errorOccured)
+ {
+ break;
+ }
+ // Send the next read request
+ OutstandingReadRequest req = new OutstandingReadRequest();
+ req.req_id = generateNextRequestID();
+ req.serverOffset = serverOffset;
+ req.len = (remaining > len) ? len : remaining;
+ req.buffer = dst;
+ req.dstOffset = dstoff;
+
+ serverOffset += req.len;
+ clientOffset += req.len;
+ remaining -= req.len;
+
+ sendReadRequest(req.req_id, handle, req.serverOffset, req.len);
+
+ pendingReadQueue.put(req.req_id, req);
+ }
+ if (pendingReadQueue.size() == 0)
+ {
+ break;
+ }
+
+ // Receive a single answer
+ byte[] resp = receiveMessage(34000);
+ TypesReader tr = new TypesReader(resp);
+
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ // Search the pending queue
+ OutstandingReadRequest req = pendingReadQueue.remove(tr.readUINT32());
+ if (null == req)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+ // Evaluate the answer
+ if (t == Packet.SSH_FXP_STATUS)
+ {
+ /* In any case, stop sending more packets */
+
+ int code = tr.readUINT32();
+ String msg = tr.readString();
+ listener.read(msg);
+
+ if (log.isDebugEnabled())
+ {
+ String[] desc = ErrorCodes.getDescription(code);
+ log.debug("Got SSH_FXP_STATUS (" + req.req_id + ") (" + ((desc != null) ? desc[0] : "UNKNOWN") + ")");
+ }
+ // Flag to read all pending requests but don't send any more.
+ errorOccured = true;
+ if (pendingReadQueue.isEmpty())
+ {
+ if (ErrorCodes.SSH_FX_EOF == code)
+ {
+ return -1;
+ }
+ throw new SFTPException(msg, code);
+ }
+ }
+ else if (t == Packet.SSH_FXP_DATA)
+ {
+ // OK, collect data
+ int readLen = tr.readUINT32();
+
+ if ((readLen < 0) || (readLen > req.len))
+ {
+ throw new IOException("The server sent an invalid length field in a SSH_FXP_DATA packet.");
+ }
+
+ if (log.isDebugEnabled())
+ {
+ log.debug("Got SSH_FXP_DATA (" + req.req_id + ") " + req.serverOffset + "/" + readLen
+ + " (requested: " + req.len + ")");
+ }
+
+ // Read bytes into buffer
+ tr.readBytes(req.buffer, req.dstOffset, readLen);
+
+ if (readLen < req.len)
+ {
+ /* Send this request packet again to request the remaing data in this slot. */
+ req.req_id = generateNextRequestID();
+ req.serverOffset += readLen;
+ req.len -= readLen;
+
+ log.debug("Requesting again: " + req.serverOffset + "/" + req.len);
+ sendReadRequest(req.req_id, handle, req.serverOffset, req.len);
+
+ pendingReadQueue.put(req.req_id, req);
+ }
+ return readLen;
+ }
+ else
+ {
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+ }
+ // Should never reach here.
+ throw new SFTPException("No EOF reached", -1);
+ }
+
+ /**
+ * A read is divided into multiple requests sent sequentially before
+ * reading any status from the server
+ */
+ private static class OutstandingStatusRequest
+ {
+ int req_id;
+ }
+
+ /**
+ * Mapping request ID to request.
+ */
+ Map<Integer, OutstandingStatusRequest> pendingStatusQueue
+ = new HashMap<Integer, OutstandingStatusRequest>();
+
+ /**
+ * Write bytes to a file. If <code>len</code> &gt; 32768, then the write operation will
+ * be split into multiple writes.
+ *
+ * @param handle a SFTPv3FileHandle handle.
+ * @param fileOffset offset (in bytes) in the file.
+ * @param src the source byte array.
+ * @param srcoff offset in the source byte array.
+ * @param len how many bytes to write.
+ * @throws IOException
+ */
+ public void write(SFTPv3FileHandle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException
+ {
+ checkHandleValidAndOpen(handle);
+
+ // Send the next write request
+ OutstandingStatusRequest req = new OutstandingStatusRequest();
+ req.req_id = generateNextRequestID();
+
+ TypesWriter tw = new TypesWriter();
+ tw.writeString(handle.fileHandle, 0, handle.fileHandle.length);
+ tw.writeUINT64(fileOffset);
+ tw.writeString(src, srcoff, len);
+
+ log.debug("Sending SSH_FXP_WRITE...");
+ sendMessage(Packet.SSH_FXP_WRITE, req.req_id, tw.getBytes());
+
+ pendingStatusQueue.put(req.req_id, req);
+
+ // Only read next status if parallelism reached
+ while (pendingStatusQueue.size() >= parallelism)
+ {
+ this.readStatus();
+ }
+ }
+
+ private void readStatus() throws IOException
+ {
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ // Search the pending queue
+ OutstandingStatusRequest status = pendingStatusQueue.remove(tr.readUINT32());
+ if (null == status)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ // Evaluate the answer
+ if (t == Packet.SSH_FXP_STATUS)
+ {
+ // In any case, stop sending more packets
+ int code = tr.readUINT32();
+ if (log.isDebugEnabled())
+ {
+ String[] desc = ErrorCodes.getDescription(code);
+ log.debug("Got SSH_FXP_STATUS (" + status.req_id + ") (" + ((desc != null) ? desc[0] : "UNKNOWN") + ")");
+ }
+ if (code == ErrorCodes.SSH_FX_OK)
+ {
+ return;
+ }
+ String msg = tr.readString();
+ listener.read(msg);
+ throw new SFTPException(msg, code);
+ }
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ private void readPendingReadStatus() throws IOException
+ {
+ byte[] resp = receiveMessage(34000);
+
+ TypesReader tr = new TypesReader(resp);
+ int t = tr.readByte();
+ listener.read(Packet.forName(t));
+
+ // Search the pending queue
+ OutstandingReadRequest status = pendingReadQueue.remove(tr.readUINT32());
+ if (null == status)
+ {
+ throw new IOException("The server sent an invalid id field.");
+ }
+
+ // Evaluate the answer
+ if (t == Packet.SSH_FXP_STATUS)
+ {
+ // In any case, stop sending more packets
+ int code = tr.readUINT32();
+ if (log.isDebugEnabled())
+ {
+ String[] desc = ErrorCodes.getDescription(code);
+ log.debug("Got SSH_FXP_STATUS (" + status.req_id + ") (" + ((desc != null) ? desc[0] : "UNKNOWN") + ")");
+ }
+ if (code == ErrorCodes.SSH_FX_OK)
+ {
+ return;
+ }
+ if (code == ErrorCodes.SSH_FX_EOF)
+ {
+ return;
+ }
+ String msg = tr.readString();
+ listener.read(msg);
+ throw new SFTPException(msg, code);
+ }
+ throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")");
+ }
+
+ /**
+ * Close a file.
+ *
+ * @param handle a SFTPv3FileHandle handle
+ * @throws IOException
+ */
+ public void closeFile(SFTPv3FileHandle handle) throws IOException
+ {
+ try
+ {
+ while (!pendingReadQueue.isEmpty())
+ {
+ this.readPendingReadStatus();
+ }
+ while (!pendingStatusQueue.isEmpty())
+ {
+ this.readStatus();
+ }
+ if (!handle.isClosed)
+ {
+ closeHandle(handle.fileHandle);
+ }
+ }
+ finally
+ {
+ handle.isClosed = true;
+ }
+ }
+}