Vladimir Golovnev (Glassez)
4 years ago
13 changed files with 745 additions and 25 deletions
@ -0,0 +1,577 @@
@@ -0,0 +1,577 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2021 Vladimir Golovnev <glassez@yandex.ru> |
||||
* |
||||
* 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 <libtorrent/bdecode.hpp> |
||||
#include <libtorrent/bencode.hpp> |
||||
#include <libtorrent/create_torrent.hpp> |
||||
#include <libtorrent/entry.hpp> |
||||
#include <libtorrent/read_resume_data.hpp> |
||||
#include <libtorrent/write_resume_data.hpp> |
||||
|
||||
#include <QByteArray> |
||||
#include <QFile> |
||||
#include <QSet> |
||||
#include <QSqlDatabase> |
||||
#include <QSqlError> |
||||
#include <QSqlQuery> |
||||
#include <QThread> |
||||
#include <QVector> |
||||
|
||||
#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 <typename LTStr> |
||||
QString fromLTString(const LTStr &str) |
||||
{ |
||||
return QString::fromUtf8(str.data(), static_cast<int>(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<Column> &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<Column> &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<TorrentID> &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::TorrentID> 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<TorrentID> registeredTorrents; |
||||
registeredTorrents.reserve(query.size()); |
||||
while (query.next()) |
||||
registeredTorrents.append(BitTorrent::TorrentID::fromString(query.value(0).toString())); |
||||
|
||||
return registeredTorrents; |
||||
} |
||||
|
||||
std::optional<BitTorrent::LoadTorrentParams> 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<TorrentContentLayout>( |
||||
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original); |
||||
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>( |
||||
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<TorrentID> &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<Column> 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<lt::torrent_info> 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<int>(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<TorrentID> &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); |
||||
} |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2021 Vladimir Golovnev <glassez@yandex.ru> |
||||
* |
||||
* 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<TorrentID> registeredTorrents() const override; |
||||
std::optional<LoadTorrentParams> 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<TorrentID> &queue) const override; |
||||
|
||||
private: |
||||
void createDB() const; |
||||
|
||||
QThread *m_ioThread = nullptr; |
||||
|
||||
class Worker; |
||||
Worker *m_asyncWorker = nullptr; |
||||
}; |
||||
} |
Loading…
Reference in new issue