First working version of DCC Receive
authorManuel Nickschas <sputnick@quassel-irc.org>
Wed, 1 Jan 2014 21:54:23 +0000 (22:54 +0100)
committerManuel Nickschas <sputnick@quassel-irc.org>
Wed, 1 Jan 2014 22:15:15 +0000 (23:15 +0100)
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.

src/client/clienttransfer.cpp
src/client/clienttransfer.h
src/common/transfer.cpp
src/common/transfer.h
src/core/coretransfer.cpp
src/core/coretransfer.h

index aab69b3..c6ba555 100644 (file)
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
+#include <QFile>
+
 #include "clienttransfer.h"
 
 INIT_SYNCABLE_OBJECT(ClientTransfer)
 ClientTransfer::ClientTransfer(const QUuid &uuid, QObject *parent)
 #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();
 }
     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:
+            ;
+    }
+}
index b1ca5df..5dc6e04 100644 (file)
@@ -25,6 +25,8 @@
 
 #include "transfer.h"
 
 
 #include "transfer.h"
 
+class QFile;
+
 class ClientTransfer : public Transfer
 {
     Q_OBJECT
 class ClientTransfer : public Transfer
 {
     Q_OBJECT
@@ -40,9 +42,16 @@ public slots:
     void accept(const QString &savePath) const;
     void reject() const;
 
     void accept(const QString &savePath) const;
     void reject() const;
 
+private slots:
+    void dataReceived(PeerPtr peer, const QByteArray &data);
+    void onStateChanged(State state);
+
 private:
 private:
-    // non-synced attributes
+    virtual void cleanUp();
+
     mutable QString _savePath;
     mutable QString _savePath;
+
+    QFile *_file;
 };
 
 #endif
 };
 
 #endif
index e9c81f7..4d71fc1 100644 (file)
@@ -171,3 +171,12 @@ void Transfer::setNick(const QString &nick)
         emit nickChanged(nick);
     }
 }
         emit nickChanged(nick);
     }
 }
+
+
+void Transfer::setError(const QString &errorString)
+{
+    qWarning() << Q_FUNC_INFO << errorString;
+    emit error(errorString);
+    setState(Failed);
+    cleanUp();
+}
index bdbad9f..ce24b8b 100644 (file)
@@ -45,6 +45,7 @@ public:
     enum State {
         New,
         Pending,
     enum State {
         New,
         Pending,
+        Connecting,
         Transferring,
         Paused,
         Completed,
         Transferring,
         Paused,
         Completed,
@@ -90,11 +91,19 @@ signals:
     void fileSizeChanged(quint64 fileSize);
     void nickChanged(const QString &nick);
 
     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;
 
     void accepted(PeerPtr peer = 0) const;
     void rejected(PeerPtr peer = 0) const;
 
-protected:
+protected slots:
     void setState(State state);
     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();
 
 private:
     void init();
index 10d609b..60e3ac7 100644 (file)
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
+#include <QtEndian>
+
+#include <QCoreApplication>
+#include <QTcpSocket>
+
 #include "coretransfer.h"
 
 #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)
 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)
 {
 }
 
 
 void CoreTransfer::requestAccepted(PeerPtr peer)
 {
+    if (_peer || !peer || state() != New)
+        return; // transfer was already accepted
+
+    _peer = peer;
+    setState(Pending);
+
     emit accepted(peer);
     emit accepted(peer);
+
+    // FIXME temporary until we have queueing
+    start();
 }
 
 
 void CoreTransfer::requestRejected(PeerPtr peer)
 {
 }
 
 
 void CoreTransfer::requestRejected(PeerPtr peer)
 {
+    if (_peer || state() != New)
+        return;
+
+    _peer = peer;
+    setState(Rejected);
+
     emit rejected(peer);
 }
     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;
+}
index 9d57db3..c8b83d4 100644 (file)
 #ifndef CORETRANSFER_H
 #define CORETRANSFER_H
 
 #ifndef CORETRANSFER_H
 #define CORETRANSFER_H
 
+#include <QPointer>
+
 #include "transfer.h"
 #include "transfer.h"
+#include "peer.h"
+
+class QTcpSocket;
 
 class CoreTransfer : public Transfer
 {
 
 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:
     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);
 
     // 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> _peer;
+    QTcpSocket *_socket;
+    quint64 _pos;
+    QByteArray _buffer;
+    bool _reading;
 };
 
 #endif
 };
 
 #endif