qt 6.5.1 original

This commit is contained in:
kleuter
2023-10-29 23:33:08 +01:00
parent 71d22ab6b0
commit 85d238dfda
21202 changed files with 5499099 additions and 0 deletions

View File

@ -0,0 +1,68 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.16)
project(torrent LANGUAGES CXX)
if(NOT DEFINED INSTALL_EXAMPLESDIR)
set(INSTALL_EXAMPLESDIR "examples")
endif()
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/network/torrent")
find_package(Qt6 REQUIRED COMPONENTS Core Gui Network Widgets)
qt_standard_project_setup()
qt_add_executable(torrent
addtorrentdialog.cpp addtorrentdialog.h
addtorrentform.ui
bencodeparser.cpp bencodeparser.h
connectionmanager.cpp connectionmanager.h
filemanager.cpp filemanager.h
main.cpp
mainwindow.cpp mainwindow.h
metainfo.cpp metainfo.h
peerwireclient.cpp peerwireclient.h
ratecontroller.cpp ratecontroller.h
torrentclient.cpp torrentclient.h
torrentserver.cpp torrentserver.h
trackerclient.cpp trackerclient.h
)
set_target_properties(torrent PROPERTIES
WIN32_EXECUTABLE TRUE
MACOSX_BUNDLE TRUE
)
target_link_libraries(torrent PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Network
Qt6::Widgets
)
# Resources:
set(icons_resource_files
"icons/1downarrow.png"
"icons/1uparrow.png"
"icons/bottom.png"
"icons/exit.png"
"icons/peertopeer.png"
"icons/player_pause.png"
"icons/player_play.png"
"icons/player_stop.png"
)
qt_add_resources(torrent "icons"
PREFIX
"/"
FILES
${icons_resource_files}
)
install(TARGETS torrent
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)

View File

@ -0,0 +1,131 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "addtorrentdialog.h"
#include "metainfo.h"
#include <QFile>
#include <QFileDialog>
#include <QLineEdit>
#include <QMetaObject>
static QString stringNumber(qint64 number)
{
if (number > (1024 * 1024 * 1024))
return QString::asprintf("%.2fGB", number / (1024.0 * 1024.0 * 1024.0));
else if (number > (1024 * 1024))
return QString::asprintf("%.2fMB", number / (1024.0 * 1024.0));
else if (number > (1024))
return QString::asprintf("%.2fKB", number / (1024.0));
else
return QString::asprintf("%d bytes", int(number));
}
AddTorrentDialog::AddTorrentDialog(QWidget *parent)
: QDialog(parent, Qt::Sheet)
{
ui.setupUi(this);
connect(ui.browseTorrents, &QPushButton::clicked,
this, &AddTorrentDialog::selectTorrent);
connect(ui.browseDestination, &QPushButton::clicked,
this, &AddTorrentDialog::selectDestination);
connect(ui.torrentFile, &QLineEdit::textChanged,
this, &AddTorrentDialog::setTorrent);
ui.destinationFolder->setText(destinationDirectory = QDir::current().path());
ui.torrentFile->setFocus();
}
void AddTorrentDialog::selectTorrent()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Choose a torrent file"),
lastDirectory,
tr("Torrents (*.torrent);; All files (*.*)"));
if (fileName.isEmpty())
return;
lastDirectory = QFileInfo(fileName).absolutePath();
setTorrent(fileName);
}
void AddTorrentDialog::selectDestination()
{
QString dir = QFileDialog::getExistingDirectory(this, tr("Choose a destination directory"),
lastDestinationDirectory);
if (dir.isEmpty())
return;
lastDestinationDirectory = destinationDirectory = dir;
ui.destinationFolder->setText(destinationDirectory);
enableOkButton();
}
void AddTorrentDialog::enableOkButton()
{
ui.okButton->setEnabled(!ui.destinationFolder->text().isEmpty()
&& !ui.torrentFile->text().isEmpty());
}
void AddTorrentDialog::setTorrent(const QString &torrentFile)
{
if (torrentFile.isEmpty()) {
enableOkButton();
return;
}
ui.torrentFile->setText(torrentFile);
if (!torrentFile.isEmpty())
lastDirectory = QFileInfo(torrentFile).absolutePath();
if (lastDestinationDirectory.isEmpty())
lastDestinationDirectory = lastDirectory;
MetaInfo metaInfo;
QFile torrent(torrentFile);
if (!torrent.open(QFile::ReadOnly) || !metaInfo.parse(torrent.readAll())) {
enableOkButton();
return;
}
ui.torrentFile->setText(torrentFile);
ui.announceUrl->setText(metaInfo.announceUrl());
if (metaInfo.comment().isEmpty())
ui.commentLabel->setText("<unknown>");
else
ui.commentLabel->setText(metaInfo.comment());
if (metaInfo.createdBy().isEmpty())
ui.creatorLabel->setText("<unknown>");
else
ui.creatorLabel->setText(metaInfo.createdBy());
ui.sizeLabel->setText(stringNumber(metaInfo.totalSize()));
if (metaInfo.fileForm() == MetaInfo::SingleFileForm) {
ui.torrentContents->setHtml(metaInfo.singleFile().name);
} else {
QString html;
const QList<MetaInfoMultiFile> multiFiles = metaInfo.multiFiles();
for (const MetaInfoMultiFile &file : multiFiles) {
QString name = metaInfo.name();
if (!name.isEmpty()) {
html += name;
if (!name.endsWith('/'))
html += '/';
}
html += file.path + "<br>";
}
ui.torrentContents->setHtml(html);
}
QFileInfo info(torrentFile);
ui.destinationFolder->setText(info.absolutePath());
enableOkButton();
}
QString AddTorrentDialog::torrentFileName() const
{
return ui.torrentFile->text();
}
QString AddTorrentDialog::destinationFolder() const
{
return ui.destinationFolder->text();
}

View File

@ -0,0 +1,36 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef ADDTORRENTDIALOG_H
#define ADDTORRENTDIALOG_H
#include <QDialog>
#include "ui_addtorrentform.h"
class AddTorrentDialog : public QDialog
{
Q_OBJECT
public:
AddTorrentDialog(QWidget *parent = nullptr);
QString torrentFileName() const;
QString destinationFolder() const;
public slots:
void setTorrent(const QString &torrentFile);
private slots:
void selectTorrent();
void selectDestination();
void enableOkButton();
private:
Ui_AddTorrentFile ui;
QString destinationDirectory;
QString lastDirectory;
QString lastDestinationDirectory;
};
#endif

View File

@ -0,0 +1,266 @@
<ui version="4.0" >
<author></author>
<comment></comment>
<exportmacro></exportmacro>
<class>AddTorrentFile</class>
<widget class="QDialog" name="AddTorrentFile" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>464</width>
<height>385</height>
</rect>
</property>
<property name="windowTitle" >
<string>Add a torrent</string>
</property>
<property name="sizeGripEnabled" >
<bool>false</bool>
</property>
<property name="modal" >
<bool>true</bool>
</property>
<layout class="QVBoxLayout" >
<property name="margin" >
<number>8</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Select a torrent source</string>
</property>
<layout class="QGridLayout" >
<property name="margin" >
<number>8</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item row="6" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>Destination:</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2" >
<widget class="QLineEdit" name="torrentFile" />
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>Tracker URL:</string>
</property>
</widget>
</item>
<item row="0" column="3" >
<widget class="QPushButton" name="browseTorrents" >
<property name="text" >
<string>Browse</string>
</property>
<property name="default" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0" >
<widget class="QLabel" name="label_5" >
<property name="text" >
<string>File(s):</string>
</property>
<property name="alignment" >
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<string>Size:</string>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>Creator:</string>
</property>
</widget>
</item>
<item row="5" column="1" colspan="3" >
<widget class="QTextEdit" name="torrentContents" >
<property name="focusPolicy" >
<enum>Qt::NoFocus</enum>
</property>
<property name="tabChangesFocus" >
<bool>true</bool>
</property>
<property name="lineWrapMode" >
<enum>QTextEdit::NoWrap</enum>
</property>
<property name="readOnly" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2" >
<widget class="QLineEdit" name="destinationFolder" >
<property name="focusPolicy" >
<enum>Qt::StrongFocus</enum>
</property>
</widget>
</item>
<item row="1" column="1" colspan="3" >
<widget class="QLabel" name="announceUrl" >
<property name="text" >
<string>&lt;none></string>
</property>
</widget>
</item>
<item row="0" column="0" >
<widget class="QLabel" name="label" >
<property name="text" >
<string>Torrent file:</string>
</property>
</widget>
</item>
<item row="6" column="3" >
<widget class="QPushButton" name="browseDestination" >
<property name="text" >
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_7" >
<property name="text" >
<string>Comment:</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="3" >
<widget class="QLabel" name="commentLabel" >
<property name="text" >
<string>&lt;none></string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="3" >
<widget class="QLabel" name="creatorLabel" >
<property name="text" >
<string>&lt;none></string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="3" >
<widget class="QLabel" name="sizeLabel" >
<property name="text" >
<string>0</string>
</property>
</widget>
</item>
</layout>
<widget class="QWidget" name="widget" >
<property name="geometry" >
<rect>
<x>10</x>
<y>40</y>
<width>364</width>
<height>33</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<spacer>
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" >
<size>
<width>131</width>
<height>31</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="okButton" >
<property name="enabled" >
<bool>false</bool>
</property>
<property name="text" >
<string>&amp;OK</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton" >
<property name="text" >
<string>&amp;Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<pixmapfunction></pixmapfunction>
<tabstops>
<tabstop>torrentFile</tabstop>
<tabstop>browseTorrents</tabstop>
<tabstop>torrentContents</tabstop>
<tabstop>destinationFolder</tabstop>
<tabstop>browseDestination</tabstop>
<tabstop>okButton</tabstop>
<tabstop>cancelButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>okButton</sender>
<signal>clicked()</signal>
<receiver>AddTorrentFile</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>278</x>
<y>253</y>
</hint>
<hint type="destinationlabel" >
<x>96</x>
<y>254</y>
</hint>
</hints>
</connection>
<connection>
<sender>cancelButton</sender>
<signal>clicked()</signal>
<receiver>AddTorrentFile</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>369</x>
<y>253</y>
</hint>
<hint type="destinationlabel" >
<x>179</x>
<y>282</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,197 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "bencodeparser.h"
#include <QList>
#include <QMetaType>
BencodeParser::BencodeParser()
{
}
bool BencodeParser::parse(const QByteArray &content)
{
if (content.isEmpty()) {
errString = QString("No content");
return false;
}
this->content = content;
index = 0;
infoStart = 0;
infoLength = 0;
return getDictionary(&dictionaryValue);
}
QString BencodeParser::errorString() const
{
return errString;
}
QMap<QByteArray, QVariant> BencodeParser::dictionary() const
{
return dictionaryValue;
}
QByteArray BencodeParser::infoSection() const
{
return content.mid(infoStart, infoLength);
}
bool BencodeParser::getByteString(QByteArray *byteString)
{
const int contentSize = content.size();
int size = -1;
do {
char c = content.at(index);
if (c < '0' || c > '9') {
if (size == -1)
return false;
if (c != ':') {
errString = QString("Unexpected character at pos %1: %2")
.arg(index).arg(c);
return false;
}
++index;
break;
}
if (size == -1)
size = 0;
size *= 10;
size += c - '0';
} while (++index < contentSize);
if (byteString)
*byteString = content.mid(index, size);
index += size;
return true;
}
bool BencodeParser::getInteger(qint64 *integer)
{
const int contentSize = content.size();
if (content.at(index) != 'i')
return false;
++index;
qint64 num = -1;
bool negative = false;
do {
char c = content.at(index);
if (c < '0' || c > '9') {
if (num == -1) {
if (c != '-' || negative)
return false;
negative = true;
continue;
} else {
if (c != 'e') {
errString = QString("Unexpected character at pos %1: %2")
.arg(index).arg(c);
return false;
}
++index;
break;
}
}
if (num == -1)
num = 0;
num *= 10;
num += c - '0';
} while (++index < contentSize);
if (integer)
*integer = negative ? -num : num;
return true;
}
bool BencodeParser::getList(QList<QVariant> *list)
{
const int contentSize = content.size();
if (content.at(index) != 'l')
return false;
QList<QVariant> tmp;
++index;
do {
if (content.at(index) == 'e') {
++index;
break;
}
qint64 number;
QByteArray byteString;
QList<QVariant> tmpList;
QMap<QByteArray, QVariant> dictionary;
if (getInteger(&number))
tmp << number;
else if (getByteString(&byteString))
tmp << byteString;
else if (getList(&tmpList))
tmp << tmpList;
else if (getDictionary(&dictionary))
tmp << QVariant::fromValue<QMap<QByteArray, QVariant> >(dictionary);
else {
errString = QString("error at index %1").arg(index);
return false;
}
} while (index < contentSize);
if (list)
*list = tmp;
return true;
}
bool BencodeParser::getDictionary(QMap<QByteArray, QVariant> *dictionary)
{
const int contentSize = content.size();
if (content.at(index) != 'd')
return false;
QMap<QByteArray, QVariant> tmp;
++index;
do {
if (content.at(index) == 'e') {
++index;
break;
}
QByteArray key;
if (!getByteString(&key))
break;
if (key == "info")
infoStart = index;
qint64 number;
QByteArray byteString;
QList<QVariant> tmpList;
QMap<QByteArray, QVariant> dictionary;
if (getInteger(&number))
tmp.insert(key, number);
else if (getByteString(&byteString))
tmp.insert(key, byteString);
else if (getList(&tmpList))
tmp.insert(key, tmpList);
else if (getDictionary(&dictionary))
tmp.insert(key, QVariant::fromValue<QMap<QByteArray, QVariant> >(dictionary));
else {
errString = QString("error at index %1").arg(index);
return false;
}
if (key == "info")
infoLength = index - infoStart;
} while (index < contentSize);
if (dictionary)
*dictionary = tmp;
return true;
}

View File

@ -0,0 +1,43 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef BENCODEPARSER_H
#define BENCODEPARSER_H
#include <QByteArray>
#include <QMap>
#include <QString>
#include <QVariant>
#include <QList>
typedef QMap<QByteArray,QVariant> Dictionary;
Q_DECLARE_METATYPE(Dictionary)
class BencodeParser
{
public:
BencodeParser();
bool parse(const QByteArray &content);
QString errorString() const;
QMap<QByteArray, QVariant> dictionary() const;
QByteArray infoSection() const;
private:
bool getByteString(QByteArray *byteString);
bool getInteger(qint64 *integer);
bool getList(QList<QVariant> *list);
bool getDictionary(QMap<QByteArray, QVariant> *dictionary);
QMap<QByteArray, QVariant> dictionaryValue;
QString errString;
QByteArray content;
int index;
int infoStart;
int infoLength;
};
#endif

View File

@ -0,0 +1,49 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "connectionmanager.h"
#include <QByteArray>
#include <QDateTime>
static const int MaxConnections = 250;
Q_GLOBAL_STATIC(ConnectionManager, connectionManager)
ConnectionManager *ConnectionManager::instance()
{
return connectionManager();
}
bool ConnectionManager::canAddConnection() const
{
return (connections.size() < MaxConnections);
}
void ConnectionManager::addConnection(PeerWireClient *client)
{
connections << client;
}
void ConnectionManager::removeConnection(PeerWireClient *client)
{
connections.remove(client);
}
int ConnectionManager::maxConnections() const
{
return MaxConnections;
}
QByteArray ConnectionManager::clientId() const
{
if (id.isEmpty()) {
// Generate peer id
qint64 startupTime = QDateTime::currentSecsSinceEpoch();
id += QString::asprintf("-QT%04x-", QT_VERSION >> 8).toLatin1();
id += QByteArray::number(startupTime, 10);
id += QByteArray(20 - id.size(), '-');
}
return id;
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef CONNECTIONMANAGER_H
#define CONNECTIONMANAGER_H
class PeerWireClient;
#include <QByteArray>
#include <QSet>
class ConnectionManager
{
public:
static ConnectionManager *instance();
bool canAddConnection() const;
void addConnection(PeerWireClient *connection);
void removeConnection(PeerWireClient *connection);
int maxConnections() const;
QByteArray clientId() const;
private:
QSet<PeerWireClient *> connections;
mutable QByteArray id;
};
#endif

View File

@ -0,0 +1,414 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "filemanager.h"
#include "metainfo.h"
#include <QByteArray>
#include <QDir>
#include <QFile>
#include <QTimer>
#include <QTimerEvent>
#include <QCryptographicHash>
FileManager::FileManager(QObject *parent)
: QThread(parent)
{
quit = false;
totalLength = 0;
readId = 0;
startVerification = false;
wokeUp = false;
newFile = false;
numPieces = 0;
verifiedPieces.fill(false);
}
FileManager::~FileManager()
{
quit = true;
cond.wakeOne();
wait();
for (QFile *file : std::as_const(files)) {
file->close();
delete file;
}
}
qint32 FileManager::read(qint32 pieceIndex, qint32 offset, qint32 length)
{
ReadRequest request;
request.pieceIndex = pieceIndex;
request.offset = offset;
request.length = length;
QMutexLocker locker(&mutex);
request.id = readId++;
readRequests << request;
if (!wokeUp) {
wokeUp = true;
QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection);
}
return request.id;
}
void FileManager::write(qint32 pieceIndex, qint32 offset, const QByteArray &data)
{
WriteRequest request;
request.pieceIndex = pieceIndex;
request.offset = offset;
request.data = data;
QMutexLocker locker(&mutex);
writeRequests << request;
if (!wokeUp) {
wokeUp = true;
QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection);
}
}
void FileManager::verifyPiece(qint32 pieceIndex)
{
QMutexLocker locker(&mutex);
pendingVerificationRequests << pieceIndex;
startVerification = true;
if (!wokeUp) {
wokeUp = true;
QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection);
}
}
qint32 FileManager::pieceLengthAt(qint32 pieceIndex) const
{
QMutexLocker locker(&mutex);
return (sha1s.size() == pieceIndex + 1)
? (totalLength % pieceLength) : pieceLength;
}
QBitArray FileManager::completedPieces() const
{
QMutexLocker locker(&mutex);
return verifiedPieces;
}
void FileManager::setCompletedPieces(const QBitArray &pieces)
{
QMutexLocker locker(&mutex);
verifiedPieces = pieces;
}
QString FileManager::errorString() const
{
return errString;
}
void FileManager::run()
{
if (!generateFiles())
return;
do {
{
// Go to sleep if there's nothing to do.
QMutexLocker locker(&mutex);
if (!quit && readRequests.isEmpty() && writeRequests.isEmpty() && !startVerification)
cond.wait(&mutex);
}
// Read pending read requests
mutex.lock();
QList<ReadRequest> newReadRequests = readRequests;
readRequests.clear();
mutex.unlock();
while (!newReadRequests.isEmpty()) {
ReadRequest request = newReadRequests.takeFirst();
QByteArray block = readBlock(request.pieceIndex, request.offset, request.length);
emit dataRead(request.id, request.pieceIndex, request.offset, block);
}
// Write pending write requests
mutex.lock();
QList<WriteRequest> newWriteRequests = writeRequests;
writeRequests.clear();
while (!quit && !newWriteRequests.isEmpty()) {
WriteRequest request = newWriteRequests.takeFirst();
writeBlock(request.pieceIndex, request.offset, request.data);
}
// Process pending verification requests
if (startVerification) {
newPendingVerificationRequests = pendingVerificationRequests;
pendingVerificationRequests.clear();
verifyFileContents();
startVerification = false;
}
mutex.unlock();
newPendingVerificationRequests.clear();
} while (!quit);
// Write pending write requests
mutex.lock();
QList<WriteRequest> newWriteRequests = writeRequests;
writeRequests.clear();
mutex.unlock();
while (!newWriteRequests.isEmpty()) {
WriteRequest request = newWriteRequests.takeFirst();
writeBlock(request.pieceIndex, request.offset, request.data);
}
}
void FileManager::startDataVerification()
{
QMutexLocker locker(&mutex);
startVerification = true;
cond.wakeOne();
}
bool FileManager::generateFiles()
{
numPieces = -1;
// Set up the thread local data
if (metaInfo.fileForm() == MetaInfo::SingleFileForm) {
QMutexLocker locker(&mutex);
MetaInfoSingleFile singleFile = metaInfo.singleFile();
QString prefix;
if (!destinationPath.isEmpty()) {
prefix = destinationPath;
if (!prefix.endsWith('/'))
prefix += '/';
QDir dir;
if (!dir.mkpath(prefix)) {
errString = tr("Failed to create directory %1").arg(prefix);
emit error();
return false;
}
}
QFile *file = new QFile(prefix + singleFile.name);
if (!file->open(QFile::ReadWrite)) {
errString = tr("Failed to open/create file %1: %2")
.arg(file->fileName()).arg(file->errorString());
emit error();
delete file;
return false;
}
if (file->size() != singleFile.length) {
newFile = true;
if (!file->resize(singleFile.length)) {
errString = tr("Failed to resize file %1: %2")
.arg(file->fileName()).arg(file->errorString());
delete file;
emit error();
return false;
}
}
fileSizes << file->size();
files << file;
file->close();
pieceLength = singleFile.pieceLength;
totalLength = singleFile.length;
sha1s = singleFile.sha1Sums;
} else {
QMutexLocker locker(&mutex);
QDir dir;
QString prefix;
if (!destinationPath.isEmpty()) {
prefix = destinationPath;
if (!prefix.endsWith('/'))
prefix += '/';
}
if (!metaInfo.name().isEmpty()) {
prefix += metaInfo.name();
if (!prefix.endsWith('/'))
prefix += '/';
}
if (!dir.mkpath(prefix)) {
errString = tr("Failed to create directory %1").arg(prefix);
emit error();
return false;
}
const QList<MetaInfoMultiFile> multiFiles = metaInfo.multiFiles();
for (const MetaInfoMultiFile &entry : multiFiles) {
QString filePath = QFileInfo(prefix + entry.path).path();
if (!QFile::exists(filePath)) {
if (!dir.mkpath(filePath)) {
errString = tr("Failed to create directory %1").arg(filePath);
emit error();
return false;
}
}
QFile *file = new QFile(prefix + entry.path);
if (!file->open(QFile::ReadWrite)) {
errString = tr("Failed to open/create file %1: %2")
.arg(file->fileName()).arg(file->errorString());
emit error();
delete file;
return false;
}
if (file->size() != entry.length) {
newFile = true;
if (!file->resize(entry.length)) {
errString = tr("Failed to resize file %1: %2")
.arg(file->fileName()).arg(file->errorString());
emit error();
delete file;
return false;
}
}
fileSizes << file->size();
files << file;
file->close();
totalLength += entry.length;
}
sha1s = metaInfo.sha1Sums();
pieceLength = metaInfo.pieceLength();
}
numPieces = sha1s.size();
return true;
}
QByteArray FileManager::readBlock(qint32 pieceIndex, qint32 offset, qint32 length)
{
QByteArray block;
qint64 startReadIndex = (quint64(pieceIndex) * pieceLength) + offset;
qint64 currentIndex = 0;
for (int i = 0; !quit && i < files.size() && length > 0; ++i) {
QFile *file = files[i];
qint64 currentFileSize = fileSizes.at(i);
if ((currentIndex + currentFileSize) > startReadIndex) {
if (!file->isOpen()) {
if (!file->open(QFile::ReadWrite)) {
errString = tr("Failed to read from file %1: %2")
.arg(file->fileName()).arg(file->errorString());
emit error();
break;
}
}
file->seek(startReadIndex - currentIndex);
QByteArray chunk = file->read(qMin<qint64>(length, currentFileSize - file->pos()));
file->close();
block += chunk;
length -= chunk.size();
startReadIndex += chunk.size();
if (length < 0) {
errString = tr("Failed to read from file %1 (read %3 bytes): %2")
.arg(file->fileName()).arg(file->errorString()).arg(length);
emit error();
break;
}
}
currentIndex += currentFileSize;
}
return block;
}
bool FileManager::writeBlock(qint32 pieceIndex, qint32 offset, const QByteArray &data)
{
qint64 startWriteIndex = (qint64(pieceIndex) * pieceLength) + offset;
qint64 currentIndex = 0;
int bytesToWrite = data.size();
int written = 0;
for (int i = 0; !quit && i < files.size(); ++i) {
QFile *file = files[i];
qint64 currentFileSize = fileSizes.at(i);
if ((currentIndex + currentFileSize) > startWriteIndex) {
if (!file->isOpen()) {
if (!file->open(QFile::ReadWrite)) {
errString = tr("Failed to write to file %1: %2")
.arg(file->fileName()).arg(file->errorString());
emit error();
break;
}
}
file->seek(startWriteIndex - currentIndex);
qint64 bytesWritten = file->write(data.constData() + written,
qMin<qint64>(bytesToWrite, currentFileSize - file->pos()));
file->close();
if (bytesWritten <= 0) {
errString = tr("Failed to write to file %1: %2")
.arg(file->fileName()).arg(file->errorString());
emit error();
return false;
}
written += bytesWritten;
startWriteIndex += bytesWritten;
bytesToWrite -= bytesWritten;
if (bytesToWrite == 0)
break;
}
currentIndex += currentFileSize;
}
return true;
}
void FileManager::verifyFileContents()
{
// Verify all pieces the first time
if (newPendingVerificationRequests.isEmpty()) {
if (verifiedPieces.count(true) == 0) {
verifiedPieces.resize(sha1s.size());
int oldPercent = 0;
if (!newFile) {
qint32 numPieces = sha1s.size();
for (qint32 index = 0; index < numPieces; ++index) {
verifySinglePiece(index);
int percent = ((index + 1) * 100) / numPieces;
if (oldPercent != percent) {
emit verificationProgress(percent);
oldPercent = percent;
}
}
}
}
emit verificationDone();
return;
}
// Verify all pending pieces
for (int index : std::as_const(newPendingVerificationRequests))
emit pieceVerified(index, verifySinglePiece(index));
}
bool FileManager::verifySinglePiece(qint32 pieceIndex)
{
QByteArray block = readBlock(pieceIndex, 0, pieceLength);
QByteArray sha1Sum = QCryptographicHash::hash(block, QCryptographicHash::Sha1);
if (sha1Sum != sha1s.at(pieceIndex))
return false;
verifiedPieces.setBit(pieceIndex);
return true;
}
void FileManager::wakeUp()
{
QMutexLocker locker(&mutex);
wokeUp = false;
cond.wakeOne();
}

View File

@ -0,0 +1,106 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef FILEMANAGER_H
#define FILEMANAGER_H
#include <QBitArray>
#include <QList>
#include <QMutex>
#include <QThread>
#include <QWaitCondition>
#include "metainfo.h"
QT_BEGIN_NAMESPACE
class QByteArray;
class QFile;
class QTimerEvent;
QT_END_NAMESPACE
class FileManager : public QThread
{
Q_OBJECT
public:
FileManager(QObject *parent = nullptr);
virtual ~FileManager();
inline void setMetaInfo(const MetaInfo &info) { metaInfo = info; }
inline void setDestinationFolder(const QString &directory) { destinationPath = directory; }
qint32 read(qint32 pieceIndex, qint32 offset, qint32 length);
void write(qint32 pieceIndex, qint32 offset, const QByteArray &data);
void verifyPiece(qint32 pieceIndex);
inline qint64 totalSize() const { return totalLength; }
inline qint32 pieceCount() const { return numPieces; }
qint32 pieceLengthAt(qint32 pieceIndex) const;
QBitArray completedPieces() const;
void setCompletedPieces(const QBitArray &pieces);
QString errorString() const;
public slots:
void startDataVerification();
signals:
void dataRead(qint32 id, qint32 pieceIndex, qint32 offset, const QByteArray &data);
void error();
void verificationProgress(int percent);
void verificationDone();
void pieceVerified(qint32 pieceIndex, bool verified);
protected:
void run() override;
private slots:
bool verifySinglePiece(qint32 pieceIndex);
void wakeUp();
private:
bool generateFiles();
QByteArray readBlock(qint32 pieceIndex, qint32 offset, qint32 length);
bool writeBlock(qint32 pieceIndex, qint32 offset, const QByteArray &data);
void verifyFileContents();
struct WriteRequest {
qint32 pieceIndex;
qint32 offset;
QByteArray data;
};
struct ReadRequest {
qint32 pieceIndex;
qint32 offset;
qint32 length;
qint32 id;
};
QString errString;
QString destinationPath;
MetaInfo metaInfo;
QList<QFile *> files;
QList<QByteArray> sha1s;
QBitArray verifiedPieces;
bool newFile;
int pieceLength;
qint64 totalLength;
int numPieces;
int readId;
bool startVerification;
bool quit;
bool wokeUp;
QList<WriteRequest> writeRequests;
QList<ReadRequest> readRequests;
QList<int> pendingVerificationRequests;
QList<int> newPendingVerificationRequests;
QList<qint64> fileSizes;
mutable QMutex mutex;
mutable QWaitCondition cond;
};
#endif

View File

@ -0,0 +1,12 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
<file>icons/peertopeer.png</file>
<file>icons/1uparrow.png</file>
<file>icons/1downarrow.png</file>
<file>icons/bottom.png</file>
<file>icons/player_pause.png</file>
<file>icons/player_play.png</file>
<file>icons/player_stop.png</file>
<file>icons/exit.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,18 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include <QApplication>
#include <QtCore>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
Q_INIT_RESOURCE(icons);
MainWindow window;
window.show();
return app.exec();
}

View File

@ -0,0 +1,690 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include <QtWidgets>
#include "addtorrentdialog.h"
#include "mainwindow.h"
#include "ratecontroller.h"
#include "torrentclient.h"
// TorrentView extends QTreeWidget to allow drag and drop.
class TorrentView : public QTreeWidget
{
Q_OBJECT
public:
TorrentView(QWidget *parent = nullptr);
#if QT_CONFIG(draganddrop)
signals:
void fileDropped(const QString &fileName);
protected:
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
#endif
};
// TorrentViewDelegate is used to draw the progress bars.
class TorrentViewDelegate : public QItemDelegate
{
Q_OBJECT
public:
inline TorrentViewDelegate(MainWindow *mainWindow) : QItemDelegate(mainWindow) {}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index ) const override
{
if (index.column() != 2) {
QItemDelegate::paint(painter, option, index);
return;
}
// Set up a QStyleOptionProgressBar to precisely mimic the
// environment of a progress bar.
QStyleOptionProgressBar progressBarOption;
progressBarOption.state |= QStyle::State_Enabled;
progressBarOption.direction = QApplication::layoutDirection();
progressBarOption.rect = option.rect;
progressBarOption.fontMetrics = QFontMetrics(QApplication::font());
progressBarOption.minimum = 0;
progressBarOption.maximum = 100;
progressBarOption.textAlignment = Qt::AlignCenter;
progressBarOption.textVisible = true;
// Set the progress and text values of the style option.
int progress = qobject_cast<MainWindow *>(parent())->clientForRow(index.row())->progress();
progressBarOption.progress = progress < 0 ? 0 : progress;
progressBarOption.text = QString::asprintf("%d%%", progressBarOption.progress);
// Draw the progress bar onto the view.
QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter);
}
};
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), quitDialog(nullptr), saveChanges(false)
{
// Initialize some static strings
QStringList headers;
headers << tr("Torrent") << tr("Peers/Seeds") << tr("Progress")
<< tr("Down rate") << tr("Up rate") << tr("Status");
// Main torrent list
torrentView = new TorrentView(this);
torrentView->setItemDelegate(new TorrentViewDelegate(this));
torrentView->setHeaderLabels(headers);
torrentView->setSelectionBehavior(QAbstractItemView::SelectRows);
torrentView->setAlternatingRowColors(true);
torrentView->setRootIsDecorated(false);
setCentralWidget(torrentView);
// Set header resize modes and initial section sizes
QFontMetrics fm = fontMetrics();
QHeaderView *header = torrentView->header();
header->resizeSection(0, fm.horizontalAdvance("typical-name-for-a-torrent.torrent"));
header->resizeSection(1, fm.horizontalAdvance(headers.at(1) + " "));
header->resizeSection(2, fm.horizontalAdvance(headers.at(2) + " "));
header->resizeSection(3, qMax(fm.horizontalAdvance(headers.at(3) + " "), fm.horizontalAdvance(" 1234.0 KB/s ")));
header->resizeSection(4, qMax(fm.horizontalAdvance(headers.at(4) + " "), fm.horizontalAdvance(" 1234.0 KB/s ")));
header->resizeSection(5, qMax(fm.horizontalAdvance(headers.at(5) + " "), fm.horizontalAdvance(tr("Downloading") + " ")));
// Create common actions
QAction *newTorrentAction = new QAction(QIcon(":/icons/bottom.png"), tr("Add &new torrent"), this);
pauseTorrentAction = new QAction(QIcon(":/icons/player_pause.png"), tr("&Pause torrent"), this);
removeTorrentAction = new QAction(QIcon(":/icons/player_stop.png"), tr("&Remove torrent"), this);
// File menu
QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(newTorrentAction);
fileMenu->addAction(pauseTorrentAction);
fileMenu->addAction(removeTorrentAction);
fileMenu->addSeparator();
fileMenu->addAction(QIcon(":/icons/exit.png"), tr("E&xit"), this, &MainWindow::close);
// Help menu
QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(tr("&About"), this, &MainWindow::about);
helpMenu->addAction(tr("About &Qt"), qApp, QApplication::aboutQt);
// Top toolbar
QToolBar *topBar = new QToolBar(tr("Tools"));
addToolBar(Qt::TopToolBarArea, topBar);
topBar->setMovable(false);
topBar->addAction(newTorrentAction);
topBar->addAction(removeTorrentAction);
topBar->addAction(pauseTorrentAction);
topBar->addSeparator();
downActionTool = topBar->addAction(QIcon(tr(":/icons/1downarrow.png")), tr("Move down"));
upActionTool = topBar->addAction(QIcon(tr(":/icons/1uparrow.png")), tr("Move up"));
// Bottom toolbar
QToolBar *bottomBar = new QToolBar(tr("Rate control"));
addToolBar(Qt::BottomToolBarArea, bottomBar);
bottomBar->setMovable(false);
downloadLimitSlider = new QSlider(Qt::Horizontal);
downloadLimitSlider->setRange(0, 1000);
bottomBar->addWidget(new QLabel(tr("Max download:")));
bottomBar->addWidget(downloadLimitSlider);
bottomBar->addWidget((downloadLimitLabel = new QLabel(tr("0 KB/s"))));
downloadLimitLabel->setFixedSize(QSize(fm.horizontalAdvance(tr("99999 KB/s")), fm.lineSpacing()));
bottomBar->addSeparator();
uploadLimitSlider = new QSlider(Qt::Horizontal);
uploadLimitSlider->setRange(0, 1000);
bottomBar->addWidget(new QLabel(tr("Max upload:")));
bottomBar->addWidget(uploadLimitSlider);
bottomBar->addWidget((uploadLimitLabel = new QLabel(tr("0 KB/s"))));
uploadLimitLabel->setFixedSize(QSize(fm.horizontalAdvance(tr("99999 KB/s")), fm.lineSpacing()));
#ifdef Q_OS_MACOS
setUnifiedTitleAndToolBarOnMac(true);
#endif
// Set up connections
connect(torrentView, &TorrentView::itemSelectionChanged,
this, &MainWindow::setActionsEnabled);
connect(torrentView, &TorrentView::fileDropped,
this, &MainWindow::acceptFileDrop);
connect(uploadLimitSlider, &QSlider::valueChanged,
this, &MainWindow::setUploadLimit);
connect(downloadLimitSlider, &QSlider::valueChanged,
this, &MainWindow::setDownloadLimit);
connect(newTorrentAction, &QAction::triggered,
this, QOverload<>::of(&MainWindow::addTorrent));
connect(pauseTorrentAction, &QAction::triggered,
this, &MainWindow::pauseTorrent);
connect(removeTorrentAction, &QAction::triggered,
this, &MainWindow::removeTorrent);
connect(upActionTool, &QAction::triggered,
this, &MainWindow::moveTorrentUp);
connect(downActionTool, &QAction::triggered,
this, &MainWindow::moveTorrentDown);
// Load settings and start
setWindowTitle(tr("Torrent Client"));
setActionsEnabled();
QMetaObject::invokeMethod(this, "loadSettings", Qt::QueuedConnection);
}
QSize MainWindow::sizeHint() const
{
const QHeaderView *header = torrentView->header();
// Add up the sizes of all header sections. The last section is
// stretched, so its size is relative to the size of the width;
// instead of counting it, we count the size of its largest value.
int width = fontMetrics().horizontalAdvance(tr("Downloading") + " ");
for (int i = 0; i < header->count() - 1; ++i)
width += header->sectionSize(i);
return QSize(width, QMainWindow::sizeHint().height());
}
const TorrentClient *MainWindow::clientForRow(int row) const
{
// Return the client at the given row.
return jobs.at(row).client;
}
int MainWindow::rowOfClient(TorrentClient *client) const
{
// Return the row that displays this client's status, or -1 if the
// client is not known.
int row = 0;
for (const Job &job : jobs) {
if (job.client == client)
return row;
++row;
}
return -1;
}
void MainWindow::loadSettings()
{
// Load base settings (last working directory, upload/download limits).
QSettings settings("QtProject", "Torrent");
lastDirectory = settings.value("LastDirectory").toString();
if (lastDirectory.isEmpty())
lastDirectory = QDir::currentPath();
int up = settings.value("UploadLimit").toInt();
int down = settings.value("DownloadLimit").toInt();
uploadLimitSlider->setValue(up ? up : 170);
downloadLimitSlider->setValue(down ? down : 550);
// Resume all previous downloads.
int size = settings.beginReadArray("Torrents");
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
QByteArray resumeState = settings.value("resumeState").toByteArray();
QString fileName = settings.value("sourceFileName").toString();
QString dest = settings.value("destinationFolder").toString();
if (addTorrent(fileName, dest, resumeState)) {
TorrentClient *client = jobs.last().client;
client->setDownloadedBytes(settings.value("downloadedBytes").toLongLong());
client->setUploadedBytes(settings.value("uploadedBytes").toLongLong());
}
}
}
bool MainWindow::addTorrent()
{
// Show the file dialog, let the user select what torrent to start downloading.
QString fileName = QFileDialog::getOpenFileName(this, tr("Choose a torrent file"),
lastDirectory,
tr("Torrents (*.torrent);;"
" All files (*.*)"));
if (fileName.isEmpty())
return false;
lastDirectory = QFileInfo(fileName).absolutePath();
// Show the "Add Torrent" dialog.
AddTorrentDialog *addTorrentDialog = new AddTorrentDialog(this);
addTorrentDialog->setTorrent(fileName);
addTorrentDialog->deleteLater();
if (!addTorrentDialog->exec())
return false;
// Add the torrent to our list of downloads
addTorrent(fileName, addTorrentDialog->destinationFolder());
if (!saveChanges) {
saveChanges = true;
QTimer::singleShot(1000, this, &MainWindow::saveSettings);
}
return true;
}
void MainWindow::removeTorrent()
{
// Find the row of the current item, and find the torrent client
// for that row.
int row = torrentView->indexOfTopLevelItem(torrentView->currentItem());
TorrentClient *client = jobs.at(row).client;
// Stop the client.
client->disconnect();
connect(client, &TorrentClient::stopped,
this, &MainWindow::torrentStopped);
client->stop();
// Remove the row from the view.
delete torrentView->takeTopLevelItem(row);
jobs.removeAt(row);
setActionsEnabled();
saveChanges = true;
saveSettings();
}
void MainWindow::torrentStopped()
{
// Schedule the client for deletion.
TorrentClient *client = qobject_cast<TorrentClient *>(sender());
client->deleteLater();
// If the quit dialog is shown, update its progress.
if (quitDialog) {
if (++jobsStopped == jobsToStop)
quitDialog->close();
}
}
void MainWindow::torrentError(TorrentClient::Error)
{
// Delete the client.
TorrentClient *client = qobject_cast<TorrentClient *>(sender());
int row = rowOfClient(client);
QString fileName = jobs.at(row).torrentFileName;
jobs.removeAt(row);
// Display the warning.
QMessageBox::warning(this, tr("Error"),
tr("An error occurred while downloading %0: %1")
.arg(fileName)
.arg(client->errorString()));
delete torrentView->takeTopLevelItem(row);
client->deleteLater();
}
bool MainWindow::addTorrent(const QString &fileName, const QString &destinationFolder,
const QByteArray &resumeState)
{
// Check if the torrent is already being downloaded.
for (const Job &job : std::as_const(jobs)) {
if (job.torrentFileName == fileName && job.destinationDirectory == destinationFolder) {
QMessageBox::warning(this, tr("Already downloading"),
tr("The torrent file %1 is "
"already being downloaded.").arg(fileName));
return false;
}
}
// Create a new torrent client and attempt to parse the torrent data.
TorrentClient *client = new TorrentClient(this);
if (!client->setTorrent(fileName)) {
QMessageBox::warning(this, tr("Error"),
tr("The torrent file %1 cannot not be opened/resumed.").arg(fileName));
delete client;
return false;
}
client->setDestinationFolder(destinationFolder);
client->setDumpedState(resumeState);
// Setup the client connections.
connect(client, &TorrentClient::stateChanged,
this, &MainWindow::updateState);
connect(client, &TorrentClient::peerInfoUpdated,
this, &MainWindow::updatePeerInfo);
connect(client, &TorrentClient::progressUpdated,
this, &MainWindow::updateProgress);
connect(client, &TorrentClient::downloadRateUpdated,
this, &MainWindow::updateDownloadRate);
connect(client, &TorrentClient::uploadRateUpdated,
this, &MainWindow::updateUploadRate);
connect(client, &TorrentClient::stopped,
this, &MainWindow::torrentStopped);
connect(client, QOverload<TorrentClient::Error>::of(&TorrentClient::error),
this, &MainWindow::torrentError);
// Add the client to the list of downloading jobs.
Job job;
job.client = client;
job.torrentFileName = fileName;
job.destinationDirectory = destinationFolder;
jobs << job;
// Create and add a row in the torrent view for this download.
QTreeWidgetItem *item = new QTreeWidgetItem(torrentView);
QString baseFileName = QFileInfo(fileName).fileName();
if (baseFileName.endsWith(u".torrent", Qt::CaseInsensitive))
baseFileName.chop(8);
item->setText(0, baseFileName);
item->setToolTip(0, tr("Torrent: %1<br>Destination: %2")
.arg(baseFileName).arg(destinationFolder));
item->setText(1, tr("0/0"));
item->setText(2, "0");
item->setText(3, "0.0 KB/s");
item->setText(4, "0.0 KB/s");
item->setText(5, tr("Idle"));
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
item->setTextAlignment(1, Qt::AlignHCenter);
if (!saveChanges) {
saveChanges = true;
QTimer::singleShot(5000, this, &MainWindow::saveSettings);
}
client->start();
return true;
}
void MainWindow::saveSettings()
{
if (!saveChanges)
return;
saveChanges = false;
// Prepare and reset the settings
QSettings settings("QtProject", "Torrent");
settings.clear();
settings.setValue("LastDirectory", lastDirectory);
settings.setValue("UploadLimit", uploadLimitSlider->value());
settings.setValue("DownloadLimit", downloadLimitSlider->value());
// Store data on all known torrents
settings.beginWriteArray("Torrents");
for (int i = 0; i < jobs.size(); ++i) {
settings.setArrayIndex(i);
settings.setValue("sourceFileName", jobs.at(i).torrentFileName);
settings.setValue("destinationFolder", jobs.at(i).destinationDirectory);
settings.setValue("uploadedBytes", jobs.at(i).client->uploadedBytes());
settings.setValue("downloadedBytes", jobs.at(i).client->downloadedBytes());
settings.setValue("resumeState", jobs.at(i).client->dumpedState());
}
settings.endArray();
settings.sync();
}
void MainWindow::updateState(TorrentClient::State)
{
// Update the state string whenever the client's state changes.
TorrentClient *client = qobject_cast<TorrentClient *>(sender());
int row = rowOfClient(client);
QTreeWidgetItem *item = torrentView->topLevelItem(row);
if (item) {
item->setToolTip(0, tr("Torrent: %1<br>Destination: %2<br>State: %3")
.arg(jobs.at(row).torrentFileName)
.arg(jobs.at(row).destinationDirectory)
.arg(client->stateString()));
item->setText(5, client->stateString());
}
setActionsEnabled();
}
void MainWindow::updatePeerInfo()
{
// Update the number of connected, visited, seed and leecher peers.
TorrentClient *client = qobject_cast<TorrentClient *>(sender());
int row = rowOfClient(client);
QTreeWidgetItem *item = torrentView->topLevelItem(row);
item->setText(1, tr("%1/%2").arg(client->connectedPeerCount())
.arg(client->seedCount()));
}
void MainWindow::updateProgress(int percent)
{
TorrentClient *client = qobject_cast<TorrentClient *>(sender());
int row = rowOfClient(client);
// Update the progressbar.
QTreeWidgetItem *item = torrentView->topLevelItem(row);
if (item)
item->setText(2, QString::number(percent));
}
void MainWindow::setActionsEnabled()
{
// Find the view item and client for the current row, and update
// the states of the actions.
QTreeWidgetItem *item = nullptr;
if (!torrentView->selectedItems().isEmpty())
item = torrentView->selectedItems().first();
TorrentClient *client = item ? jobs.at(torrentView->indexOfTopLevelItem(item)).client : nullptr;
bool pauseEnabled = client && ((client->state() == TorrentClient::Paused)
|| (client->state() > TorrentClient::Preparing));
removeTorrentAction->setEnabled(item != nullptr);
pauseTorrentAction->setEnabled(item && pauseEnabled);
if (client && client->state() == TorrentClient::Paused) {
pauseTorrentAction->setIcon(QIcon(":/icons/player_play.png"));
pauseTorrentAction->setText(tr("Resume torrent"));
} else {
pauseTorrentAction->setIcon(QIcon(":/icons/player_pause.png"));
pauseTorrentAction->setText(tr("Pause torrent"));
}
int row = torrentView->indexOfTopLevelItem(item);
upActionTool->setEnabled(item && row != 0);
downActionTool->setEnabled(item && row != jobs.size() - 1);
}
void MainWindow::updateDownloadRate(int bytesPerSecond)
{
// Update the download rate.
TorrentClient *client = qobject_cast<TorrentClient *>(sender());
int row = rowOfClient(client);
const QString num = QString::asprintf("%.1f KB/s", bytesPerSecond / 1024.0);
torrentView->topLevelItem(row)->setText(3, num);
if (!saveChanges) {
saveChanges = true;
QTimer::singleShot(5000, this, &MainWindow::saveSettings);
}
}
void MainWindow::updateUploadRate(int bytesPerSecond)
{
// Update the upload rate.
TorrentClient *client = qobject_cast<TorrentClient *>(sender());
int row = rowOfClient(client);
const QString num = QString::asprintf("%.1f KB/s", bytesPerSecond / 1024.0);
torrentView->topLevelItem(row)->setText(4, num);
if (!saveChanges) {
saveChanges = true;
QTimer::singleShot(5000, this, &MainWindow::saveSettings);
}
}
void MainWindow::pauseTorrent()
{
// Pause or unpause the current torrent.
int row = torrentView->indexOfTopLevelItem(torrentView->currentItem());
TorrentClient *client = jobs.at(row).client;
client->setPaused(client->state() != TorrentClient::Paused);
setActionsEnabled();
}
void MainWindow::moveTorrentUp()
{
QTreeWidgetItem *item = torrentView->currentItem();
int row = torrentView->indexOfTopLevelItem(item);
if (row == 0)
return;
Job tmp = jobs.at(row - 1);
jobs[row - 1] = jobs[row];
jobs[row] = tmp;
QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(row - 1);
torrentView->insertTopLevelItem(row, itemAbove);
setActionsEnabled();
}
void MainWindow::moveTorrentDown()
{
QTreeWidgetItem *item = torrentView->currentItem();
int row = torrentView->indexOfTopLevelItem(item);
if (row == jobs.size() - 1)
return;
Job tmp = jobs.at(row + 1);
jobs[row + 1] = jobs[row];
jobs[row] = tmp;
QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(row + 1);
torrentView->insertTopLevelItem(row, itemAbove);
setActionsEnabled();
}
static int rateFromValue(int value)
{
int rate = 0;
if (value >= 0 && value < 250) {
rate = 1 + int(value * 0.124);
} else if (value < 500) {
rate = 32 + int((value - 250) * 0.384);
} else if (value < 750) {
rate = 128 + int((value - 500) * 1.536);
} else {
rate = 512 + int((value - 750) * 6.1445);
}
return rate;
}
void MainWindow::setUploadLimit(int value)
{
int rate = rateFromValue(value);
uploadLimitLabel->setText(tr("%1 KB/s").arg(QString::asprintf("%4d", rate)));
RateController::instance()->setUploadLimit(rate * 1024);
}
void MainWindow::setDownloadLimit(int value)
{
int rate = rateFromValue(value);
downloadLimitLabel->setText(tr("%1 KB/s").arg(QString::asprintf("%4d", rate)));
RateController::instance()->setDownloadLimit(rate * 1024);
}
void MainWindow::about()
{
QLabel *icon = new QLabel;
icon->setPixmap(QPixmap(":/icons/peertopeer.png"));
QLabel *text = new QLabel;
text->setWordWrap(true);
text->setText("<p>The <b>Torrent Client</b> example demonstrates how to"
" write a complete peer-to-peer file sharing"
" application using Qt's network and thread classes.</p>"
"<p>This feature complete client implementation of"
" the BitTorrent protocol can efficiently"
" maintain several hundred network connections"
" simultaneously.</p>");
QPushButton *quitButton = new QPushButton("OK");
QHBoxLayout *topLayout = new QHBoxLayout;
topLayout->setContentsMargins(10, 10, 10, 10);
topLayout->setSpacing(10);
topLayout->addWidget(icon);
topLayout->addWidget(text);
QHBoxLayout *bottomLayout = new QHBoxLayout;
bottomLayout->addStretch();
bottomLayout->addWidget(quitButton);
bottomLayout->addStretch();
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(topLayout);
mainLayout->addLayout(bottomLayout);
QDialog about(this);
about.setModal(true);
about.setWindowTitle(tr("About Torrent Client"));
about.setLayout(mainLayout);
connect(quitButton, &QPushButton::clicked, &about, &QDialog::close);
about.exec();
}
void MainWindow::acceptFileDrop(const QString &fileName)
{
// Create and show the "Add Torrent" dialog.
AddTorrentDialog *addTorrentDialog = new AddTorrentDialog;
lastDirectory = QFileInfo(fileName).absolutePath();
addTorrentDialog->setTorrent(fileName);
addTorrentDialog->deleteLater();
if (!addTorrentDialog->exec())
return;
// Add the torrent to our list of downloads.
addTorrent(fileName, addTorrentDialog->destinationFolder());
saveSettings();
}
void MainWindow::closeEvent(QCloseEvent *)
{
if (jobs.isEmpty())
return;
// Save upload / download numbers.
saveSettings();
saveChanges = false;
quitDialog = new QProgressDialog(tr("Disconnecting from trackers"), tr("Abort"), 0, jobsToStop, this);
// Stop all clients, remove the rows from the view and wait for
// them to signal that they have stopped.
jobsToStop = 0;
jobsStopped = 0;
for (const Job &job : std::as_const(jobs)) {
++jobsToStop;
TorrentClient *client = job.client;
client->disconnect();
connect(client, &TorrentClient::stopped, this, &MainWindow::torrentStopped);
client->stop();
delete torrentView->takeTopLevelItem(0);
}
if (jobsToStop > jobsStopped)
quitDialog->exec();
quitDialog->deleteLater();
quitDialog = nullptr;
}
TorrentView::TorrentView(QWidget *parent)
: QTreeWidget(parent)
{
#if QT_CONFIG(draganddrop)
setAcceptDrops(true);
#endif
}
#if QT_CONFIG(draganddrop)
void TorrentView::dragMoveEvent(QDragMoveEvent *event)
{
// Accept file actions with a '.torrent' extension.
QUrl url(event->mimeData()->text());
if (url.isValid() && url.scheme() == "file"
&& url.path().toLower().endsWith(".torrent"))
event->acceptProposedAction();
}
void TorrentView::dropEvent(QDropEvent *event)
{
// Accept drops if the file has a '.torrent' extension and it
// exists.
QString fileName = QUrl(event->mimeData()->text()).path();
if (QFile::exists(fileName) && fileName.toLower().endsWith(".torrent"))
emit fileDropped(fileName);
}
#endif
#include "mainwindow.moc"

View File

@ -0,0 +1,94 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QList>
#include <QStringList>
#include <QMainWindow>
#include "torrentclient.h"
QT_BEGIN_NAMESPACE
class QAction;
class QCloseEvent;
class QLabel;
class QProgressDialog;
class QSlider;
QT_END_NAMESPACE
class TorrentView;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
QSize sizeHint() const override;
const TorrentClient *clientForRow(int row) const;
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void loadSettings();
void saveSettings();
bool addTorrent();
void removeTorrent();
void pauseTorrent();
void moveTorrentUp();
void moveTorrentDown();
void torrentStopped();
void torrentError(TorrentClient::Error error);
void updateState(TorrentClient::State state);
void updatePeerInfo();
void updateProgress(int percent);
void updateDownloadRate(int bytesPerSecond);
void updateUploadRate(int bytesPerSecond);
void setUploadLimit(int bytes);
void setDownloadLimit(int bytes);
void about();
void setActionsEnabled();
void acceptFileDrop(const QString &fileName);
private:
int rowOfClient(TorrentClient *client) const;
bool addTorrent(const QString &fileName, const QString &destinationFolder,
const QByteArray &resumeState = QByteArray());
TorrentView *torrentView;
QAction *pauseTorrentAction;
QAction *removeTorrentAction;
QAction *upActionTool;
QAction *downActionTool;
QSlider *uploadLimitSlider;
QSlider *downloadLimitSlider;
QLabel *uploadLimitLabel;
QLabel *downloadLimitLabel;
int uploadLimit;
int downloadLimit;
struct Job {
TorrentClient *client;
QString torrentFileName;
QString destinationDirectory;
};
QList<Job> jobs;
int jobsStopped;
int jobsToStop;
QString lastDirectory;
QProgressDialog *quitDialog;
bool saveChanges;
};
#endif

View File

@ -0,0 +1,180 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "bencodeparser.h"
#include "metainfo.h"
#include <QDateTime>
#include <QMetaType>
#include <QString>
MetaInfo::MetaInfo()
{
clear();
}
void MetaInfo::clear()
{
errString = "Unknown error";
content.clear();
infoData.clear();
metaInfoMultiFiles.clear();
metaInfoAnnounce.clear();
metaInfoAnnounceList.clear();
metaInfoCreationDate = QDateTime();
metaInfoComment.clear();
metaInfoCreatedBy.clear();
metaInfoName.clear();
metaInfoPieceLength = 0;
metaInfoSha1Sums.clear();
}
bool MetaInfo::parse(const QByteArray &data)
{
clear();
content = data;
BencodeParser parser;
if (!parser.parse(content)) {
errString = parser.errorString();
return false;
}
infoData = parser.infoSection();
QMap<QByteArray, QVariant> dict = parser.dictionary();
if (!dict.contains("info"))
return false;
QMap<QByteArray, QVariant> info = qvariant_cast<Dictionary>(dict.value("info"));
if (info.contains("files")) {
metaInfoFileForm = MultiFileForm;
QList<QVariant> files = info.value("files").toList();
for (int i = 0; i < files.size(); ++i) {
const QMap<QByteArray, QVariant> file = qvariant_cast<Dictionary>(files.at(i));
const QList<QVariant> pathElements = file.value("path").toList();
QByteArray path;
for (const QVariant &p : pathElements) {
if (!path.isEmpty())
path += '/';
path += p.toByteArray();
}
MetaInfoMultiFile multiFile;
multiFile.length = file.value("length").toLongLong();
multiFile.path = QString::fromUtf8(path);
multiFile.md5sum = file.value("md5sum").toByteArray();
metaInfoMultiFiles << multiFile;
}
metaInfoName = QString::fromUtf8(info.value("name").toByteArray());
metaInfoPieceLength = info.value("piece length").toInt();
QByteArray pieces = info.value("pieces").toByteArray();
for (int i = 0; i < pieces.size(); i += 20)
metaInfoSha1Sums << pieces.mid(i, 20);
} else if (info.contains("length")) {
metaInfoFileForm = SingleFileForm;
metaInfoSingleFile.length = info.value("length").toLongLong();
metaInfoSingleFile.md5sum = info.value("md5sum").toByteArray();
metaInfoSingleFile.name = QString::fromUtf8(info.value("name").toByteArray());
metaInfoSingleFile.pieceLength = info.value("piece length").toInt();
QByteArray pieces = info.value("pieces").toByteArray();
for (int i = 0; i < pieces.size(); i += 20)
metaInfoSingleFile.sha1Sums << pieces.mid(i, 20);
}
metaInfoAnnounce = QString::fromUtf8(dict.value("announce").toByteArray());
if (dict.contains("announce-list")) {
// ### unimplemented
}
if (dict.contains("creation date"))
metaInfoCreationDate.setSecsSinceEpoch(dict.value("creation date").toInt());
if (dict.contains("comment"))
metaInfoComment = QString::fromUtf8(dict.value("comment").toByteArray());
if (dict.contains("created by"))
metaInfoCreatedBy = QString::fromUtf8(dict.value("created by").toByteArray());
return true;
}
QByteArray MetaInfo::infoValue() const
{
return infoData;
}
QString MetaInfo::errorString() const
{
return errString;
}
MetaInfo::FileForm MetaInfo::fileForm() const
{
return metaInfoFileForm;
}
QString MetaInfo::announceUrl() const
{
return metaInfoAnnounce;
}
QStringList MetaInfo::announceList() const
{
return metaInfoAnnounceList;
}
QDateTime MetaInfo::creationDate() const
{
return metaInfoCreationDate;
}
QString MetaInfo::comment() const
{
return metaInfoComment;
}
QString MetaInfo::createdBy() const
{
return metaInfoCreatedBy;
}
MetaInfoSingleFile MetaInfo::singleFile() const
{
return metaInfoSingleFile;
}
QList<MetaInfoMultiFile> MetaInfo::multiFiles() const
{
return metaInfoMultiFiles;
}
QString MetaInfo::name() const
{
return metaInfoName;
}
int MetaInfo::pieceLength() const
{
return metaInfoPieceLength;
}
QList<QByteArray> MetaInfo::sha1Sums() const
{
return metaInfoSha1Sums;
}
qint64 MetaInfo::totalSize() const
{
if (fileForm() == SingleFileForm)
return singleFile().length;
qint64 size = 0;
for (const MetaInfoMultiFile &file : metaInfoMultiFiles)
size += file.length;
return size;
}

View File

@ -0,0 +1,84 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef METAINFO_H
#define METAINFO_H
#include <QByteArray>
#include <QDateTime>
#include <QList>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QVariant>
struct MetaInfoSingleFile
{
qint64 length;
QByteArray md5sum;
QString name;
qint32 pieceLength;
QList<QByteArray> sha1Sums;
};
struct MetaInfoMultiFile
{
qint64 length;
QByteArray md5sum;
QString path;
};
class MetaInfo
{
public:
enum FileForm {
SingleFileForm,
MultiFileForm
};
MetaInfo();
void clear();
bool parse(const QByteArray &data);
QString errorString() const;
QByteArray infoValue() const;
FileForm fileForm() const;
QString announceUrl() const;
QStringList announceList() const;
QDateTime creationDate() const;
QString comment() const;
QString createdBy() const;
// For single file form
MetaInfoSingleFile singleFile() const;
// For multifile form
QList<MetaInfoMultiFile> multiFiles() const;
QString name() const;
int pieceLength() const;
QList<QByteArray> sha1Sums() const;
// Total size
qint64 totalSize() const;
private:
QString errString;
QByteArray content;
QByteArray infoData;
FileForm metaInfoFileForm;
MetaInfoSingleFile metaInfoSingleFile;
QList<MetaInfoMultiFile> metaInfoMultiFiles;
QString metaInfoAnnounce;
QStringList metaInfoAnnounceList;
QDateTime metaInfoCreationDate;
QString metaInfoComment;
QString metaInfoCreatedBy;
QString metaInfoName;
int metaInfoPieceLength;
QList<QByteArray> metaInfoSha1Sums;
};
#endif

View File

@ -0,0 +1,610 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "peerwireclient.h"
#include <QHostAddress>
#include <QTimerEvent>
#include <QtEndian>
#include <chrono>
static constexpr std::chrono::seconds PendingRequestTimeout(60);
static constexpr std::chrono::seconds ClientTimeout(120);
static constexpr std::chrono::seconds ConnectTimeout(60);
static constexpr std::chrono::seconds KeepAliveInterval(30);
static constexpr std::chrono::seconds PeerRateControlTimerDelay(2);
static const int MinimalHeaderSize = 48;
static const char ProtocolId[] = "BitTorrent protocol";
static const char ProtocolIdSize = 19;
// Constructs an unconnected PeerWire client and starts the connect timer.
PeerWireClient::PeerWireClient(const QByteArray &peerId, QObject *parent)
: QTcpSocket(parent), pendingBlockSizes(0),
pwState(ChokingPeer | ChokedByPeer), receivedHandShake(false), gotPeerId(false),
sentHandShake(false), nextPacketLength(-1), pendingRequestTimer(0), invalidateTimeout(false),
keepAliveTimer(0), torrentPeer(nullptr)
{
memset(uploadSpeedData, 0, sizeof(uploadSpeedData));
memset(downloadSpeedData, 0, sizeof(downloadSpeedData));
transferSpeedTimer = startTimer(PeerRateControlTimerDelay);
timeoutTimer = startTimer(ConnectTimeout);
peerIdString = peerId;
connect(this, &PeerWireClient::readyRead,
this, &PeerWireClient::readyToTransfer);
connect(this, &PeerWireClient::connected,
this, &PeerWireClient::readyToTransfer);
connect(&socket, &QTcpSocket::connected,
this, &PeerWireClient::connected);
connect(&socket, &QTcpSocket::readyRead,
this, &PeerWireClient::readyRead);
connect(&socket, &QTcpSocket::disconnected,
this, &PeerWireClient::disconnected);
connect(&socket, &QTcpSocket::errorOccurred,
this, &PeerWireClient::errorOccurred);
connect(&socket, &QTcpSocket::bytesWritten,
this, &PeerWireClient::bytesWritten);
connect(&socket, &QTcpSocket::stateChanged,
this, &PeerWireClient::socketStateChanged);
}
// Registers the peer ID and SHA1 sum of the torrent, and initiates
// the handshake.
void PeerWireClient::initialize(const QByteArray &infoHash, qint32 pieceCount)
{
this->infoHash = infoHash;
peerPieces.resize(pieceCount);
if (!sentHandShake)
sendHandShake();
}
void PeerWireClient::setPeer(TorrentPeer *peer)
{
torrentPeer = peer;
}
TorrentPeer *PeerWireClient::peer() const
{
return torrentPeer;
}
QBitArray PeerWireClient::availablePieces() const
{
return peerPieces;
}
QList<TorrentBlock> PeerWireClient::incomingBlocks() const
{
return incoming;
}
// Sends a "choke" message, asking the peer to stop requesting blocks.
void PeerWireClient::chokePeer()
{
const char message[] = {0, 0, 0, 1, 0};
write(message, sizeof(message));
pwState |= ChokingPeer;
// After receiving a choke message, the peer will assume all
// pending requests are lost.
pendingBlocks.clear();
pendingBlockSizes = 0;
}
// Sends an "unchoke" message, allowing the peer to start/resume
// requesting blocks.
void PeerWireClient::unchokePeer()
{
const char message[] = {0, 0, 0, 1, 1};
write(message, sizeof(message));
pwState &= ~ChokingPeer;
if (pendingRequestTimer)
killTimer(pendingRequestTimer);
}
// Sends a "keep-alive" message to prevent the peer from closing
// the connection when there's no activity
void PeerWireClient::sendKeepAlive()
{
const char message[] = {0, 0, 0, 0};
write(message, sizeof(message));
}
// Sends an "interested" message, informing the peer that it has got
// pieces that we'd like to download.
void PeerWireClient::sendInterested()
{
const char message[] = {0, 0, 0, 1, 2};
write(message, sizeof(message));
pwState |= InterestedInPeer;
// After telling the peer that we're interested, we expect to get
// unchoked within a certain timeframe; otherwise we'll drop the
// connection.
if (pendingRequestTimer)
killTimer(pendingRequestTimer);
pendingRequestTimer = startTimer(PendingRequestTimeout);
}
// Sends a "not interested" message, informing the peer that it does
// not have any pieces that we'd like to download.
void PeerWireClient::sendNotInterested()
{
const char message[] = {0, 0, 0, 1, 3};
write(message, sizeof(message));
pwState &= ~InterestedInPeer;
}
// Sends a piece notification / a "have" message, informing the peer
// that we have just downloaded a new piece.
void PeerWireClient::sendPieceNotification(qint32 piece)
{
if (!sentHandShake)
sendHandShake();
char message[] = {0, 0, 0, 5, 4, 0, 0, 0, 0};
qToBigEndian(piece, &message[5]);
write(message, sizeof(message));
}
// Sends the complete list of pieces that we have downloaded.
void PeerWireClient::sendPieceList(const QBitArray &bitField)
{
// The bitfield message may only be sent immediately after the
// handshaking sequence is completed, and before any other
// messages are sent.
if (!sentHandShake)
sendHandShake();
// Don't send the bitfield if it's all zeros.
if (bitField.count(true) == 0)
return;
int bitFieldSize = bitField.size();
int size = (bitFieldSize + 7) / 8;
QByteArray bits(size, '\0');
for (int i = 0; i < bitFieldSize; ++i) {
if (bitField.testBit(i)) {
quint32 byte = quint32(i) / 8;
quint32 bit = quint32(i) % 8;
bits[byte] = uchar(bits.at(byte)) | (1 << (7 - bit));
}
}
char message[] = {0, 0, 0, 1, 5};
qToBigEndian<qint32>(bits.size() + 1, &message[0]);
write(message, sizeof(message));
write(bits);
}
// Sends a request for a block.
void PeerWireClient::requestBlock(qint32 piece, qint32 offset, qint32 length)
{
char message[] = {0, 0, 0, 1, 6};
qToBigEndian(13, &message[0]);
write(message, sizeof(message));
char numbers[4 * 3];
qToBigEndian(piece, &numbers[0]);
qToBigEndian(offset, &numbers[4]);
qToBigEndian(length, &numbers[8]);
write(numbers, sizeof(numbers));
incoming << TorrentBlock(piece, offset, length);
// After requesting a block, we expect the block to be sent by the
// other peer within a certain number of seconds. Otherwise, we
// drop the connection.
if (pendingRequestTimer)
killTimer(pendingRequestTimer);
pendingRequestTimer = startTimer(PendingRequestTimeout);
}
// Cancels a request for a block.
void PeerWireClient::cancelRequest(qint32 piece, qint32 offset, qint32 length)
{
char message[] = {0, 0, 0, 1, 8};
qToBigEndian(13, &message[0]);
write(message, sizeof(message));
char numbers[4 * 3];
qToBigEndian(piece, &numbers[0]);
qToBigEndian(offset, &numbers[4]);
qToBigEndian(length, &numbers[8]);
write(numbers, sizeof(numbers));
incoming.removeAll(TorrentBlock(piece, offset, length));
}
// Sends a block to the peer.
void PeerWireClient::sendBlock(qint32 piece, qint32 offset, const QByteArray &data)
{
QByteArray block;
char message[] = {0, 0, 0, 1, 7};
qToBigEndian<qint32>(9 + data.size(), &message[0]);
block += QByteArray(message, sizeof(message));
char numbers[4 * 2];
qToBigEndian(piece, &numbers[0]);
qToBigEndian(offset, &numbers[4]);
block += QByteArray(numbers, sizeof(numbers));
block += data;
BlockInfo blockInfo;
blockInfo.pieceIndex = piece;
blockInfo.offset = offset;
blockInfo.length = data.size();
blockInfo.block = block;
pendingBlocks << blockInfo;
pendingBlockSizes += block.size();
if (pendingBlockSizes > 32 * 16384) {
chokePeer();
unchokePeer();
return;
}
emit readyToTransfer();
}
// Attempts to write 'bytes' bytes to the socket from the buffer.
// This is used by RateController, which precisely controls how much
// each client can write.
qint64 PeerWireClient::writeToSocket(qint64 bytes)
{
qint64 totalWritten = 0;
do {
if (outgoingBuffer.isEmpty() && !pendingBlocks.isEmpty()) {
BlockInfo block = pendingBlocks.takeFirst();
pendingBlockSizes -= block.length;
outgoingBuffer += block.block;
}
qint64 written = socket.write(outgoingBuffer.constData(),
qMin<qint64>(bytes - totalWritten, outgoingBuffer.size()));
if (written <= 0)
return totalWritten ? totalWritten : written;
totalWritten += written;
uploadSpeedData[0] += written;
outgoingBuffer.remove(0, written);
} while (totalWritten < bytes && (!outgoingBuffer.isEmpty() || !pendingBlocks.isEmpty()));
return totalWritten;
}
// Attempts to read at most 'bytes' bytes from the socket.
qint64 PeerWireClient::readFromSocket(qint64 bytes)
{
char buffer[1024];
qint64 totalRead = 0;
do {
qint64 bytesRead = socket.read(buffer, qMin<qint64>(sizeof(buffer), bytes - totalRead));
if (bytesRead <= 0)
break;
qint64 oldSize = incomingBuffer.size();
incomingBuffer.resize(oldSize + bytesRead);
memcpy(incomingBuffer.data() + oldSize, buffer, bytesRead);
totalRead += bytesRead;
} while (totalRead < bytes);
if (totalRead > 0) {
downloadSpeedData[0] += totalRead;
emit bytesReceived(totalRead);
processIncomingData();
}
return totalRead;
}
// Returns the average number of bytes per second this client is
// downloading.
qint64 PeerWireClient::downloadSpeed() const
{
qint64 sum = 0;
for (unsigned int i = 0; i < sizeof(downloadSpeedData) / sizeof(qint64); ++i)
sum += downloadSpeedData[i];
return sum / (8 * 2);
}
// Returns the average number of bytes per second this client is
// uploading.
qint64 PeerWireClient::uploadSpeed() const
{
qint64 sum = 0;
for (unsigned int i = 0; i < sizeof(uploadSpeedData) / sizeof(qint64); ++i)
sum += uploadSpeedData[i];
return sum / (8 * 2);
}
void PeerWireClient::setReadBufferSize(qint64 size)
{
socket.setReadBufferSize(size);
}
bool PeerWireClient::canTransferMore() const
{
return bytesAvailable() > 0 || socket.bytesAvailable() > 0
|| !outgoingBuffer.isEmpty() || !pendingBlocks.isEmpty();
}
void PeerWireClient::connectToHost(const QString &address, quint16 port, OpenMode openMode,
NetworkLayerProtocol protocol)
{
setOpenMode(openMode);
socket.connectToHost(address, port, openMode, protocol);
}
void PeerWireClient::diconnectFromHost()
{
socket.disconnectFromHost();
}
void PeerWireClient::timerEvent(QTimerEvent *event)
{
if (event->timerId() == transferSpeedTimer) {
// Rotate the upload / download records.
for (int i = 6; i >= 0; --i) {
uploadSpeedData[i + 1] = uploadSpeedData[i];
downloadSpeedData[i + 1] = downloadSpeedData[i];
}
uploadSpeedData[0] = 0;
downloadSpeedData[0] = 0;
} else if (event->timerId() == timeoutTimer) {
// Disconnect if we timed out; otherwise the timeout is
// restarted.
if (invalidateTimeout) {
invalidateTimeout = false;
} else {
abort();
emit infoHashReceived(QByteArray());
}
} else if (event->timerId() == pendingRequestTimer) {
abort();
} else if (event->timerId() == keepAliveTimer) {
sendKeepAlive();
}
QTcpSocket::timerEvent(event);
}
// Sends the handshake to the peer.
void PeerWireClient::sendHandShake()
{
sentHandShake = true;
// Restart the timeout
if (timeoutTimer)
killTimer(timeoutTimer);
timeoutTimer = startTimer(ClientTimeout);
// Write the 68 byte PeerWire handshake.
write(&ProtocolIdSize, 1);
write(ProtocolId, ProtocolIdSize);
write(QByteArray(8, '\0'));
write(infoHash);
write(peerIdString);
}
void PeerWireClient::processIncomingData()
{
invalidateTimeout = true;
if (!receivedHandShake) {
// Check that we received enough data
if (bytesAvailable() < MinimalHeaderSize)
return;
// Sanity check the protocol ID
QByteArray id = read(ProtocolIdSize + 1);
if (id.at(0) != ProtocolIdSize || !id.mid(1).startsWith(ProtocolId)) {
abort();
return;
}
// Discard 8 reserved bytes, then read the info hash and peer ID
(void) read(8);
// Read infoHash
QByteArray peerInfoHash = read(20);
if (!infoHash.isEmpty() && peerInfoHash != infoHash) {
abort();
return;
}
emit infoHashReceived(peerInfoHash);
if (infoHash.isEmpty()) {
abort();
return;
}
// Send handshake
if (!sentHandShake)
sendHandShake();
receivedHandShake = true;
}
// Handle delayed peer id arrival
if (!gotPeerId) {
if (bytesAvailable() < 20)
return;
gotPeerId = true;
if (read(20) == peerIdString) {
// We connected to ourself
abort();
return;
}
}
// Initialize keep-alive timer
if (!keepAliveTimer)
keepAliveTimer = startTimer(KeepAliveInterval);
do {
// Find the packet length
if (nextPacketLength == -1) {
if (bytesAvailable() < 4)
return;
char tmp[4];
read(tmp, sizeof(tmp));
nextPacketLength = qFromBigEndian<qint32>(tmp);
if (nextPacketLength < 0 || nextPacketLength > 200000) {
// Prevent DoS
abort();
return;
}
}
// KeepAlive
if (nextPacketLength == 0) {
nextPacketLength = -1;
continue;
}
// Wait with parsing until the whole packet has been received
if (bytesAvailable() < nextPacketLength)
return;
// Read the packet
QByteArray packet = read(nextPacketLength);
if (packet.size() != nextPacketLength) {
abort();
return;
}
switch (packet.at(0)) {
case ChokePacket:
// We have been choked.
pwState |= ChokedByPeer;
incoming.clear();
if (pendingRequestTimer)
killTimer(pendingRequestTimer);
emit choked();
break;
case UnchokePacket:
// We have been unchoked.
pwState &= ~ChokedByPeer;
emit unchoked();
break;
case InterestedPacket:
// The peer is interested in downloading.
pwState |= PeerIsInterested;
emit interested();
break;
case NotInterestedPacket:
// The peer is not interested in downloading.
pwState &= ~PeerIsInterested;
emit notInterested();
break;
case HavePacket: {
// The peer has a new piece available.
quint32 index = qFromBigEndian<quint32>(&packet.data()[1]);
if (index < quint32(peerPieces.size())) {
// Only accept indexes within the valid range.
peerPieces.setBit(int(index));
}
emit piecesAvailable(availablePieces());
break;
}
case BitFieldPacket:
// The peer has the following pieces available.
for (int i = 1; i < packet.size(); ++i) {
for (int bit = 0; bit < 8; ++bit) {
if (packet.at(i) & (1 << (7 - bit))) {
qint32 bitIndex = qint32(((i - 1) * 8) + bit);
if (bitIndex >= 0 && bitIndex < peerPieces.size()) {
// Occasionally, broken clients claim to have
// pieces whose index is outside the valid range.
// The most common mistake is the index == size
// case.
peerPieces.setBit(bitIndex);
}
}
}
}
emit piecesAvailable(availablePieces());
break;
case RequestPacket: {
// The peer requests a block.
quint32 index = qFromBigEndian<quint32>(&packet.data()[1]);
quint32 begin = qFromBigEndian<quint32>(&packet.data()[5]);
quint32 length = qFromBigEndian<quint32>(&packet.data()[9]);
emit blockRequested(qint32(index), qint32(begin), qint32(length));
break;
}
case PiecePacket: {
qint32 index = qint32(qFromBigEndian<quint32>(&packet.data()[1]));
qint32 begin = qint32(qFromBigEndian<quint32>(&packet.data()[5]));
incoming.removeAll(TorrentBlock(index, begin, packet.size() - 9));
// The peer sends a block.
emit blockReceived(index, begin, packet.mid(9));
// Kill the pending block timer.
if (pendingRequestTimer) {
killTimer(pendingRequestTimer);
pendingRequestTimer = 0;
}
break;
}
case CancelPacket: {
// The peer cancels a block request.
quint32 index = qFromBigEndian<quint32>(&packet.data()[1]);
quint32 begin = qFromBigEndian<quint32>(&packet.data()[5]);
quint32 length = qFromBigEndian<quint32>(&packet.data()[9]);
for (int i = 0; i < pendingBlocks.size(); ++i) {
const BlockInfo &blockInfo = pendingBlocks.at(i);
if (blockInfo.pieceIndex == qint32(index)
&& blockInfo.offset == qint32(begin)
&& blockInfo.length == qint32(length)) {
pendingBlocks.removeAt(i);
break;
}
}
break;
}
default:
// Unsupported packet type; just ignore it.
break;
}
nextPacketLength = -1;
} while (bytesAvailable() > 0);
}
void PeerWireClient::socketStateChanged(QAbstractSocket::SocketState state)
{
setLocalAddress(socket.localAddress());
setLocalPort(socket.localPort());
setPeerName(socket.peerName());
setPeerAddress(socket.peerAddress());
setPeerPort(socket.peerPort());
setSocketState(state);
}
qint64 PeerWireClient::readData(char *data, qint64 size)
{
int n = qMin<int>(size, incomingBuffer.size());
memcpy(data, incomingBuffer.constData(), n);
incomingBuffer.remove(0, n);
return n;
}
qint64 PeerWireClient::readLineData(char *data, qint64 maxlen)
{
return QIODevice::readLineData(data, maxlen);
}
qint64 PeerWireClient::writeData(const char *data, qint64 size)
{
int oldSize = outgoingBuffer.size();
outgoingBuffer.resize(oldSize + size);
memcpy(outgoingBuffer.data() + oldSize, data, size);
emit readyToTransfer();
return size;
}

View File

@ -0,0 +1,172 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef PEERWIRECLIENT_H
#define PEERWIRECLIENT_H
#include <QBitArray>
#include <QList>
#include <QTcpSocket>
QT_BEGIN_NAMESPACE
class QHostAddress;
class QTimerEvent;
QT_END_NAMESPACE
class TorrentPeer;
struct TorrentBlock
{
inline TorrentBlock(qint32 p, qint32 o, qint32 l)
: pieceIndex(p), offset(o), length(l)
{
}
inline bool operator==(const TorrentBlock &other) const
{
return pieceIndex == other.pieceIndex
&& offset == other.offset
&& length == other.length;
}
qint32 pieceIndex;
qint32 offset;
qint32 length;
};
class PeerWireClient : public QTcpSocket
{
Q_OBJECT
public:
enum PeerWireStateFlag {
ChokingPeer = 0x1,
InterestedInPeer = 0x2,
ChokedByPeer = 0x4,
PeerIsInterested = 0x8
};
Q_DECLARE_FLAGS(PeerWireState, PeerWireStateFlag)
explicit PeerWireClient(const QByteArray &peerId, QObject *parent = nullptr);
void initialize(const QByteArray &infoHash, int pieceCount);
void setPeer(TorrentPeer *peer);
TorrentPeer *peer() const;
// State
inline PeerWireState peerWireState() const { return pwState; }
QBitArray availablePieces() const;
QList<TorrentBlock> incomingBlocks() const;
// Protocol
void chokePeer();
void unchokePeer();
void sendInterested();
void sendKeepAlive();
void sendNotInterested();
void sendPieceNotification(int piece);
void sendPieceList(const QBitArray &bitField);
void requestBlock(int piece, int offset, int length);
void cancelRequest(int piece, int offset, int length);
void sendBlock(int piece, int offset, const QByteArray &data);
// Rate control
qint64 writeToSocket(qint64 bytes);
qint64 readFromSocket(qint64 bytes);
qint64 downloadSpeed() const;
qint64 uploadSpeed() const;
bool canTransferMore() const;
qint64 bytesAvailable() const override { return incomingBuffer.size() + QTcpSocket::bytesAvailable(); }
qint64 socketBytesAvailable() const { return socket.bytesAvailable(); }
qint64 socketBytesToWrite() const { return socket.bytesToWrite(); }
void setReadBufferSize(qint64 size) override;
using QTcpSocket::connectToHost;
void connectToHost(const QString &address, quint16 port, OpenMode openMode = ReadWrite,
NetworkLayerProtocol protocol = AnyIPProtocol) override;
void diconnectFromHost();
signals:
void infoHashReceived(const QByteArray &infoHash);
void readyToTransfer();
void choked();
void unchoked();
void interested();
void notInterested();
void piecesAvailable(const QBitArray &pieces);
void blockRequested(int pieceIndex, int begin, int length);
void blockReceived(int pieceIndex, int begin, const QByteArray &data);
void bytesReceived(qint64 size);
protected:
void timerEvent(QTimerEvent *event) override;
qint64 readData(char *data, qint64 maxlen) override;
qint64 readLineData(char *data, qint64 maxlen) override;
qint64 writeData(const char *data, qint64 len) override;
private slots:
void sendHandShake();
void processIncomingData();
void socketStateChanged(QAbstractSocket::SocketState state);
private:
// Data waiting to be read/written
QByteArray incomingBuffer;
QByteArray outgoingBuffer;
struct BlockInfo {
int pieceIndex;
int offset;
int length;
QByteArray block;
};
QList<BlockInfo> pendingBlocks;
int pendingBlockSizes;
QList<TorrentBlock> incoming;
enum PacketType {
ChokePacket = 0,
UnchokePacket = 1,
InterestedPacket = 2,
NotInterestedPacket = 3,
HavePacket = 4,
BitFieldPacket = 5,
RequestPacket = 6,
PiecePacket = 7,
CancelPacket = 8
};
// State
PeerWireState pwState;
bool receivedHandShake;
bool gotPeerId;
bool sentHandShake;
int nextPacketLength;
// Upload/download speed records
qint64 uploadSpeedData[8];
qint64 downloadSpeedData[8];
int transferSpeedTimer;
// Timeout handling
int timeoutTimer;
int pendingRequestTimer;
bool invalidateTimeout;
int keepAliveTimer;
// Checksum, peer ID and set of available pieces
QByteArray infoHash;
QByteArray peerIdString;
QBitArray peerPieces;
TorrentPeer *torrentPeer;
QTcpSocket socket;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(PeerWireClient::PeerWireState)
#endif

View File

@ -0,0 +1,120 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "peerwireclient.h"
#include "ratecontroller.h"
#include <QtCore>
Q_GLOBAL_STATIC(RateController, rateController)
RateController *RateController::instance()
{
return rateController();
}
void RateController::addSocket(PeerWireClient *socket)
{
connect(socket, &PeerWireClient::readyToTransfer,
this, &RateController::scheduleTransfer);
socket->setReadBufferSize(downLimit * 4);
sockets << socket;
scheduleTransfer();
}
void RateController::removeSocket(PeerWireClient *socket)
{
disconnect(socket, &PeerWireClient::readyToTransfer,
this, &RateController::scheduleTransfer);
socket->setReadBufferSize(0);
sockets.remove(socket);
}
void RateController::setDownloadLimit(int bytesPerSecond)
{
downLimit = bytesPerSecond;
for (PeerWireClient *socket : std::as_const(sockets))
socket->setReadBufferSize(downLimit * 4);
}
void RateController::scheduleTransfer()
{
if (transferScheduled)
return;
transferScheduled = true;
QTimer::singleShot(50, this, SLOT(transfer()));
}
void RateController::transfer()
{
transferScheduled = false;
if (sockets.isEmpty())
return;
qint64 msecs = 1000;
if (stopWatch.isValid())
msecs = qMin(msecs, stopWatch.elapsed());
qint64 bytesToWrite = (upLimit * msecs) / 1000;
qint64 bytesToRead = (downLimit * msecs) / 1000;
if (bytesToWrite == 0 && bytesToRead == 0) {
scheduleTransfer();
return;
}
QSet<PeerWireClient *> pendingSockets;
for (PeerWireClient *client : std::as_const(sockets)) {
if (client->canTransferMore())
pendingSockets << client;
}
if (pendingSockets.isEmpty())
return;
stopWatch.start();
bool canTransferMore;
do {
canTransferMore = false;
qint64 writeChunk = qMax<qint64>(1, bytesToWrite / pendingSockets.size());
qint64 readChunk = qMax<qint64>(1, bytesToRead / pendingSockets.size());
for (auto it = pendingSockets.begin(), end = pendingSockets.end(); it != end && (bytesToWrite > 0 || bytesToRead > 0); /*erasing*/) {
auto current = it++;
PeerWireClient *socket = *current;
if (socket->state() != QAbstractSocket::ConnectedState) {
it = pendingSockets.erase(current);
continue;
}
bool dataTransferred = false;
qint64 available = qMin<qint64>(socket->socketBytesAvailable(), readChunk);
if (available > 0) {
qint64 readBytes = socket->readFromSocket(qMin<qint64>(available, bytesToRead));
if (readBytes > 0) {
bytesToRead -= readBytes;
dataTransferred = true;
}
}
if (upLimit * 2 > socket->bytesToWrite()) {
qint64 chunkSize = qMin<qint64>(writeChunk, bytesToWrite);
qint64 toWrite = qMin(upLimit * 2 - socket->bytesToWrite(), chunkSize);
if (toWrite > 0) {
qint64 writtenBytes = socket->writeToSocket(toWrite);
if (writtenBytes > 0) {
bytesToWrite -= writtenBytes;
dataTransferred = true;
}
}
}
if (dataTransferred && socket->canTransferMore())
canTransferMore = true;
else
it = pendingSockets.erase(current);
}
} while (canTransferMore && (bytesToWrite > 0 || bytesToRead > 0) && !pendingSockets.isEmpty());
if (canTransferMore || bytesToWrite == 0 || bytesToRead == 0)
scheduleTransfer();
}

View File

@ -0,0 +1,41 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef RATECONTROLLER_H
#define RATECONTROLLER_H
#include <QObject>
#include <QSet>
#include <QElapsedTimer>
class PeerWireClient;
class RateController : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
static RateController *instance();
void addSocket(PeerWireClient *socket);
void removeSocket(PeerWireClient *socket);
inline int uploadLimit() const { return upLimit; }
inline int downloadLimit() const { return downLimit; }
inline void setUploadLimit(int bytesPerSecond) { upLimit = bytesPerSecond; }
void setDownloadLimit(int bytesPerSecond);
public slots:
void transfer();
void scheduleTransfer();
private:
QElapsedTimer stopWatch;
QSet<PeerWireClient *> sockets;
int upLimit = 0;
int downLimit = 0;
bool transferScheduled = false;
};
#endif

View File

@ -0,0 +1,35 @@
QT += network widgets
requires(qtConfig(filedialog))
HEADERS += addtorrentdialog.h \
bencodeparser.h \
connectionmanager.h \
mainwindow.h \
metainfo.h \
peerwireclient.h \
ratecontroller.h \
filemanager.h \
torrentclient.h \
torrentserver.h \
trackerclient.h
SOURCES += main.cpp \
addtorrentdialog.cpp \
bencodeparser.cpp \
connectionmanager.cpp \
mainwindow.cpp \
metainfo.cpp \
peerwireclient.cpp \
ratecontroller.cpp \
filemanager.cpp \
torrentclient.cpp \
torrentserver.cpp \
trackerclient.cpp
# Forms and resources
FORMS += addtorrentform.ui
RESOURCES += icons.qrc
# install
target.path = $$[QT_INSTALL_EXAMPLES]/network/torrent
INSTALLS += target

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,163 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef TORRENTCLIENT_H
#define TORRENTCLIENT_H
#include <QBitArray>
#include <QHostAddress>
#include <QList>
class MetaInfo;
class PeerWireClient;
class TorrentClientPrivate;
struct TorrentPiece;
QT_BEGIN_NAMESPACE
class QTimerEvent;
QT_END_NAMESPACE
class TorrentPeer {
public:
QHostAddress address;
quint16 port;
QString id;
bool interesting;
bool seed;
uint lastVisited;
uint connectStart;
uint connectTime;
QBitArray pieces;
int numCompletedPieces;
inline bool operator==(const TorrentPeer &other)
{
return port == other.port
&& address == other.address
&& id == other.id;
}
};
class TorrentClient : public QObject
{
Q_OBJECT
public:
enum State {
Idle,
Paused,
Stopping,
Preparing,
Searching,
Connecting,
WarmingUp,
Downloading,
Endgame,
Seeding
};
enum Error {
UnknownError,
TorrentParseError,
InvalidTrackerError,
FileError,
ServerError
};
TorrentClient(QObject *parent = nullptr);
~TorrentClient();
bool setTorrent(const QString &fileName);
bool setTorrent(const QByteArray &torrentData);
MetaInfo metaInfo() const;
void setDestinationFolder(const QString &directory);
QString destinationFolder() const;
void setDumpedState(const QByteArray &dumpedState);
QByteArray dumpedState() const;
// Progress and stats for download feedback.
qint64 progress() const;
void setDownloadedBytes(qint64 bytes);
qint64 downloadedBytes() const;
void setUploadedBytes(qint64 bytes);
qint64 uploadedBytes() const;
int connectedPeerCount() const;
int seedCount() const;
// Accessors for the tracker
QByteArray peerId() const;
QByteArray infoHash() const;
quint16 serverPort() const;
// State and error.
State state() const;
QString stateString() const;
Error error() const;
QString errorString() const;
signals:
void stateChanged(TorrentClient::State state);
void error(TorrentClient::Error error);
void downloadCompleted();
void peerInfoUpdated();
void dataSent(int uploadedBytes);
void dataReceived(int downloadedBytes);
void progressUpdated(int percentProgress);
void downloadRateUpdated(int bytesPerSecond);
void uploadRateUpdated(int bytesPerSecond);
void stopped();
public slots:
void start();
void stop();
void setPaused(bool paused);
void setupIncomingConnection(PeerWireClient *client);
protected slots:
void timerEvent(QTimerEvent *event) override;
private slots:
// File management
void sendToPeer(int readId, int pieceIndex, int begin, const QByteArray &data);
void fullVerificationDone();
void pieceVerified(int pieceIndex, bool ok);
void handleFileError();
// Connection handling
void connectToPeers();
QList<TorrentPeer *> weighedFreePeers() const;
void setupOutgoingConnection();
void initializeConnection(PeerWireClient *client);
void removeClient();
void peerPiecesAvailable(const QBitArray &pieces);
void peerRequestsBlock(int pieceIndex, int begin, int length);
void blockReceived(int pieceIndex, int begin, const QByteArray &data);
void peerWireBytesWritten(qint64 bytes);
void peerWireBytesReceived(qint64 bytes);
int blocksLeftForPiece(const TorrentPiece *piece) const;
// Scheduling
void scheduleUploads();
void scheduleDownloads();
void schedulePieceForClient(PeerWireClient *client);
void requestMore(PeerWireClient *client);
int requestBlocks(PeerWireClient *client, TorrentPiece *piece, int maxBlocks);
void peerChoked();
void peerUnchoked();
// Tracker handling
void addToPeerList(const QList<TorrentPeer> &peerList);
void trackerStopped();
// Progress
void updateProgress(int progress = -1);
private:
TorrentClientPrivate *d;
friend class TorrentClientPrivate;
};
#endif

View File

@ -0,0 +1,66 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "connectionmanager.h"
#include "peerwireclient.h"
#include "ratecontroller.h"
#include "torrentclient.h"
#include "torrentserver.h"
Q_GLOBAL_STATIC(TorrentServer, torrentServer)
TorrentServer *TorrentServer::instance()
{
return torrentServer();
}
void TorrentServer::addClient(TorrentClient *client)
{
clients << client;
}
void TorrentServer::removeClient(TorrentClient *client)
{
clients.removeAll(client);
}
void TorrentServer::incomingConnection(qintptr socketDescriptor)
{
PeerWireClient *client =
new PeerWireClient(ConnectionManager::instance()->clientId(), this);
if (client->setSocketDescriptor(socketDescriptor)) {
if (ConnectionManager::instance()->canAddConnection() && !clients.isEmpty()) {
connect(client, &PeerWireClient::infoHashReceived,
this, &TorrentServer::processInfoHash);
connect(client, &PeerWireClient::errorOccurred,
this, QOverload<>::of(&TorrentServer::removeClient));
RateController::instance()->addSocket(client);
ConnectionManager::instance()->addConnection(client);
return;
}
}
client->abort();
delete client;
}
void TorrentServer::removeClient()
{
PeerWireClient *peer = qobject_cast<PeerWireClient *>(sender());
RateController::instance()->removeSocket(peer);
ConnectionManager::instance()->removeConnection(peer);
peer->deleteLater();
}
void TorrentServer::processInfoHash(const QByteArray &infoHash)
{
PeerWireClient *peer = qobject_cast<PeerWireClient *>(sender());
for (TorrentClient *client : std::as_const(clients)) {
if (client->state() >= TorrentClient::Searching && client->infoHash() == infoHash) {
peer->disconnect(peer, nullptr, this, nullptr);
client->setupIncomingConnection(peer);
return;
}
}
removeClient();
}

View File

@ -0,0 +1,34 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef TORRENTSERVER_H
#define TORRENTSERVER_H
#include <QList>
#include <QTcpServer>
class TorrentClient;
class TorrentServer : public QTcpServer
{
Q_OBJECT
public:
inline TorrentServer() {}
static TorrentServer *instance();
void addClient(TorrentClient *client);
void removeClient(TorrentClient *client);
protected:
void incomingConnection(qintptr socketDescriptor) override;
private slots:
void removeClient();
void processInfoHash(const QByteArray &infoHash);
private:
QList<TorrentClient *> clients;
};
#endif

View File

@ -0,0 +1,197 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "bencodeparser.h"
#include "connectionmanager.h"
#include "torrentclient.h"
#include "torrentserver.h"
#include "trackerclient.h"
#include <QtCore>
#include <QNetworkRequest>
TrackerClient::TrackerClient(TorrentClient *downloader, QObject *parent)
: QObject(parent), torrentDownloader(downloader)
{
connect(&http, &QNetworkAccessManager::finished,
this, &TrackerClient::httpRequestDone);
}
void TrackerClient::start(const MetaInfo &info)
{
metaInfo = info;
QTimer::singleShot(0, this, SLOT(fetchPeerList()));
if (metaInfo.fileForm() == MetaInfo::SingleFileForm) {
length = metaInfo.singleFile().length;
} else {
QList<MetaInfoMultiFile> files = metaInfo.multiFiles();
for (int i = 0; i < files.size(); ++i)
length += files.at(i).length;
}
}
void TrackerClient::startSeeding()
{
firstSeeding = true;
fetchPeerList();
}
void TrackerClient::stop()
{
lastTrackerRequest = true;
fetchPeerList();
}
void TrackerClient::timerEvent(QTimerEvent *event)
{
if (event->timerId() == requestIntervalTimer) {
fetchPeerList();
} else {
QObject::timerEvent(event);
}
}
void TrackerClient::fetchPeerList()
{
if (metaInfo.announceUrl().isEmpty())
return;
QUrl url(metaInfo.announceUrl());
// Base the query on announce url to include a passkey (if any)
QUrlQuery query(url);
// Percent encode the hash
const QByteArray infoHash = torrentDownloader->infoHash();
const QByteArray encodedSum = infoHash.toPercentEncoding();
bool seeding = (torrentDownloader->state() == TorrentClient::Seeding);
query.addQueryItem("info_hash", encodedSum);
query.addQueryItem("peer_id", ConnectionManager::instance()->clientId());
query.addQueryItem("port", QByteArray::number(TorrentServer::instance()->serverPort()));
query.addQueryItem("compact", "1");
query.addQueryItem("uploaded", QByteArray::number(torrentDownloader->uploadedBytes()));
if (!firstSeeding) {
query.addQueryItem("downloaded", "0");
query.addQueryItem("left", "0");
} else {
query.addQueryItem("downloaded",
QByteArray::number(torrentDownloader->downloadedBytes()));
int left = qMax<int>(0, metaInfo.totalSize() - torrentDownloader->downloadedBytes());
query.addQueryItem("left", QByteArray::number(seeding ? 0 : left));
}
if (seeding && firstSeeding) {
query.addQueryItem("event", "completed");
firstSeeding = false;
} else if (firstTrackerRequest) {
firstTrackerRequest = false;
query.addQueryItem("event", "started");
} else if(lastTrackerRequest) {
query.addQueryItem("event", "stopped");
}
if (!trackerId.isEmpty())
query.addQueryItem("trackerid", trackerId);
url.setQuery(query);
QNetworkRequest req(url);
if (!url.userName().isEmpty()) {
uname = url.userName();
pwd = url.password();
connect(&http, &QNetworkAccessManager::authenticationRequired,
this, &TrackerClient::provideAuthentication);
}
http.get(req);
}
void TrackerClient::httpRequestDone(QNetworkReply *reply)
{
reply->deleteLater();
if (lastTrackerRequest) {
emit stopped();
return;
}
if (reply->error() != QNetworkReply::NoError) {
emit connectionError(reply->error());
return;
}
QByteArray response = reply->readAll();
reply->abort();
BencodeParser parser;
if (!parser.parse(response)) {
qWarning("Error parsing bencode response from tracker: %s",
qPrintable(parser.errorString()));
return;
}
QMap<QByteArray, QVariant> dict = parser.dictionary();
if (dict.contains("failure reason")) {
// no other items are present
emit failure(QString::fromUtf8(dict.value("failure reason").toByteArray()));
return;
}
if (dict.contains("warning message")) {
// continue processing
emit warning(QString::fromUtf8(dict.value("warning message").toByteArray()));
}
if (dict.contains("tracker id")) {
// store it
trackerId = dict.value("tracker id").toByteArray();
}
if (dict.contains("interval")) {
// Mandatory item
if (requestIntervalTimer != -1)
killTimer(requestIntervalTimer);
requestIntervalTimer = startTimer(std::chrono::seconds(dict.value("interval").toInt()));
}
if (dict.contains("peers")) {
// store it
peers.clear();
QVariant peerEntry = dict.value("peers");
if (peerEntry.userType() == QMetaType::QVariantList) {
QList<QVariant> peerTmp = peerEntry.toList();
for (int i = 0; i < peerTmp.size(); ++i) {
TorrentPeer tmp;
QMap<QByteArray, QVariant> peer = qvariant_cast<QMap<QByteArray, QVariant> >(peerTmp.at(i));
tmp.id = QString::fromUtf8(peer.value("peer id").toByteArray());
tmp.address.setAddress(QString::fromUtf8(peer.value("ip").toByteArray()));
tmp.port = peer.value("port").toInt();
peers << tmp;
}
} else {
QByteArray peerTmp = peerEntry.toByteArray();
for (int i = 0; i < peerTmp.size(); i += 6) {
TorrentPeer tmp;
uchar *data = (uchar *)peerTmp.constData() + i;
tmp.port = (int(data[4]) << 8) + data[5];
uint ipAddress = 0;
ipAddress += uint(data[0]) << 24;
ipAddress += uint(data[1]) << 16;
ipAddress += uint(data[2]) << 8;
ipAddress += uint(data[3]);
tmp.address.setAddress(ipAddress);
peers << tmp;
}
}
emit peerListUpdated(peers);
}
}
void TrackerClient::provideAuthentication(QNetworkReply *reply, QAuthenticator *auth)
{
Q_UNUSED(reply);
auth->setUser(uname);
auth->setPassword(pwd);
}

View File

@ -0,0 +1,68 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef TRACKERCLIENT_H
#define TRACKERCLIENT_H
#include <QByteArray>
#include <QList>
#include <QObject>
#include <QHostAddress>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QAuthenticator>
#include "metainfo.h"
#include "torrentclient.h"
class TorrentClient;
class TrackerClient : public QObject
{
Q_OBJECT
public:
explicit TrackerClient(TorrentClient *downloader, QObject *parent = nullptr);
void start(const MetaInfo &info);
void stop();
void startSeeding();
signals:
void connectionError(QNetworkReply::NetworkError error);
void failure(const QString &reason);
void warning(const QString &message);
void peerListUpdated(const QList<TorrentPeer> &peerList);
void uploadCountUpdated(qint64 newUploadCount);
void downloadCountUpdated(qint64 newDownloadCount);
void stopped();
protected:
void timerEvent(QTimerEvent *event) override;
private slots:
void fetchPeerList();
void httpRequestDone(QNetworkReply *reply);
void provideAuthentication(QNetworkReply *reply, QAuthenticator *auth);
private:
TorrentClient *torrentDownloader;
int requestIntervalTimer = -1;
QNetworkAccessManager http;
MetaInfo metaInfo;
QByteArray trackerId;
QList<TorrentPeer> peers;
qint64 length = 0;
QString uname;
QString pwd;
bool firstTrackerRequest = true;
bool lastTrackerRequest = false;
bool firstSeeding = true;
};
#endif