From: Manuel Nickschas Date: Wed, 1 Jan 2014 21:54:23 +0000 (+0100) Subject: First working version of DCC Receive X-Git-Tag: 0.10-beta1~59 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=e3c42d072b2b8f39c8c9ea44dfc3bded87ae43b0 First working version of DCC Receive This finally allows accepting a file via DCC and storing it on the client side. Note that this is still a very basic implementation, in particular missing * Proper status messages (still claiming to receive unknown CTCP, for example) * Proper error handling * Proper testing * Any kind of configuration * Any kind of UI besides the simple dialog on incoming files * No queuing or any form of transfer management * Things like DCC REVERSE However, it serves as a nice proof-of-concept for the general approach of passing data directly to a particular client using the usual sync mechanism (i.e. no special protocol support or additional connections needed). First client to accept the transfer will get the file. Note that the *core* is the one handling the DCC connection itself, so things like port forwarding or firewall support need to be done core-side. --- diff --git a/src/client/clienttransfer.cpp b/src/client/clienttransfer.cpp index aab69b3b..c6ba5551 100644 --- a/src/client/clienttransfer.cpp +++ b/src/client/clienttransfer.cpp @@ -18,13 +18,26 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ +#include + #include "clienttransfer.h" INIT_SYNCABLE_OBJECT(ClientTransfer) ClientTransfer::ClientTransfer(const QUuid &uuid, QObject *parent) - : Transfer(uuid, parent) + : Transfer(uuid, parent), + _file(0) { + connect(this, SIGNAL(stateChanged(State)), SLOT(onStateChanged(State))); +} + +void ClientTransfer::cleanUp() +{ + if (_file) { + _file->close(); + _file->deleteLater(); + _file = 0; + } } @@ -49,3 +62,41 @@ void ClientTransfer::reject() const REQUEST_OTHER(requestRejected, ARG(ptr)); emit rejected(); } + + +void ClientTransfer::dataReceived(PeerPtr, const QByteArray &data) +{ + // TODO: proper error handling (relay to core) + if (!_file) { + _file = new QFile(_savePath, this); + if (!_file->open(QFile::WriteOnly|QFile::Truncate)) { + qWarning() << Q_FUNC_INFO << "Could not open file:" << _file->errorString(); + return; + } + } + + if (!_file->isOpen()) + return; + + if (_file->write(data) < 0) { + qWarning() << Q_FUNC_INFO << "Could not write to file:" << _file->errorString(); + return; + } +} + + +void ClientTransfer::onStateChanged(Transfer::State state) +{ + switch(state) { + case Completed: + if (_file) + _file->close(); + break; + case Failed: + if (_file) + _file->remove(); + break; + default: + ; + } +} diff --git a/src/client/clienttransfer.h b/src/client/clienttransfer.h index b1ca5df9..5dc6e046 100644 --- a/src/client/clienttransfer.h +++ b/src/client/clienttransfer.h @@ -25,6 +25,8 @@ #include "transfer.h" +class QFile; + class ClientTransfer : public Transfer { Q_OBJECT @@ -40,9 +42,16 @@ public slots: void accept(const QString &savePath) const; void reject() const; +private slots: + void dataReceived(PeerPtr peer, const QByteArray &data); + void onStateChanged(State state); + private: - // non-synced attributes + virtual void cleanUp(); + mutable QString _savePath; + + QFile *_file; }; #endif diff --git a/src/common/transfer.cpp b/src/common/transfer.cpp index e9c81f77..4d71fc1d 100644 --- a/src/common/transfer.cpp +++ b/src/common/transfer.cpp @@ -171,3 +171,12 @@ void Transfer::setNick(const QString &nick) emit nickChanged(nick); } } + + +void Transfer::setError(const QString &errorString) +{ + qWarning() << Q_FUNC_INFO << errorString; + emit error(errorString); + setState(Failed); + cleanUp(); +} diff --git a/src/common/transfer.h b/src/common/transfer.h index bdbad9fc..ce24b8b7 100644 --- a/src/common/transfer.h +++ b/src/common/transfer.h @@ -45,6 +45,7 @@ public: enum State { New, Pending, + Connecting, Transferring, Paused, Completed, @@ -90,11 +91,19 @@ signals: void fileSizeChanged(quint64 fileSize); void nickChanged(const QString &nick); + void error(const QString &errorString); + void accepted(PeerPtr peer = 0) const; void rejected(PeerPtr peer = 0) const; -protected: +protected slots: void setState(State state); + void setError(const QString &errorString); + + // called on the client side through sync calls + virtual void dataReceived(PeerPtr, const QByteArray &data) { Q_UNUSED(data); } + + virtual void cleanUp() = 0; private: void init(); diff --git a/src/core/coretransfer.cpp b/src/core/coretransfer.cpp index 10d609ba..60e3ac74 100644 --- a/src/core/coretransfer.cpp +++ b/src/core/coretransfer.cpp @@ -18,24 +18,171 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ +#include + +#include +#include + #include "coretransfer.h" +const qint64 chunkSize = 16 * 1024; + INIT_SYNCABLE_OBJECT(CoreTransfer) CoreTransfer::CoreTransfer(Direction direction, const QString &nick, const QString &fileName, const QHostAddress &address, quint16 port, quint64 fileSize, QObject *parent) - : Transfer(direction, nick, fileName, address, port, fileSize, parent) + : Transfer(direction, nick, fileName, address, port, fileSize, parent), + _socket(0), + _pos(0), + _reading(false) +{ + +} + + +void CoreTransfer::cleanUp() { + if (_socket) { + _socket->close(); + _socket->deleteLater(); + _socket = 0; + } + _buffer.clear(); + _reading = false; +} + + +void CoreTransfer::onSocketDisconnected() +{ + if (state() == Connecting || state() == Transferring) { + setError(tr("Socket closed while still transferring!")); + } + else + cleanUp(); +} + + +void CoreTransfer::onSocketError(QAbstractSocket::SocketError error) +{ + Q_UNUSED(error) + + if (state() == Connecting || state() == Transferring) { + setError(tr("DCC connection error: %1").arg(_socket->errorString())); + } } void CoreTransfer::requestAccepted(PeerPtr peer) { + if (_peer || !peer || state() != New) + return; // transfer was already accepted + + _peer = peer; + setState(Pending); + emit accepted(peer); + + // FIXME temporary until we have queueing + start(); } void CoreTransfer::requestRejected(PeerPtr peer) { + if (_peer || state() != New) + return; + + _peer = peer; + setState(Rejected); + emit rejected(peer); } + + +void CoreTransfer::start() +{ + if (!_peer || state() != Pending || direction() != Receive) + return; + + setupConnectionForReceive(); +} + + +void CoreTransfer::setupConnectionForReceive() +{ + if (port() == 0) { + setError(tr("Reverse DCC not supported yet!")); + return; + } + + setState(Connecting); + + _socket = new QTcpSocket(this); + connect(_socket, SIGNAL(connected()), SLOT(startReceiving())); + connect(_socket, SIGNAL(disconnected()), SLOT(onSocketDisconnected())); + connect(_socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(onSocketError(QAbstractSocket::SocketError))); + connect(_socket, SIGNAL(readyRead()), SLOT(onDataReceived())); + + _socket->connectToHost(address(), port()); +} + + +void CoreTransfer::startReceiving() +{ + setState(Transferring); +} + + +void CoreTransfer::onDataReceived() +{ + if (_reading) // since we're spinning the event loop, we may get another readyRead() and thus reentrancy + return; + _reading = true; + + while (_socket->bytesAvailable()) { + QByteArray data = _socket->read(chunkSize); + _pos += data.size(); + if (!relayData(data, true)) + return; + + QCoreApplication::processEvents(); // don't block the rest of the core/client communication + if (!_socket) // just in case something happened during spinning the event loop that killed our socket + return; + } + + // Send ack to sender. The DCC protocol only specifies 32 bit values, but modern clients (i.e. those who can send files + // larger than 4 GB) will ignore this anyway... + quint32 ack = qToBigEndian((quint32)_pos);// qDebug() << Q_FUNC_INFO << _pos; + _socket->write((char *)&ack, 4); + + if (_pos > fileSize()) { + qWarning() << "DCC Receive: Got more data than expected!"; + setError(tr("DCC Receive: Got more data than expected!")); + } + else if (_pos == fileSize()) { + qDebug() << "DCC Receive: Transfer finished"; + if (relayData(QByteArray(), false)) // empty buffer + setState(Completed); + } + + _reading = false; +} + + +bool CoreTransfer::relayData(const QByteArray &data, bool requireChunkSize) +{ + // safeguard against a disconnecting quasselclient + if (!_peer) { + setError(tr("DCC Receive: Quassel Client disconnected during transfer!")); + return false; + } + _buffer.append(data); + + // we only want to send data to the client once we have reached the chunksize + if (_buffer.size() > 0 && (_buffer.size() >= chunkSize || !requireChunkSize)) { + SYNC_OTHER(dataReceived, ARG(_peer), ARG(_buffer)); + _buffer.clear(); + } + + return true; +} diff --git a/src/core/coretransfer.h b/src/core/coretransfer.h index 9d57db37..c8b83d49 100644 --- a/src/core/coretransfer.h +++ b/src/core/coretransfer.h @@ -21,7 +21,12 @@ #ifndef CORETRANSFER_H #define CORETRANSFER_H +#include + #include "transfer.h" +#include "peer.h" + +class QTcpSocket; class CoreTransfer : public Transfer { @@ -32,10 +37,28 @@ public: CoreTransfer(Direction direction, const QString &nick, const QString &fileName, const QHostAddress &address, quint16 port, quint64 size = 0, QObject *parent = 0); public slots: + void start(); + // called through sync calls void requestAccepted(PeerPtr peer); void requestRejected(PeerPtr peer); +private slots: + void startReceiving(); + void onDataReceived(); + void onSocketDisconnected(); + void onSocketError(QAbstractSocket::SocketError error); + +private: + void setupConnectionForReceive(); + bool relayData(const QByteArray &data, bool requireChunkSize); + virtual void cleanUp(); + + QPointer _peer; + QTcpSocket *_socket; + quint64 _pos; + QByteArray _buffer; + bool _reading; }; #endif