package org.dcache.ftp.data;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import org.dcache.pool.repository.RepositoryChannel;
/**
* Implementation of MODE E.
*
* Be aware that it is quite easy to introduce race conditions, so
* please keep this in mind when making changes. In particular the EOD
* and EOD count handling is a little tricky.
*/
public class ModeE extends Mode
{
/**
* Header length of a mode E block.
*/
public static final int HEADER_LENGTH = 17;
public static final int EOR_DESCRIPTOR = 128;
public static final int EOF_DESCRIPTOR = 64;
public static final int SUSPECTED_ERROR_DESCRIPTOR = 32;
public static final int RESTART_MARKER_DESCRIPTOR = 16;
public static final int EOD_DESCRIPTOR = 8;
public static final int SENDER_CLOSES_THIS_STREAM_DESCRIPTOR = 4;
public static final int KNOWN_DESCRIPTORS =
EOF_DESCRIPTOR | EOD_DESCRIPTOR | SENDER_CLOSES_THIS_STREAM_DESCRIPTOR;
/**
* The chunk size used when sending files.
*
* Large blocks will reduce the overhead of sending. However, it
* case of multible concurrent streams, large blocks will make
* disk access less sequential on both the sending and receiving
* side.
*/
private final int _blockSize;
/** Position in file when sending data. Used by the sender. */
private long _currentPosition;
/**
* Number of bytes that have to be transferred. Used by the sender.
*/
private long _currentCount;
/**
* EOD count received. Zero as long as no EOD count was received.
*/
private long _eodc;
/**
* Implementation of send in mode E. There will be an instance per
* data channel. The sender repeatedly bites _blockSize bytes of
* the file and transfers it as a single block. I.e.
* _currentPosition is incremented by _blockSize bytes at a time.
*/
private class Sender extends AbstractMultiplexerListener
{
protected static final int PREPARE_BLOCK = 0;
protected static final int SEND_HEADER = 1;
protected static final int SEND_DATA = 2;
/** Socket used for data channel. */
protected final SocketChannel _socket;
/** State of the sender. */
protected int _state;
/** Position in file, which we will send next. */
protected long _position;
/** Bytes remaining from current block. */
protected long _count;
/** True if this sender must send the EOF. */
protected final boolean _sendEOF;
/** Buffer for sending the block header. */
protected final ByteBuffer _header =
ByteBuffer.allocate(HEADER_LENGTH);
public Sender(SocketChannel socket) {
_socket = socket;
_state = PREPARE_BLOCK;
_sendEOF = (_opened == 1); // First sender sends EOF
}
@Override
public void register(Multiplexer multiplexer) throws IOException
{
multiplexer.register(this, SelectionKey.OP_WRITE, _socket);
}
@Override
public void write(Multiplexer multiplexer, SelectionKey key)
throws IOException, FTPException
{
switch (_state) {
case PREPARE_BLOCK:
/* Prepare new block. We 'bite' up to _blockSize bytes
* of the file and reserve it for this data channel.
*/
_position = _currentPosition;
_count = Math.min(_currentCount, _blockSize);
_currentPosition += _count;
_currentCount -= _count;
/* Prepare header.
*/
_header.clear();
if (_count > 0) {
// Regular block.
_header.put((byte)0);
_header.putLong(_count); // Count
_header.putLong(_position); // Position
} else if (_sendEOF) {
/* This would fail if we are a passive
* sender. Luckily, senders are never passive.
*/
if (!waitForConnectionCompletion(key)) {
return;
}
// Send EOD and EOD count. Since all connections
// have been established by now, we know that
// _opened is the actual number of connections
// that have been established.
_header.put((byte)(EOF_DESCRIPTOR | EOD_DESCRIPTOR | SENDER_CLOSES_THIS_STREAM_DESCRIPTOR));
_header.putLong(0); // Unused
_header.putLong(_opened); // EOD count
} else {
// No more data. Send EOD.
_header.put((byte)(EOD_DESCRIPTOR | SENDER_CLOSES_THIS_STREAM_DESCRIPTOR));
_header.putLong(0); // Count
_header.putLong(0); // Position
}
_header.flip();
_state = SEND_HEADER;
case SEND_HEADER:
/* Send header.
*/
_socket.write(_header);
if (_header.position() < _header.limit()) {
break;
}
/* If at end of stream, close the channel.
*
* Notice that we allow close() to shut down the
* multiplexer if all connections have been closed
* (the third argument is true). This is valid because
* the first sender being created does not close the
* connection until it has sent EOF, and it does not
* sent EOF until all connections have been
* established.
*/
if (_count == 0) {
close(multiplexer, key, true);
break;
}
_state = SEND_DATA;
case SEND_DATA:
/* Send data.
*/
long nbytes = transferTo(_position, _count, _socket);
_monitor.sentBlock(_position, nbytes);
_position += nbytes;
_count -= nbytes;
if (_count == 0) {
_state = PREPARE_BLOCK;
}
break;
}
}
}
/**
* Implementation of receive in mode E. There will be an instance
* per data channel.
*/
class Receiver extends AbstractMultiplexerListener
{
/** Socket used for data channel. */
protected final SocketChannel _socket;
/** Number of bytes left of current block. */
protected long _count;
/** The file position at which we will receive data next. */
protected long _position;
/** Header flags from the current block. */
protected int _flags;
/** True if any data has flown over this data channel. */
protected boolean _used;
/** Buffer for receiving the block header. */
protected final ByteBuffer _header =
ByteBuffer.allocate(HEADER_LENGTH);
public Receiver(SocketChannel socket) {
_socket = socket;
_count = 0;
_position = 0;
_flags = 0;
_used = false;
}
@Override
public void register(Multiplexer multiplexer) throws IOException
{
multiplexer.register(this, SelectionKey.OP_READ, _socket);
}
@Override
public void read(Multiplexer multiplexer, SelectionKey key)
throws IOException, FTPException, InterruptedException
{
/* _count is zero when we have received all of the
* previous block. We expect to read the header of the
* next block.
*/
if (_count == 0) {
long nbytes = _socket.read(_header);
if (nbytes == -1) {
/* Stream was closed. The GridFTP 1 spec states
* that the sender must send EOD when no more data
* is to be send on a channel. However, the Globus
* GridFTP client library seems to consider it
* acceptable to close a data channel without
* sending EOD as long as it has not transferred
* any data on the channel. (REVISIT: Is this
* actually the case?)
*/
if (_used) {
throw new FTPException("Stream ended before EOD");
}
close(multiplexer, key, _opened == _eodc);
return;
}
_used = true;
if (_header.position() < _header.limit()) {
/* Incomplete header.
*/
return;
}
_header.rewind();
_flags = _header.get();
_count = _header.getLong();
_position = _header.getLong();
_header.clear();
/* The GridFTP spec states that we should generate an
* error whenever we receive a descriptor we don't
* know how to handle.
*/
if ((_flags & ~KNOWN_DESCRIPTORS) != 0) {
throw new FTPException("Received block with unknown descriptor (" + _flags + ")");
}
/* Exactly one EOF must be received on one of the data
* channels. It contains the number of EOD markers
* that must be received. Such blocks have a different
* interpretation of the two fields following the
* descriptor. Therefore these blocks cannot contain data.
*
* The GridFTP spec is not clear about EOF being
* received after EOD. As far as I can see, there
* would be a race condition if EOF after EOD were
* allowed when caching data channels: We have two
* channels A and B. We send EOD on both, and then
* EOF(2) on A. Now sender considers the transfer to
* be completed and initiates a new transfer on cached
* data channels A and B, however the file is so small
* that only an EOF is sent on B. It may now happen
* that the receiver sees an EOF on both channels and
* is not able to distinguish them. I therefore
* consider EOF after EOD to be disallowed. REMARK: We
* do not support caching of data channels.
*
* The GridFTP spec is also not clear about data being
* send after EOF, however we handle that case.
*/
if ((_flags & EOF_DESCRIPTOR) != 0) {
if (_eodc != 0) {
throw new FTPException("Multible EODC received");
}
if (_position <= 0) {
throw new FTPException("Non-positive EODC received");
}
_eodc = (int)_position;
_count = _position = 0; // No data
}
/* transferFrom does not like empty reads. Therefore
* we exit early in case of empty blocks.
*/
if (_count == 0) {
/* If EOD was received, then close channel.
*/
if ((_flags & EOD_DESCRIPTOR) != 0) {
close(multiplexer, key, _opened == _eodc);
}
return;
}
_monitor.preallocate(_position + _count);
}
/* Receive data.
*/
long nbytes = transferFrom(_socket, _position, _count);
if (nbytes == -1) {
throw new FTPException("Stream was closed in the middle of a block");
}
_monitor.receivedBlock(_position, nbytes);
_position += nbytes;
_count -= nbytes;
/* If EOD was received, then close channel.
*/
if (_count == 0 && (_flags & EOD_DESCRIPTOR) != 0) {
close(multiplexer, key, _opened == _eodc);
}
}
}
public ModeE(Role role, RepositoryChannel file, ConnectionMonitor monitor,
int blockSize)
throws IOException
{
super(role, file, monitor);
_currentPosition = getStartPosition();
_currentCount = getSize();
_eodc = 0;
_blockSize = blockSize;
}
@Override
public void setPartialRetrieveParameters(long position, long size)
{
super.setPartialRetrieveParameters(position, size);
_currentPosition = getStartPosition();
_currentCount = getSize();
}
@Override
public void newConnection(Multiplexer multiplexer, SocketChannel socket)
throws IOException
{
switch (_role) {
case Sender:
multiplexer.add(new Sender(socket));
break;
case Receiver:
multiplexer.add(new Receiver(socket));
break;
}
}
}