From 383eaf44ac9966bd7f9eee27e426c108510388c3 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Wed, 24 Mar 2021 11:53:47 +0300 Subject: [PATCH] Implement DBResumeDataStorage class --- src/CMakeLists.txt | 2 +- src/base/CMakeLists.txt | 4 +- src/base/base.pri | 2 + .../bittorrent/bencoderesumedatastorage.cpp | 6 +- .../bittorrent/bencoderesumedatastorage.h | 2 +- src/base/bittorrent/dbresumedatastorage.cpp | 577 ++++++++++++++++++ src/base/bittorrent/dbresumedatastorage.h | 60 ++ src/base/bittorrent/session.cpp | 73 ++- src/base/bittorrent/session.h | 12 +- src/base/bittorrent/torrent.h | 18 +- src/gui/advancedsettings.cpp | 9 + src/gui/advancedsettings.h | 3 +- src/src.pro | 2 +- 13 files changed, 745 insertions(+), 25 deletions(-) create mode 100644 src/base/bittorrent/dbresumedatastorage.cpp create mode 100644 src/base/bittorrent/dbresumedatastorage.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 349f967cd..25c821090 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,7 +35,7 @@ set_property(CACHE LibtorrentRasterbar_DIR PROPERTY TYPE PATH) find_package(Boost ${minBoostVersion} REQUIRED) find_package(OpenSSL ${minOpenSSLVersion} REQUIRED) find_package(ZLIB ${minZlibVersion} REQUIRED) -find_package(Qt5 ${minQtVersion} REQUIRED COMPONENTS Core Network Xml LinguistTools) +find_package(Qt5 ${minQtVersion} REQUIRED COMPONENTS Core Network Sql Xml LinguistTools) if (DBUS) find_package(Qt5 ${minQtVersion} REQUIRED COMPONENTS DBus) set_package_properties(Qt5DBus PROPERTIES diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index b90dfd8af..d151809e4 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(qbt_base STATIC bittorrent/cachestatus.h bittorrent/common.h bittorrent/customstorage.h + bittorrent/dbresumedatastorage.h bittorrent/downloadpriority.h bittorrent/filesearcher.h bittorrent/filterparserthread.h @@ -100,6 +101,7 @@ add_library(qbt_base STATIC bittorrent/bandwidthscheduler.cpp bittorrent/bencoderesumedatastorage.cpp bittorrent/customstorage.cpp + bittorrent/dbresumedatastorage.cpp bittorrent/downloadpriority.cpp bittorrent/filesearcher.cpp bittorrent/filterparserthread.cpp @@ -176,7 +178,7 @@ target_link_libraries(qbt_base ZLIB::ZLIB PUBLIC LibtorrentRasterbar::torrent-rasterbar - Qt5::Core Qt5::Network Qt5::Xml + Qt5::Core Qt5::Network Qt5::Sql Qt5::Xml qbt_common_cfg ) diff --git a/src/base/base.pri b/src/base/base.pri index e39ae6600..7a863b538 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -9,6 +9,7 @@ HEADERS += \ $$PWD/bittorrent/common.h \ $$PWD/bittorrent/customstorage.h \ $$PWD/bittorrent/downloadpriority.h \ + $$PWD/bittorrent/dbresumedatastorage.h \ $$PWD/bittorrent/filesearcher.h \ $$PWD/bittorrent/filterparserthread.h \ $$PWD/bittorrent/infohash.h \ @@ -100,6 +101,7 @@ SOURCES += \ $$PWD/bittorrent/bandwidthscheduler.cpp \ $$PWD/bittorrent/bencoderesumedatastorage.cpp \ $$PWD/bittorrent/customstorage.cpp \ + $$PWD/bittorrent/dbresumedatastorage.cpp \ $$PWD/bittorrent/downloadpriority.cpp \ $$PWD/bittorrent/filesearcher.cpp \ $$PWD/bittorrent/filterparserthread.cpp \ diff --git a/src/base/bittorrent/bencoderesumedatastorage.cpp b/src/base/bittorrent/bencoderesumedatastorage.cpp index ecf45ee3d..e6157caf8 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.cpp +++ b/src/base/bittorrent/bencoderesumedatastorage.cpp @@ -73,8 +73,6 @@ namespace BitTorrent namespace { - const char RESUME_FOLDER[] = "BT_backup"; - template QString fromLTString(const LTStr &str) { @@ -104,9 +102,9 @@ namespace } } -BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(QObject *parent) +BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const QString &path, QObject *parent) : ResumeDataStorage {parent} - , m_resumeDataDir {Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Data) + RESUME_FOLDER)} + , m_resumeDataDir {path} , m_ioThread {new QThread {this}} , m_asyncWorker {new Worker {m_resumeDataDir}} { diff --git a/src/base/bittorrent/bencoderesumedatastorage.h b/src/base/bittorrent/bencoderesumedatastorage.h index 7523cda5c..6653fad1f 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.h +++ b/src/base/bittorrent/bencoderesumedatastorage.h @@ -46,7 +46,7 @@ namespace BitTorrent Q_DISABLE_COPY(BencodeResumeDataStorage) public: - explicit BencodeResumeDataStorage(QObject *parent = nullptr); + explicit BencodeResumeDataStorage(const QString &path, QObject *parent = nullptr); ~BencodeResumeDataStorage() override; QVector registeredTorrents() const override; diff --git a/src/base/bittorrent/dbresumedatastorage.cpp b/src/base/bittorrent/dbresumedatastorage.cpp new file mode 100644 index 000000000..ba4857411 --- /dev/null +++ b/src/base/bittorrent/dbresumedatastorage.cpp @@ -0,0 +1,577 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "dbresumedatastorage.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/exceptions.h" +#include "base/global.h" +#include "base/logger.h" +#include "base/profile.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" +#include "infohash.h" +#include "loadtorrentparams.h" +#include "torrentinfo.h" + +namespace +{ + const char DB_CONNECTION_NAME[] = "ResumeDataStorage"; + + const int DB_VERSION = 1; + + const char DB_TABLE_META[] = "meta"; + const char DB_TABLE_TORRENTS[] = "torrents"; + + struct Column + { + QString name; + QString placeholder; + }; + + Column makeColumn(const char *columnName) + { + return {QLatin1String(columnName), (QLatin1Char(':') + QLatin1String(columnName))}; + } + + const Column DB_COLUMN_ID = makeColumn("id"); + const Column DB_COLUMN_TORRENT_ID = makeColumn("torrent_id"); + const Column DB_COLUMN_QUEUE_POSITION = makeColumn("queue_position"); + const Column DB_COLUMN_NAME = makeColumn("name"); + const Column DB_COLUMN_CATEGORY = makeColumn("category"); + const Column DB_COLUMN_TAGS = makeColumn("tags"); + const Column DB_COLUMN_TARGET_SAVE_PATH = makeColumn("target_save_path"); + const Column DB_COLUMN_CONTENT_LAYOUT = makeColumn("content_layout"); + const Column DB_COLUMN_RATIO_LIMIT = makeColumn("ratio_limit"); + const Column DB_COLUMN_SEEDING_TIME_LIMIT = makeColumn("seeding_time_limit"); + const Column DB_COLUMN_HAS_OUTER_PIECES_PRIORITY = makeColumn("has_outer_pieces_priority"); + const Column DB_COLUMN_HAS_SEED_STATUS = makeColumn("has_seed_status"); + const Column DB_COLUMN_OPERATING_MODE = makeColumn("operating_mode"); + const Column DB_COLUMN_STOPPED = makeColumn("stopped"); + const Column DB_COLUMN_RESUMEDATA = makeColumn("libtorrent_resume_data"); + const Column DB_COLUMN_METADATA = makeColumn("metadata"); + const Column DB_COLUMN_VALUE = makeColumn("value"); + + template + QString fromLTString(const LTStr &str) + { + return QString::fromUtf8(str.data(), static_cast(str.size())); + } + + QString quoted(const QString &name) + { + const QLatin1Char quote {'`'}; + + return (quote + name + quote); + } + + QString makeCreateTableStatement(const QString &tableName, const QStringList &items) + { + return QString::fromLatin1("CREATE TABLE %1 (%2)").arg(quoted(tableName), items.join(QLatin1Char(','))); + } + + QString makeInsertStatement(const QString &tableName, const QVector &columns) + { + QStringList names; + names.reserve(columns.size()); + QStringList values; + values.reserve(columns.size()); + for (const Column &column : columns) + { + names.append(quoted(column.name)); + values.append(column.placeholder); + } + + const QString jointNames = names.join(QLatin1Char(',')); + const QString jointValues = values.join(QLatin1Char(',')); + + return QString::fromLatin1("INSERT INTO %1 (%2) VALUES (%3)") + .arg(quoted(tableName), jointNames, jointValues); + } + + QString makeOnConflictUpdateStatement(const Column &constraint, const QVector &columns) + { + QStringList names; + names.reserve(columns.size()); + QStringList values; + values.reserve(columns.size()); + for (const Column &column : columns) + { + names.append(quoted(column.name)); + values.append(column.placeholder); + } + + const QString jointNames = names.join(QLatin1Char(',')); + const QString jointValues = values.join(QLatin1Char(',')); + + return QString::fromLatin1(" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)") + .arg(quoted(constraint.name), jointNames, jointValues); + } + + QString makeColumnDefinition(const Column &column, const char *definition) + { + return QString::fromLatin1("%1 %2").arg(quoted(column.name), QLatin1String(definition)); + } +} + +namespace BitTorrent +{ + class DBResumeDataStorage::Worker final : public QObject + { + Q_DISABLE_COPY(Worker) + + public: + Worker(const QString &dbPath, const QString &dbConnectionName); + + void openDatabase() const; + void closeDatabase() const; + + void store(const TorrentID &id, const LoadTorrentParams &resumeData) const; + void remove(const TorrentID &id) const; + void storeQueue(const QVector &queue) const; + + private: + const QString m_path; + const QString m_connectionName; + }; +} + +BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const QString &dbPath, QObject *parent) + : ResumeDataStorage {parent} + , m_ioThread {new QThread(this)} +{ + const bool needCreateDB = !QFile::exists(dbPath); + + auto db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), DB_CONNECTION_NAME); + db.setDatabaseName(dbPath); + if (!db.open()) + throw RuntimeError(db.lastError().text()); + + if (needCreateDB) + createDB(); + + m_asyncWorker = new Worker(dbPath, QLatin1String("ResumeDataStorageWorker")); + m_asyncWorker->moveToThread(m_ioThread); + connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater); + m_ioThread->start(); + + RuntimeError *errPtr = nullptr; + QMetaObject::invokeMethod(m_asyncWorker, [this, &errPtr]() + { + try + { + m_asyncWorker->openDatabase(); + } + catch (const RuntimeError &err) + { + errPtr = new RuntimeError(err); + } + }, Qt::BlockingQueuedConnection); + + if (errPtr) + throw *errPtr; +} + +BitTorrent::DBResumeDataStorage::~DBResumeDataStorage() +{ + QMetaObject::invokeMethod(m_asyncWorker, &Worker::closeDatabase); + QSqlDatabase::removeDatabase(DB_CONNECTION_NAME); + + m_ioThread->quit(); + m_ioThread->wait(); +} + +QVector BitTorrent::DBResumeDataStorage::registeredTorrents() const +{ + const auto selectTorrentIDStatement = QString::fromLatin1("SELECT %1 FROM %2 ORDER BY %3;") + .arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name)); + + auto db = QSqlDatabase::database(DB_CONNECTION_NAME); + QSqlQuery query {db}; + + if (!query.exec(selectTorrentIDStatement)) + throw RuntimeError(query.lastError().text()); + + QVector registeredTorrents; + registeredTorrents.reserve(query.size()); + while (query.next()) + registeredTorrents.append(BitTorrent::TorrentID::fromString(query.value(0).toString())); + + return registeredTorrents; +} + +std::optional BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const +{ + const QString selectTorrentStatement = + QString(QLatin1String("SELECT * FROM %1 WHERE %2 = %3;")) + .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder); + + auto db = QSqlDatabase::database(DB_CONNECTION_NAME); + QSqlQuery query {db}; + try + { + if (!query.prepare(selectTorrentStatement)) + throw RuntimeError(query.lastError().text()); + + query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString()); + if (!query.exec()) + throw RuntimeError(query.lastError().text()); + + if (!query.next()) + throw RuntimeError(tr("Not found.")); + } + catch (const RuntimeError &err) + { + LogMsg(tr("Couldn't load resume data of torrent '%1'. Error: %2") + .arg(id.toString(), err.message()), Log::CRITICAL); + return std::nullopt; + } + + LoadTorrentParams resumeData; + resumeData.restored = true; + resumeData.name = query.value(DB_COLUMN_NAME.name).toString(); + resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString(); + const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString(); + if (!tagsData.isEmpty()) + { + const QStringList tagList = tagsData.split(QLatin1Char(',')); + resumeData.tags.insert(tagList.cbegin(), tagList.cend()); + } + resumeData.savePath = Profile::instance()->fromPortablePath( + Utils::Fs::toUniformPath(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString())); + resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool(); + resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool(); + resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0; + resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt(); + resumeData.contentLayout = Utils::String::toEnum( + query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original); + resumeData.operatingMode = Utils::String::toEnum( + query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged); + resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool(); + + const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray(); + + lt::error_code ec; + const lt::bdecode_node root = lt::bdecode(bencodedResumeData, ec); + + lt::add_torrent_params &p = resumeData.ltAddTorrentParams; + + p = lt::read_resume_data(root, ec); + p.save_path = Profile::instance()->fromPortablePath(fromLTString(p.save_path)).toStdString(); + + const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray(); + auto metadata = TorrentInfo::load(bencodedMetadata); + if (metadata.isValid()) + p.ti = metadata.nativeInfo(); + + return resumeData; +} + +void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const +{ + QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData]() + { + m_asyncWorker->store(id, resumeData); + }); +} + +void BitTorrent::DBResumeDataStorage::remove(const BitTorrent::TorrentID &id) const +{ + QMetaObject::invokeMethod(m_asyncWorker, [this, id]() + { + m_asyncWorker->remove(id); + }); +} + +void BitTorrent::DBResumeDataStorage::storeQueue(const QVector &queue) const +{ + QMetaObject::invokeMethod(m_asyncWorker, [this, queue]() + { + m_asyncWorker->storeQueue(queue); + }); +} + +void BitTorrent::DBResumeDataStorage::createDB() const +{ + auto db = QSqlDatabase::database(DB_CONNECTION_NAME); + + if (!db.transaction()) + throw RuntimeError(db.lastError().text()); + + QSqlQuery query {db}; + + try + { + const QStringList tableMetaItems = { + makeColumnDefinition(DB_COLUMN_ID, "INTEGER PRIMARY KEY"), + makeColumnDefinition(DB_COLUMN_NAME, "TEXT NOT NULL UNIQUE"), + makeColumnDefinition(DB_COLUMN_VALUE, "BLOB") + }; + const QString createTableMetaQuery = makeCreateTableStatement(DB_TABLE_META, tableMetaItems); + if (!query.exec(createTableMetaQuery)) + throw RuntimeError(query.lastError().text()); + + const QString insertMetaVersionQuery = makeInsertStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE}); + if (!query.prepare(insertMetaVersionQuery)) + throw RuntimeError(query.lastError().text()); + + query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1("version")); + query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION); + + if (!query.exec()) + throw RuntimeError(query.lastError().text()); + + const QStringList tableTorrentsItems = { + makeColumnDefinition(DB_COLUMN_ID, "INTEGER PRIMARY KEY"), + makeColumnDefinition(DB_COLUMN_TORRENT_ID, "BLOB NOT NULL UNIQUE"), + makeColumnDefinition(DB_COLUMN_QUEUE_POSITION, "INTEGER NOT NULL DEFAULT -1"), + makeColumnDefinition(DB_COLUMN_NAME, "TEXT"), + makeColumnDefinition(DB_COLUMN_CATEGORY, "TEXT"), + makeColumnDefinition(DB_COLUMN_TAGS, "TEXT"), + makeColumnDefinition(DB_COLUMN_TARGET_SAVE_PATH, "TEXT"), + makeColumnDefinition(DB_COLUMN_CONTENT_LAYOUT, "TEXT NOT NULL"), + makeColumnDefinition(DB_COLUMN_RATIO_LIMIT, "INTEGER NOT NULL"), + makeColumnDefinition(DB_COLUMN_SEEDING_TIME_LIMIT, "INTEGER NOT NULL"), + makeColumnDefinition(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY, "INTEGER NOT NULL"), + makeColumnDefinition(DB_COLUMN_HAS_SEED_STATUS, "INTEGER NOT NULL"), + makeColumnDefinition(DB_COLUMN_OPERATING_MODE, "TEXT NOT NULL"), + makeColumnDefinition(DB_COLUMN_STOPPED, "INTEGER NOT NULL"), + makeColumnDefinition(DB_COLUMN_RESUMEDATA, "BLOB NOT NULL"), + makeColumnDefinition(DB_COLUMN_METADATA, "BLOB") + }; + const QString createTableTorrentsQuery = makeCreateTableStatement(DB_TABLE_TORRENTS, tableTorrentsItems); + if (!query.exec(createTableTorrentsQuery)) + throw RuntimeError(query.lastError().text()); + + if (!db.commit()) + throw RuntimeError(db.lastError().text()); + } + catch (const RuntimeError &err) + { + db.rollback(); + throw err; + } +} + +BitTorrent::DBResumeDataStorage::Worker::Worker(const QString &dbPath, const QString &dbConnectionName) + : m_path {dbPath} + , m_connectionName {dbConnectionName} +{ +} + +void BitTorrent::DBResumeDataStorage::Worker::openDatabase() const +{ + auto db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), m_connectionName); + db.setDatabaseName(m_path); + if (!db.open()) + throw RuntimeError(db.lastError().text()); +} + +void BitTorrent::DBResumeDataStorage::Worker::closeDatabase() const +{ + QSqlDatabase::removeDatabase(m_connectionName); +} + +void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const LoadTorrentParams &resumeData) const +{ + // We need to adjust native libtorrent resume data + lt::add_torrent_params p = resumeData.ltAddTorrentParams; + p.save_path = Profile::instance()->toPortablePath(QString::fromStdString(p.save_path)).toStdString(); + if (resumeData.stopped) + { + p.flags |= lt::torrent_flags::paused; + p.flags &= ~lt::torrent_flags::auto_managed; + } + else + { + // Torrent can be actually "running" but temporarily "paused" to perform some + // service jobs behind the scenes so we need to restore it as "running" + if (resumeData.operatingMode == BitTorrent::TorrentOperatingMode::AutoManaged) + { + p.flags |= lt::torrent_flags::auto_managed; + } + else + { + p.flags &= ~lt::torrent_flags::paused; + p.flags &= ~lt::torrent_flags::auto_managed; + } + } + + QVector columns { + DB_COLUMN_TORRENT_ID, + DB_COLUMN_NAME, + DB_COLUMN_CATEGORY, + DB_COLUMN_TAGS, + DB_COLUMN_TARGET_SAVE_PATH, + DB_COLUMN_CONTENT_LAYOUT, + DB_COLUMN_RATIO_LIMIT, + DB_COLUMN_SEEDING_TIME_LIMIT, + DB_COLUMN_HAS_OUTER_PIECES_PRIORITY, + DB_COLUMN_HAS_SEED_STATUS, + DB_COLUMN_OPERATING_MODE, + DB_COLUMN_STOPPED, + DB_COLUMN_RESUMEDATA + }; + + // metadata is stored in separate column + QByteArray bencodedMetadata; + bencodedMetadata.reserve(512 * 1024); + const std::shared_ptr torrentInfo = std::move(p.ti); + if (torrentInfo) + { + const lt::create_torrent torrentCreator = lt::create_torrent(*torrentInfo); + const lt::entry metadata = torrentCreator.generate(); + lt::bencode(std::back_inserter(bencodedMetadata), metadata); + + columns.append(DB_COLUMN_METADATA); + } + + QByteArray bencodedResumeData; + bencodedResumeData.reserve(256 * 1024); + lt::bencode(std::back_inserter(bencodedResumeData), lt::write_resume_data(p)); + + const QString insertTorrentStatement = makeInsertStatement(DB_TABLE_TORRENTS, columns) + + makeOnConflictUpdateStatement(DB_COLUMN_TORRENT_ID, columns); + auto db = QSqlDatabase::database(m_connectionName); + QSqlQuery query {db}; + + try + { + if (!query.prepare(insertTorrentStatement)) + throw RuntimeError(query.lastError().text()); + + query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString()); + query.bindValue(DB_COLUMN_NAME.placeholder, resumeData.name); + query.bindValue(DB_COLUMN_CATEGORY.placeholder, resumeData.category); + query.bindValue(DB_COLUMN_TAGS.placeholder, (resumeData.tags.isEmpty() + ? QVariant(QVariant::String) : resumeData.tags.join(QLatin1String(",")))); + query.bindValue(DB_COLUMN_TARGET_SAVE_PATH.placeholder, Profile::instance()->toPortablePath(resumeData.savePath)); + query.bindValue(DB_COLUMN_CONTENT_LAYOUT.placeholder, Utils::String::fromEnum(resumeData.contentLayout)); + query.bindValue(DB_COLUMN_RATIO_LIMIT.placeholder, static_cast(resumeData.ratioLimit * 1000)); + query.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT.placeholder, resumeData.seedingTimeLimit); + query.bindValue(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.placeholder, resumeData.firstLastPiecePriority); + query.bindValue(DB_COLUMN_HAS_SEED_STATUS.placeholder, resumeData.hasSeedStatus); + query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(resumeData.operatingMode)); + query.bindValue(DB_COLUMN_STOPPED.placeholder, resumeData.stopped); + query.bindValue(DB_COLUMN_RESUMEDATA.placeholder, bencodedResumeData); + if (torrentInfo) + query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata); + + if (!query.exec()) + throw RuntimeError(query.lastError().text()); + } + catch (const RuntimeError &err) + { + LogMsg(tr("Couldn't store resume data for torrent '%1'. Error: %2") + .arg(id.toString(), err.message()), Log::CRITICAL); + } +} + +void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id) const +{ + const auto deleteTorrentStatement = QString::fromLatin1("DELETE FROM %1 WHERE %2 = %3;") + .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder); + + auto db = QSqlDatabase::database(m_connectionName); + QSqlQuery query {db}; + + try + { + if (!query.prepare(deleteTorrentStatement)) + throw RuntimeError(query.lastError().text()); + + query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString()); + if (!query.exec()) + throw RuntimeError(query.lastError().text()); + } + catch (const RuntimeError &err) + { + LogMsg(tr("Couldn't delete resume data of torrent '%1'. Error: %2") + .arg(id.toString(), err.message()), Log::CRITICAL); + } +} + +void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector &queue) const +{ + const auto updateQueuePosStatement = QString::fromLatin1("UPDATE %1 SET %2 = %3 WHERE %4 = %5;") + .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name), DB_COLUMN_QUEUE_POSITION.placeholder + , quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder); + + auto db = QSqlDatabase::database(m_connectionName); + + try + { + if (!db.transaction()) + throw RuntimeError(db.lastError().text()); + + QSqlQuery query {db}; + + try + { + if (!query.prepare(updateQueuePosStatement)) + throw RuntimeError(query.lastError().text()); + + int pos = 0; + for (const TorrentID &torrentID : queue) + { + query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, torrentID.toString()); + query.bindValue(DB_COLUMN_QUEUE_POSITION.placeholder, pos++); + if (!query.exec()) + throw RuntimeError(query.lastError().text()); + } + + if (!db.commit()) + throw RuntimeError(db.lastError().text()); + } + catch (const RuntimeError &err) + { + db.rollback(); + throw err; + } + } + catch (const RuntimeError &err) + { + LogMsg(tr("Couldn't store torrents queue positions. Error: %1") + .arg(err.message()), Log::CRITICAL); + } +} diff --git a/src/base/bittorrent/dbresumedatastorage.h b/src/base/bittorrent/dbresumedatastorage.h new file mode 100644 index 000000000..775656322 --- /dev/null +++ b/src/base/bittorrent/dbresumedatastorage.h @@ -0,0 +1,60 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include "resumedatastorage.h" + +class QThread; + +namespace BitTorrent +{ + class DBResumeDataStorage final : public ResumeDataStorage + { + Q_OBJECT + Q_DISABLE_COPY(DBResumeDataStorage) + + public: + explicit DBResumeDataStorage(const QString &dbPath, QObject *parent = nullptr); + ~DBResumeDataStorage() override; + + QVector registeredTorrents() const override; + std::optional load(const TorrentID &id) const override; + void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override; + void remove(const TorrentID &id) const override; + void storeQueue(const QVector &queue) const override; + + private: + void createDB() const; + + QThread *m_ioThread = nullptr; + + class Worker; + Worker *m_asyncWorker = nullptr; + }; +} diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 9aefb3a30..f1b22cf9f 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -88,6 +88,7 @@ #include "bencoderesumedatastorage.h" #include "common.h" #include "customstorage.h" +#include "dbresumedatastorage.h" #include "filesearcher.h" #include "filterparserthread.h" #include "loadtorrentparams.h" @@ -436,6 +437,7 @@ Session::Session(QObject *parent) return tmp; } ) + , m_resumeDataStorageType(BITTORRENT_SESSION_KEY("ResumeDataStorageType"), ResumeDataStorageType::Legacy) #if defined(Q_OS_WIN) , m_OSMemoryPriority(BITTORRENT_KEY("OSMemoryPriority"), OSMemoryPriority::BelowNormal) #endif @@ -451,8 +453,6 @@ Session::Session(QObject *parent) if (port() < 0) m_port = Utils::Random::rand(1024, 65535); - initResumeDataStorage(); - m_recentErroredTorrentsTimer->setSingleShot(true); m_recentErroredTorrentsTimer->setInterval(1000); connect(m_recentErroredTorrentsTimer, &QTimer::timeout @@ -2938,6 +2938,16 @@ void Session::setBannedIPs(const QStringList &newList) configureDeferred(); } +ResumeDataStorageType Session::resumeDataStorageType() const +{ + return m_resumeDataStorageType; +} + +void Session::setResumeDataStorageType(const ResumeDataStorageType type) +{ + m_resumeDataStorageType = type; +} + QStringList Session::bannedIPs() const { return m_bannedIPs; @@ -4035,11 +4045,6 @@ bool Session::hasPerTorrentSeedingTimeLimit() const }); } -void Session::initResumeDataStorage() -{ - m_resumeDataStorage = new BencodeResumeDataStorage(this); -} - void Session::configureDeferred() { if (m_deferredConfigureScheduled) @@ -4118,18 +4123,56 @@ const CacheStatus &Session::cacheStatus() const return m_cacheStatus; } -// Will resume torrents in backup directory void Session::startUpTorrents() { + qDebug("Initializing torrents resume data storage..."); + + const QString dbPath = Utils::Fs::expandPathAbs( + specialFolderLocation(SpecialFolder::Data) + QLatin1String("torrents.db")); + const bool dbStorageExists = QFile::exists(dbPath); + + ResumeDataStorage *startupStorage = nullptr; + if (resumeDataStorageType() == ResumeDataStorageType::SQLite) + { + m_resumeDataStorage = new DBResumeDataStorage(dbPath, this); + + if (!dbStorageExists) + { + const QString dataPath = Utils::Fs::expandPathAbs( + specialFolderLocation(SpecialFolder::Data) + QLatin1String("BT_backup")); + startupStorage = new BencodeResumeDataStorage(dataPath, this); + } + } + else + { + const QString dataPath = Utils::Fs::expandPathAbs( + specialFolderLocation(SpecialFolder::Data) + QLatin1String("BT_backup")); + m_resumeDataStorage = new BencodeResumeDataStorage(dataPath, this); + + if (dbStorageExists) + startupStorage = new DBResumeDataStorage(dbPath, this); + } + + if (!startupStorage) + startupStorage = m_resumeDataStorage; + qDebug("Starting up torrents..."); - const QVector torrents = m_resumeDataStorage->registeredTorrents(); + const QVector torrents = startupStorage->registeredTorrents(); int resumedTorrentsCount = 0; + QVector queue; for (const TorrentID &torrentID : torrents) { - const std::optional resumeData = m_resumeDataStorage->load(torrentID); + const std::optional resumeData = startupStorage->load(torrentID); if (resumeData) { + if (m_resumeDataStorage != startupStorage) + { + m_resumeDataStorage->store(torrentID, *resumeData); + if (isQueueingSystemEnabled() && !resumeData->hasSeedStatus) + queue.append(torrentID); + } + qDebug() << "Starting up torrent" << torrentID.toString() << "..."; if (!loadTorrent(*resumeData)) LogMsg(tr("Unable to resume torrent '%1'.", "e.g: Unable to resume torrent 'hash'.") @@ -4146,6 +4189,16 @@ void Session::startUpTorrents() .arg(torrentID.toString()), Log::CRITICAL); } } + + if (m_resumeDataStorage != startupStorage) + { + delete startupStorage; + if (resumeDataStorageType() == ResumeDataStorageType::Legacy) + Utils::Fs::forceRemove(dbPath); + + if (isQueueingSystemEnabled()) + m_resumeDataStorage->storeQueue(queue); + } } quint64 Session::getAlltimeDL() const diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index 344f95433..9754ce529 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -142,6 +142,13 @@ namespace BitTorrent }; Q_ENUM_NS(SeedChokingAlgorithm) + enum class ResumeDataStorageType + { + Legacy, + SQLite + }; + Q_ENUM_NS(ResumeDataStorageType) + #if defined(Q_OS_WIN) enum class OSMemoryPriority : int { @@ -429,6 +436,8 @@ namespace BitTorrent void setTrackerFilteringEnabled(bool enabled); QStringList bannedIPs() const; void setBannedIPs(const QStringList &newList); + ResumeDataStorageType resumeDataStorageType() const; + void setResumeDataStorageType(ResumeDataStorageType type); #if defined(Q_OS_WIN) OSMemoryPriority getOSMemoryPriority() const; void setOSMemoryPriority(OSMemoryPriority priority); @@ -570,8 +579,6 @@ namespace BitTorrent bool hasPerTorrentRatioLimit() const; bool hasPerTorrentSeedingTimeLimit() const; - void initResumeDataStorage(); - // Session configuration Q_INVOKABLE void configure(); void configureComponents(); @@ -738,6 +745,7 @@ namespace BitTorrent CachedSettingValue m_peerTurnoverCutoff; CachedSettingValue m_peerTurnoverInterval; CachedSettingValue m_bannedIPs; + CachedSettingValue m_resumeDataStorageType; #if defined(Q_OS_WIN) CachedSettingValue m_OSMemoryPriority; #endif diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index 6da873021..ae9da421a 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -50,11 +50,21 @@ namespace BitTorrent struct PeerAddress; struct TrackerEntry; - enum class TorrentOperatingMode + // Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised + // since `Q_NAMESPACE` cannot be used when the same namespace resides at different files. + // https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779 + inline namespace TorrentOperatingModeNS { - AutoManaged = 0, - Forced = 1 - }; + Q_NAMESPACE + + enum class TorrentOperatingMode + { + AutoManaged = 0, + Forced = 1 + }; + + Q_ENUM_NS(TorrentOperatingMode) + } enum class TorrentState { diff --git a/src/gui/advancedsettings.cpp b/src/gui/advancedsettings.cpp index 0d8f32a7e..c070ed6fb 100644 --- a/src/gui/advancedsettings.cpp +++ b/src/gui/advancedsettings.cpp @@ -61,6 +61,7 @@ namespace { // qBittorrent section QBITTORRENT_HEADER, + RESUME_DATA_STORAGE, #if defined(Q_OS_WIN) OS_MEMORY_PRIORITY, #endif @@ -166,6 +167,10 @@ void AdvancedSettings::saveAdvancedSettings() Preferences *const pref = Preferences::instance(); BitTorrent::Session *const session = BitTorrent::Session::instance(); + session->setResumeDataStorageType((m_comboBoxResumeDataStorage.currentIndex() == 0) + ? BitTorrent::ResumeDataStorageType::Legacy + : BitTorrent::ResumeDataStorageType::SQLite); + #if defined(Q_OS_WIN) BitTorrent::OSMemoryPriority prio = BitTorrent::OSMemoryPriority::Normal; switch (m_comboBoxOSMemoryPriority.currentIndex()) @@ -392,6 +397,10 @@ void AdvancedSettings::loadAdvancedSettings() addRow(LIBTORRENT_HEADER, QString::fromLatin1("%1").arg(tr("libtorrent Section")), labelLibtorrentLink); static_cast(cellWidget(LIBTORRENT_HEADER, PROPERTY))->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); + m_comboBoxResumeDataStorage.addItems({tr("Fastresume files"), tr("SQLite database (experimental)")}); + m_comboBoxResumeDataStorage.setCurrentIndex((session->resumeDataStorageType() == BitTorrent::ResumeDataStorageType::Legacy) ? 0 : 1); + addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage); + #if defined(Q_OS_WIN) m_comboBoxOSMemoryPriority.addItems({tr("Normal"), tr("Below normal"), tr("Medium"), tr("Low"), tr("Very low")}); int OSMemoryPriorityIndex = 0; diff --git a/src/gui/advancedsettings.h b/src/gui/advancedsettings.h index 8bf07c43e..27ed46f1e 100644 --- a/src/gui/advancedsettings.h +++ b/src/gui/advancedsettings.h @@ -70,7 +70,8 @@ private: m_checkBoxConfirmTorrentRecheck, m_checkBoxConfirmRemoveAllTags, m_checkBoxAnnounceAllTrackers, m_checkBoxAnnounceAllTiers, m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity, m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport; - QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm, m_comboBoxSeedChokingAlgorithm; + QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm, + m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage; QLineEdit m_lineEditAnnounceIP; #if (LIBTORRENT_VERSION_NUM < 20000) diff --git a/src/src.pro b/src/src.pro index 0ae7e2f64..fc48051b8 100644 --- a/src/src.pro +++ b/src/src.pro @@ -7,7 +7,7 @@ win32: include(../winconf.pri) macx: include(../macxconf.pri) unix:!macx: include(../unixconf.pri) -QT += network xml +QT += network sql xml macx|*-clang*: QMAKE_CXXFLAGS_WARN_ON += -Wno-range-loop-analysis