| /* |
| * 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 < 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> > 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; |
| } |
| } |
| } |