qt 6.5.1 original
68
examples/network/torrent/CMakeLists.txt
Normal 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}"
|
||||
)
|
131
examples/network/torrent/addtorrentdialog.cpp
Normal 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();
|
||||
}
|
36
examples/network/torrent/addtorrentdialog.h
Normal 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
|
266
examples/network/torrent/addtorrentform.ui
Normal 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><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><none></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="3" >
|
||||
<widget class="QLabel" name="creatorLabel" >
|
||||
<property name="text" >
|
||||
<string><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>&OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton" >
|
||||
<property name="text" >
|
||||
<string>&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>
|
197
examples/network/torrent/bencodeparser.cpp
Normal 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;
|
||||
}
|
43
examples/network/torrent/bencodeparser.h
Normal 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
|
49
examples/network/torrent/connectionmanager.cpp
Normal 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;
|
||||
}
|
28
examples/network/torrent/connectionmanager.h
Normal 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
|
414
examples/network/torrent/filemanager.cpp
Normal 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();
|
||||
}
|
106
examples/network/torrent/filemanager.h
Normal 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
|
12
examples/network/torrent/icons.qrc
Normal 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>
|
BIN
examples/network/torrent/icons/1downarrow.png
Normal file
After Width: | Height: | Size: 895 B |
BIN
examples/network/torrent/icons/1uparrow.png
Normal file
After Width: | Height: | Size: 822 B |
BIN
examples/network/torrent/icons/bottom.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
examples/network/torrent/icons/edit_add.png
Normal file
After Width: | Height: | Size: 394 B |
BIN
examples/network/torrent/icons/edit_remove.png
Normal file
After Width: | Height: | Size: 368 B |
BIN
examples/network/torrent/icons/exit.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
examples/network/torrent/icons/peertopeer.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
examples/network/torrent/icons/player_pause.png
Normal file
After Width: | Height: | Size: 690 B |
BIN
examples/network/torrent/icons/player_play.png
Normal file
After Width: | Height: | Size: 900 B |
BIN
examples/network/torrent/icons/player_stop.png
Normal file
After Width: | Height: | Size: 627 B |
BIN
examples/network/torrent/icons/stop.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
18
examples/network/torrent/main.cpp
Normal 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();
|
||||
}
|
690
examples/network/torrent/mainwindow.cpp
Normal 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"
|
94
examples/network/torrent/mainwindow.h
Normal 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
|
180
examples/network/torrent/metainfo.cpp
Normal 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;
|
||||
}
|
84
examples/network/torrent/metainfo.h
Normal 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
|
610
examples/network/torrent/peerwireclient.cpp
Normal 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;
|
||||
}
|
172
examples/network/torrent/peerwireclient.h
Normal 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
|
120
examples/network/torrent/ratecontroller.cpp
Normal 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();
|
||||
}
|
41
examples/network/torrent/ratecontroller.h
Normal 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
|
35
examples/network/torrent/torrent.pro
Normal 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
|
1488
examples/network/torrent/torrentclient.cpp
Normal file
163
examples/network/torrent/torrentclient.h
Normal 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
|
66
examples/network/torrent/torrentserver.cpp
Normal 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();
|
||||
}
|
34
examples/network/torrent/torrentserver.h
Normal 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
|
197
examples/network/torrent/trackerclient.cpp
Normal 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);
|
||||
}
|
68
examples/network/torrent/trackerclient.h
Normal 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
|