Allow to refresh existing search
[qBittorrent.git] / src / base / bittorrent / dbresumedatastorage.cpp
blobd9320e5286daa3eab4fb6fc813218bfdfb03393d
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2021-2023 Vladimir Golovnev <glassez@yandex.ru>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 #include "dbresumedatastorage.h"
31 #include <memory>
32 #include <queue>
33 #include <utility>
35 #include <libtorrent/bdecode.hpp>
36 #include <libtorrent/bencode.hpp>
37 #include <libtorrent/entry.hpp>
38 #include <libtorrent/read_resume_data.hpp>
39 #include <libtorrent/torrent_info.hpp>
40 #include <libtorrent/write_resume_data.hpp>
42 #include <QByteArray>
43 #include <QDebug>
44 #include <QList>
45 #include <QMutex>
46 #include <QSet>
47 #include <QSqlDatabase>
48 #include <QSqlError>
49 #include <QSqlQuery>
50 #include <QSqlRecord>
51 #include <QThread>
52 #include <QWaitCondition>
54 #include "base/exceptions.h"
55 #include "base/global.h"
56 #include "base/logger.h"
57 #include "base/path.h"
58 #include "base/preferences.h"
59 #include "base/profile.h"
60 #include "base/utils/fs.h"
61 #include "base/utils/sslkey.h"
62 #include "base/utils/string.h"
63 #include "infohash.h"
64 #include "loadtorrentparams.h"
66 namespace
68 const QString DB_CONNECTION_NAME = u"ResumeDataStorage"_s;
70 const int DB_VERSION = 8;
72 const QString DB_TABLE_META = u"meta"_s;
73 const QString DB_TABLE_TORRENTS = u"torrents"_s;
75 const QString META_VERSION = u"version"_s;
77 using namespace BitTorrent;
79 class Job
81 public:
82 virtual ~Job() = default;
83 virtual void perform(QSqlDatabase db) = 0;
86 class StoreJob final : public Job
88 public:
89 StoreJob(const TorrentID &torrentID, const LoadTorrentParams &resumeData);
90 void perform(QSqlDatabase db) override;
92 private:
93 const TorrentID m_torrentID;
94 const LoadTorrentParams m_resumeData;
97 class RemoveJob final : public Job
99 public:
100 explicit RemoveJob(const TorrentID &torrentID);
101 void perform(QSqlDatabase db) override;
103 private:
104 const TorrentID m_torrentID;
107 class StoreQueueJob final : public Job
109 public:
110 explicit StoreQueueJob(const QList<TorrentID> &queue);
111 void perform(QSqlDatabase db) override;
113 private:
114 const QList<TorrentID> m_queue;
117 struct Column
119 QString name;
120 QString placeholder;
123 Column makeColumn(const QString &columnName)
125 return {.name = columnName, .placeholder = (u':' + columnName)};
128 const Column DB_COLUMN_ID = makeColumn(u"id"_s);
129 const Column DB_COLUMN_TORRENT_ID = makeColumn(u"torrent_id"_s);
130 const Column DB_COLUMN_QUEUE_POSITION = makeColumn(u"queue_position"_s);
131 const Column DB_COLUMN_NAME = makeColumn(u"name"_s);
132 const Column DB_COLUMN_CATEGORY = makeColumn(u"category"_s);
133 const Column DB_COLUMN_TAGS = makeColumn(u"tags"_s);
134 const Column DB_COLUMN_TARGET_SAVE_PATH = makeColumn(u"target_save_path"_s);
135 const Column DB_COLUMN_DOWNLOAD_PATH = makeColumn(u"download_path"_s);
136 const Column DB_COLUMN_CONTENT_LAYOUT = makeColumn(u"content_layout"_s);
137 const Column DB_COLUMN_RATIO_LIMIT = makeColumn(u"ratio_limit"_s);
138 const Column DB_COLUMN_SEEDING_TIME_LIMIT = makeColumn(u"seeding_time_limit"_s);
139 const Column DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT = makeColumn(u"inactive_seeding_time_limit"_s);
140 const Column DB_COLUMN_SHARE_LIMIT_ACTION = makeColumn(u"share_limit_action"_s);
141 const Column DB_COLUMN_HAS_OUTER_PIECES_PRIORITY = makeColumn(u"has_outer_pieces_priority"_s);
142 const Column DB_COLUMN_HAS_SEED_STATUS = makeColumn(u"has_seed_status"_s);
143 const Column DB_COLUMN_OPERATING_MODE = makeColumn(u"operating_mode"_s);
144 const Column DB_COLUMN_STOPPED = makeColumn(u"stopped"_s);
145 const Column DB_COLUMN_STOP_CONDITION = makeColumn(u"stop_condition"_s);
146 const Column DB_COLUMN_SSL_CERTIFICATE = makeColumn(u"ssl_certificate"_s);
147 const Column DB_COLUMN_SSL_PRIVATE_KEY = makeColumn(u"ssl_private_key"_s);
148 const Column DB_COLUMN_SSL_DH_PARAMS = makeColumn(u"ssl_dh_params"_s);
149 const Column DB_COLUMN_RESUMEDATA = makeColumn(u"libtorrent_resume_data"_s);
150 const Column DB_COLUMN_METADATA = makeColumn(u"metadata"_s);
151 const Column DB_COLUMN_VALUE = makeColumn(u"value"_s);
153 template <typename LTStr>
154 QString fromLTString(const LTStr &str)
156 return QString::fromUtf8(str.data(), static_cast<qsizetype>(str.size()));
159 QString quoted(const QString &name)
161 const QChar quote = u'`';
162 return (quote + name + quote);
165 QString makeCreateTableStatement(const QString &tableName, const QStringList &items)
167 return u"CREATE TABLE %1 (%2)"_s.arg(quoted(tableName), items.join(u','));
170 std::pair<QString, QString> joinColumns(const QList<Column> &columns)
172 int namesSize = columns.size();
173 int valuesSize = columns.size();
174 for (const Column &column : columns)
176 namesSize += column.name.size() + 2;
177 valuesSize += column.placeholder.size();
180 QString names;
181 names.reserve(namesSize);
182 QString values;
183 values.reserve(valuesSize);
184 for (const Column &column : columns)
186 names.append(quoted(column.name) + u',');
187 values.append(column.placeholder + u',');
189 names.chop(1);
190 values.chop(1);
192 return std::make_pair(names, values);
195 QString makeInsertStatement(const QString &tableName, const QList<Column> &columns)
197 const auto [names, values] = joinColumns(columns);
198 return u"INSERT INTO %1 (%2) VALUES (%3)"_s
199 .arg(quoted(tableName), names, values);
202 QString makeUpdateStatement(const QString &tableName, const QList<Column> &columns)
204 const auto [names, values] = joinColumns(columns);
205 return u"UPDATE %1 SET (%2) = (%3)"_s
206 .arg(quoted(tableName), names, values);
209 QString makeOnConflictUpdateStatement(const Column &constraint, const QList<Column> &columns)
211 const auto [names, values] = joinColumns(columns);
212 return u" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)"_s
213 .arg(quoted(constraint.name), names, values);
216 QString makeColumnDefinition(const Column &column, const QString &definition)
218 return u"%1 %2"_s.arg(quoted(column.name), definition);
221 LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
223 LoadTorrentParams resumeData;
224 resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
225 resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
226 const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
227 if (!tagsData.isEmpty())
229 const QStringList tagList = tagsData.split(u',');
230 resumeData.tags.insert(tagList.cbegin(), tagList.cend());
232 resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
233 resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
234 resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
235 resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
236 resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
237 resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
238 query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
239 resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
240 query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
241 resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
242 query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
243 resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
244 resumeData.stopCondition = Utils::String::toEnum(
245 query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
246 resumeData.sslParameters =
248 .certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
249 .privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
250 .dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
253 resumeData.savePath = Profile::instance()->fromPortablePath(
254 Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
255 resumeData.useAutoTMM = resumeData.savePath.isEmpty();
256 if (!resumeData.useAutoTMM)
258 resumeData.downloadPath = Profile::instance()->fromPortablePath(
259 Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
262 const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
263 const auto *pref = Preferences::instance();
264 const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
265 const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
267 lt::error_code ec;
268 const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec
269 , nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
271 lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
273 p = lt::read_resume_data(resumeDataRoot, ec);
275 if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
276 ; !bencodedMetadata.isEmpty())
278 const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
279 , nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
280 p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
283 p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
284 .toString().toStdString();
286 if (p.flags & lt::torrent_flags::stop_when_ready)
288 p.flags &= ~lt::torrent_flags::stop_when_ready;
289 resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
292 return resumeData;
296 namespace BitTorrent
298 class DBResumeDataStorage::Worker final : public QThread
300 Q_DISABLE_COPY_MOVE(Worker)
302 public:
303 Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent = nullptr);
305 void run() override;
306 void requestInterruption();
308 void store(const TorrentID &id, const LoadTorrentParams &resumeData);
309 void remove(const TorrentID &id);
310 void storeQueue(const QList<TorrentID> &queue);
312 private:
313 void addJob(std::unique_ptr<Job> job);
315 const QString m_connectionName = u"ResumeDataStorageWorker"_s;
316 const Path m_path;
317 QReadWriteLock &m_dbLock;
319 std::queue<std::unique_ptr<Job>> m_jobs;
320 QMutex m_jobsMutex;
321 QWaitCondition m_waitCondition;
325 BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject *parent)
326 : ResumeDataStorage(dbPath, parent)
328 const bool needCreateDB = !dbPath.exists();
330 auto db = QSqlDatabase::addDatabase(u"QSQLITE"_s, DB_CONNECTION_NAME);
331 db.setDatabaseName(dbPath.data());
332 if (!db.open())
333 throw RuntimeError(db.lastError().text());
335 if (needCreateDB)
337 createDB();
339 else
341 const int dbVersion = (!db.record(DB_TABLE_TORRENTS).contains(DB_COLUMN_DOWNLOAD_PATH.name) ? 1 : currentDBVersion());
342 if (dbVersion < DB_VERSION)
343 updateDB(dbVersion);
346 m_asyncWorker = new Worker(dbPath, m_dbLock, this);
347 m_asyncWorker->start();
350 BitTorrent::DBResumeDataStorage::~DBResumeDataStorage()
352 m_asyncWorker->requestInterruption();
353 m_asyncWorker->wait();
354 QSqlDatabase::removeDatabase(DB_CONNECTION_NAME);
357 QList<BitTorrent::TorrentID> BitTorrent::DBResumeDataStorage::registeredTorrents() const
359 const auto selectTorrentIDStatement = u"SELECT %1 FROM %2 ORDER BY %3;"_s
360 .arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
362 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
363 QSqlQuery query {db};
365 if (!query.exec(selectTorrentIDStatement))
366 throw RuntimeError(query.lastError().text());
368 QList<TorrentID> registeredTorrents;
369 registeredTorrents.reserve(query.size());
370 while (query.next())
371 registeredTorrents.append(BitTorrent::TorrentID::fromString(query.value(0).toString()));
373 return registeredTorrents;
376 BitTorrent::LoadResumeDataResult BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
378 const QString selectTorrentStatement = u"SELECT * FROM %1 WHERE %2 = %3;"_s
379 .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
381 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
382 QSqlQuery query {db};
385 if (!query.prepare(selectTorrentStatement))
386 throw RuntimeError(query.lastError().text());
388 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
389 if (!query.exec())
390 throw RuntimeError(query.lastError().text());
392 if (!query.next())
393 throw RuntimeError(tr("Not found."));
395 catch (const RuntimeError &err)
397 return nonstd::make_unexpected(tr("Couldn't load resume data of torrent '%1'. Error: %2")
398 .arg(id.toString(), err.message()));
401 return parseQueryResultRow(query);
404 void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
406 m_asyncWorker->store(id, resumeData);
409 void BitTorrent::DBResumeDataStorage::remove(const BitTorrent::TorrentID &id) const
411 m_asyncWorker->remove(id);
414 void BitTorrent::DBResumeDataStorage::storeQueue(const QList<TorrentID> &queue) const
416 m_asyncWorker->storeQueue(queue);
419 void BitTorrent::DBResumeDataStorage::doLoadAll() const
421 const QString connectionName = u"ResumeDataStorageLoadAll"_s;
424 auto db = QSqlDatabase::addDatabase(u"QSQLITE"_s, connectionName);
425 db.setDatabaseName(path().data());
426 if (!db.open())
427 throw RuntimeError(db.lastError().text());
429 QSqlQuery query {db};
431 const auto selectTorrentIDStatement = u"SELECT %1 FROM %2 ORDER BY %3;"_s
432 .arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
434 const QReadLocker locker {&m_dbLock};
436 if (!query.exec(selectTorrentIDStatement))
437 throw RuntimeError(query.lastError().text());
439 QList<TorrentID> registeredTorrents;
440 registeredTorrents.reserve(query.size());
441 while (query.next())
442 registeredTorrents.append(TorrentID::fromString(query.value(0).toString()));
444 emit const_cast<DBResumeDataStorage *>(this)->loadStarted(registeredTorrents);
446 const auto selectStatement = u"SELECT * FROM %1 ORDER BY %2;"_s.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
447 if (!query.exec(selectStatement))
448 throw RuntimeError(query.lastError().text());
450 while (query.next())
452 const auto torrentID = TorrentID::fromString(query.value(DB_COLUMN_TORRENT_ID.name).toString());
453 onResumeDataLoaded(torrentID, parseQueryResultRow(query));
457 emit const_cast<DBResumeDataStorage *>(this)->loadFinished();
459 QSqlDatabase::removeDatabase(connectionName);
462 int BitTorrent::DBResumeDataStorage::currentDBVersion() const
464 const auto selectDBVersionStatement = u"SELECT %1 FROM %2 WHERE %3 = %4;"_s
465 .arg(quoted(DB_COLUMN_VALUE.name), quoted(DB_TABLE_META), quoted(DB_COLUMN_NAME.name), DB_COLUMN_NAME.placeholder);
467 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
468 QSqlQuery query {db};
470 if (!query.prepare(selectDBVersionStatement))
471 throw RuntimeError(query.lastError().text());
473 query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
475 const QReadLocker locker {&m_dbLock};
477 if (!query.exec())
478 throw RuntimeError(query.lastError().text());
480 if (!query.next())
481 throw RuntimeError(tr("Database is corrupted."));
483 bool ok;
484 const int dbVersion = query.value(0).toInt(&ok);
485 if (!ok)
486 throw RuntimeError(tr("Database is corrupted."));
488 return dbVersion;
491 void BitTorrent::DBResumeDataStorage::createDB() const
495 enableWALMode();
497 catch (const RuntimeError &err)
499 LogMsg(tr("Couldn't enable Write-Ahead Logging (WAL) journaling mode. Error: %1.")
500 .arg(err.message()), Log::WARNING);
503 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
505 if (!db.transaction())
506 throw RuntimeError(db.lastError().text());
508 QSqlQuery query {db};
512 const QStringList tableMetaItems = {
513 makeColumnDefinition(DB_COLUMN_ID, u"INTEGER PRIMARY KEY"_s),
514 makeColumnDefinition(DB_COLUMN_NAME, u"TEXT NOT NULL UNIQUE"_s),
515 makeColumnDefinition(DB_COLUMN_VALUE, u"BLOB"_s)
517 const QString createTableMetaQuery = makeCreateTableStatement(DB_TABLE_META, tableMetaItems);
518 if (!query.exec(createTableMetaQuery))
519 throw RuntimeError(query.lastError().text());
521 const QString insertMetaVersionQuery = makeInsertStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
522 if (!query.prepare(insertMetaVersionQuery))
523 throw RuntimeError(query.lastError().text());
525 query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
526 query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
528 if (!query.exec())
529 throw RuntimeError(query.lastError().text());
531 const QStringList tableTorrentsItems = {
532 makeColumnDefinition(DB_COLUMN_ID, u"INTEGER PRIMARY KEY"_s),
533 makeColumnDefinition(DB_COLUMN_TORRENT_ID, u"BLOB NOT NULL UNIQUE"_s),
534 makeColumnDefinition(DB_COLUMN_QUEUE_POSITION, u"INTEGER NOT NULL DEFAULT -1"_s),
535 makeColumnDefinition(DB_COLUMN_NAME, u"TEXT"_s),
536 makeColumnDefinition(DB_COLUMN_CATEGORY, u"TEXT"_s),
537 makeColumnDefinition(DB_COLUMN_TAGS, u"TEXT"_s),
538 makeColumnDefinition(DB_COLUMN_TARGET_SAVE_PATH, u"TEXT"_s),
539 makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, u"TEXT"_s),
540 makeColumnDefinition(DB_COLUMN_CONTENT_LAYOUT, u"TEXT NOT NULL"_s),
541 makeColumnDefinition(DB_COLUMN_RATIO_LIMIT, u"INTEGER NOT NULL"_s),
542 makeColumnDefinition(DB_COLUMN_SEEDING_TIME_LIMIT, u"INTEGER NOT NULL"_s),
543 makeColumnDefinition(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT, u"INTEGER NOT NULL"_s),
544 makeColumnDefinition(DB_COLUMN_SHARE_LIMIT_ACTION, u"TEXT NOT NULL DEFAULT `Default`"_s),
545 makeColumnDefinition(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY, u"INTEGER NOT NULL"_s),
546 makeColumnDefinition(DB_COLUMN_HAS_SEED_STATUS, u"INTEGER NOT NULL"_s),
547 makeColumnDefinition(DB_COLUMN_OPERATING_MODE, u"TEXT NOT NULL"_s),
548 makeColumnDefinition(DB_COLUMN_STOPPED, u"INTEGER NOT NULL"_s),
549 makeColumnDefinition(DB_COLUMN_STOP_CONDITION, u"TEXT NOT NULL DEFAULT `None`"_s),
550 makeColumnDefinition(DB_COLUMN_SSL_CERTIFICATE, u"TEXT"_s),
551 makeColumnDefinition(DB_COLUMN_SSL_PRIVATE_KEY, u"TEXT"_s),
552 makeColumnDefinition(DB_COLUMN_SSL_DH_PARAMS, u"TEXT"_s),
553 makeColumnDefinition(DB_COLUMN_RESUMEDATA, u"BLOB NOT NULL"_s),
554 makeColumnDefinition(DB_COLUMN_METADATA, u"BLOB"_s)
556 const QString createTableTorrentsQuery = makeCreateTableStatement(DB_TABLE_TORRENTS, tableTorrentsItems);
557 if (!query.exec(createTableTorrentsQuery))
558 throw RuntimeError(query.lastError().text());
560 const QString torrentsQueuePositionIndexName = u"%1_%2_INDEX"_s.arg(DB_TABLE_TORRENTS, DB_COLUMN_QUEUE_POSITION.name);
561 const QString createTorrentsQueuePositionIndexQuery = u"CREATE INDEX %1 ON %2 (%3)"_s
562 .arg(quoted(torrentsQueuePositionIndexName), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
563 if (!query.exec(createTorrentsQueuePositionIndexQuery))
564 throw RuntimeError(query.lastError().text());
566 if (!db.commit())
567 throw RuntimeError(db.lastError().text());
569 catch (const RuntimeError &)
571 db.rollback();
572 throw;
576 void BitTorrent::DBResumeDataStorage::updateDB(const int fromVersion) const
578 Q_ASSERT(fromVersion > 0);
579 Q_ASSERT(fromVersion != DB_VERSION);
581 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
583 const QWriteLocker locker {&m_dbLock};
585 if (!db.transaction())
586 throw RuntimeError(db.lastError().text());
588 QSqlQuery query {db};
592 const auto addColumn = [&query](const QString &table, const Column &column, const QString &definition)
594 const auto testQuery = u"SELECT COUNT(%1) FROM %2;"_s.arg(quoted(column.name), quoted(table));
595 if (query.exec(testQuery))
596 return;
598 const auto alterTableQuery = u"ALTER TABLE %1 ADD %2"_s.arg(quoted(table), makeColumnDefinition(column, definition));
599 if (!query.exec(alterTableQuery))
600 throw RuntimeError(query.lastError().text());
603 if (fromVersion <= 1)
604 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_DOWNLOAD_PATH, u"TEXT"_s);
606 if (fromVersion <= 2)
607 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_STOP_CONDITION, u"TEXT NOT NULL DEFAULT `None`"_s);
609 if (fromVersion <= 3)
611 const QString torrentsQueuePositionIndexName = u"%1_%2_INDEX"_s.arg(DB_TABLE_TORRENTS, DB_COLUMN_QUEUE_POSITION.name);
612 const QString createTorrentsQueuePositionIndexQuery = u"CREATE INDEX IF NOT EXISTS %1 ON %2 (%3)"_s
613 .arg(quoted(torrentsQueuePositionIndexName), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
614 if (!query.exec(createTorrentsQueuePositionIndexQuery))
615 throw RuntimeError(query.lastError().text());
618 if (fromVersion <= 4)
619 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT, u"INTEGER NOT NULL DEFAULT -2"_s);
621 if (fromVersion <= 5)
623 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SSL_CERTIFICATE, u"TEXT"_s);
624 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SSL_PRIVATE_KEY, u"TEXT"_s);
625 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SSL_DH_PARAMS, u"TEXT"_s);
628 if (fromVersion <= 6)
629 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SHARE_LIMIT_ACTION, u"TEXT NOT NULL DEFAULT `Default`"_s);
631 if (fromVersion == 7)
633 const QString TEMP_COLUMN_NAME = DB_COLUMN_SHARE_LIMIT_ACTION.name + u"_temp";
635 auto queryStr = u"ALTER TABLE %1 ADD %2 %3"_s
636 .arg(quoted(DB_TABLE_TORRENTS), TEMP_COLUMN_NAME, u"TEXT NOT NULL DEFAULT `Default`");
637 if (!query.exec(queryStr))
638 throw RuntimeError(query.lastError().text());
640 queryStr = u"UPDATE %1 SET %2 = %3"_s
641 .arg(quoted(DB_TABLE_TORRENTS), quoted(TEMP_COLUMN_NAME), quoted(DB_COLUMN_SHARE_LIMIT_ACTION.name));
642 if (!query.exec(queryStr))
643 throw RuntimeError(query.lastError().text());
645 queryStr = u"ALTER TABLE %1 DROP %2"_s.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_SHARE_LIMIT_ACTION.name));
646 if (!query.exec(queryStr))
647 throw RuntimeError(query.lastError().text());
649 queryStr = u"ALTER TABLE %1 RENAME %2 TO %3"_s
650 .arg(quoted(DB_TABLE_TORRENTS), quoted(TEMP_COLUMN_NAME), quoted(DB_COLUMN_SHARE_LIMIT_ACTION.name));
651 if (!query.exec(queryStr))
652 throw RuntimeError(query.lastError().text());
655 const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
656 if (!query.prepare(updateMetaVersionQuery))
657 throw RuntimeError(query.lastError().text());
659 query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
660 query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
662 if (!query.exec())
663 throw RuntimeError(query.lastError().text());
665 if (!db.commit())
666 throw RuntimeError(db.lastError().text());
668 catch (const RuntimeError &)
670 db.rollback();
671 throw;
675 void BitTorrent::DBResumeDataStorage::enableWALMode() const
677 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
678 QSqlQuery query {db};
680 if (!query.exec(u"PRAGMA journal_mode = WAL;"_s))
681 throw RuntimeError(query.lastError().text());
683 if (!query.next())
684 throw RuntimeError(tr("Couldn't obtain query result."));
686 const QString result = query.value(0).toString();
687 if (result.compare(u"WAL"_s, Qt::CaseInsensitive) != 0)
688 throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
691 BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent)
692 : QThread(parent)
693 , m_path {dbPath}
694 , m_dbLock {dbLock}
698 void BitTorrent::DBResumeDataStorage::Worker::run()
701 auto db = QSqlDatabase::addDatabase(u"QSQLITE"_s, m_connectionName);
702 db.setDatabaseName(m_path.data());
703 if (!db.open())
704 throw RuntimeError(db.lastError().text());
706 int64_t transactedJobsCount = 0;
707 while (true)
709 m_jobsMutex.lock();
710 if (m_jobs.empty())
712 if (transactedJobsCount > 0)
714 db.commit();
715 m_dbLock.unlock();
717 qDebug() << "Resume data changes are committed. Transacted jobs:" << transactedJobsCount;
718 transactedJobsCount = 0;
721 if (isInterruptionRequested())
723 m_jobsMutex.unlock();
724 break;
727 m_waitCondition.wait(&m_jobsMutex);
728 if (isInterruptionRequested())
730 m_jobsMutex.unlock();
731 break;
734 m_dbLock.lockForWrite();
735 if (!db.transaction())
737 LogMsg(tr("Couldn't begin transaction. Error: %1").arg(db.lastError().text()), Log::WARNING);
738 m_dbLock.unlock();
739 break;
742 std::unique_ptr<Job> job = std::move(m_jobs.front());
743 m_jobs.pop();
744 m_jobsMutex.unlock();
746 job->perform(db);
747 ++transactedJobsCount;
750 db.close();
753 QSqlDatabase::removeDatabase(m_connectionName);
756 void DBResumeDataStorage::Worker::requestInterruption()
758 QThread::requestInterruption();
759 m_waitCondition.wakeAll();
762 void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const LoadTorrentParams &resumeData)
764 addJob(std::make_unique<StoreJob>(id, resumeData));
767 void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id)
769 addJob(std::make_unique<RemoveJob>(id));
772 void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QList<TorrentID> &queue)
774 addJob(std::make_unique<StoreQueueJob>(queue));
777 void BitTorrent::DBResumeDataStorage::Worker::addJob(std::unique_ptr<Job> job)
779 m_jobsMutex.lock();
780 m_jobs.push(std::move(job));
781 m_jobsMutex.unlock();
783 m_waitCondition.wakeAll();
786 namespace
788 using namespace BitTorrent;
790 StoreJob::StoreJob(const TorrentID &torrentID, const LoadTorrentParams &resumeData)
791 : m_torrentID {torrentID}
792 , m_resumeData {resumeData}
796 void StoreJob::perform(QSqlDatabase db)
798 // We need to adjust native libtorrent resume data
799 lt::add_torrent_params p = m_resumeData.ltAddTorrentParams;
800 p.save_path = Profile::instance()->toPortablePath(Path(p.save_path))
801 .toString().toStdString();
802 if (m_resumeData.stopped)
804 p.flags |= lt::torrent_flags::paused;
805 p.flags &= ~lt::torrent_flags::auto_managed;
807 else
809 // Torrent can be actually "running" but temporarily "paused" to perform some
810 // service jobs behind the scenes so we need to restore it as "running"
811 if (m_resumeData.operatingMode == BitTorrent::TorrentOperatingMode::AutoManaged)
813 p.flags |= lt::torrent_flags::auto_managed;
815 else
817 p.flags &= ~lt::torrent_flags::paused;
818 p.flags &= ~lt::torrent_flags::auto_managed;
822 QList<Column> columns {
823 DB_COLUMN_TORRENT_ID,
824 DB_COLUMN_NAME,
825 DB_COLUMN_CATEGORY,
826 DB_COLUMN_TAGS,
827 DB_COLUMN_TARGET_SAVE_PATH,
828 DB_COLUMN_DOWNLOAD_PATH,
829 DB_COLUMN_CONTENT_LAYOUT,
830 DB_COLUMN_RATIO_LIMIT,
831 DB_COLUMN_SEEDING_TIME_LIMIT,
832 DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT,
833 DB_COLUMN_SHARE_LIMIT_ACTION,
834 DB_COLUMN_HAS_OUTER_PIECES_PRIORITY,
835 DB_COLUMN_HAS_SEED_STATUS,
836 DB_COLUMN_OPERATING_MODE,
837 DB_COLUMN_STOPPED,
838 DB_COLUMN_STOP_CONDITION,
839 DB_COLUMN_SSL_CERTIFICATE,
840 DB_COLUMN_SSL_PRIVATE_KEY,
841 DB_COLUMN_SSL_DH_PARAMS,
842 DB_COLUMN_RESUMEDATA
845 lt::entry data = lt::write_resume_data(p);
847 // metadata is stored in separate column
848 QByteArray bencodedMetadata;
849 if (p.ti)
851 lt::entry::dictionary_type &dataDict = data.dict();
852 lt::entry metadata {lt::entry::dictionary_t};
853 lt::entry::dictionary_type &metadataDict = metadata.dict();
854 metadataDict.insert(dataDict.extract("info"));
855 metadataDict.insert(dataDict.extract("creation date"));
856 metadataDict.insert(dataDict.extract("created by"));
857 metadataDict.insert(dataDict.extract("comment"));
861 bencodedMetadata.reserve(512 * 1024);
862 lt::bencode(std::back_inserter(bencodedMetadata), metadata);
864 catch (const std::exception &err)
866 LogMsg(ResumeDataStorage::tr("Couldn't save torrent metadata. Error: %1.")
867 .arg(QString::fromLocal8Bit(err.what())), Log::CRITICAL);
868 return;
871 columns.append(DB_COLUMN_METADATA);
874 QByteArray bencodedResumeData;
875 bencodedResumeData.reserve(256 * 1024);
876 lt::bencode(std::back_inserter(bencodedResumeData), data);
878 const QString insertTorrentStatement = makeInsertStatement(DB_TABLE_TORRENTS, columns)
879 + makeOnConflictUpdateStatement(DB_COLUMN_TORRENT_ID, columns);
880 QSqlQuery query {db};
884 if (!query.prepare(insertTorrentStatement))
885 throw RuntimeError(query.lastError().text());
887 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, m_torrentID.toString());
888 query.bindValue(DB_COLUMN_NAME.placeholder, m_resumeData.name);
889 query.bindValue(DB_COLUMN_CATEGORY.placeholder, m_resumeData.category);
890 query.bindValue(DB_COLUMN_TAGS.placeholder, (m_resumeData.tags.isEmpty()
891 ? QString() : Utils::String::joinIntoString(m_resumeData.tags, u","_s)));
892 query.bindValue(DB_COLUMN_CONTENT_LAYOUT.placeholder, Utils::String::fromEnum(m_resumeData.contentLayout));
893 query.bindValue(DB_COLUMN_RATIO_LIMIT.placeholder, static_cast<int>(m_resumeData.ratioLimit * 1000));
894 query.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT.placeholder, m_resumeData.seedingTimeLimit);
895 query.bindValue(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.placeholder, m_resumeData.inactiveSeedingTimeLimit);
896 query.bindValue(DB_COLUMN_SHARE_LIMIT_ACTION.placeholder, Utils::String::fromEnum(m_resumeData.shareLimitAction));
897 query.bindValue(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.placeholder, m_resumeData.firstLastPiecePriority);
898 query.bindValue(DB_COLUMN_HAS_SEED_STATUS.placeholder, m_resumeData.hasFinishedStatus);
899 query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(m_resumeData.operatingMode));
900 query.bindValue(DB_COLUMN_STOPPED.placeholder, m_resumeData.stopped);
901 query.bindValue(DB_COLUMN_STOP_CONDITION.placeholder, Utils::String::fromEnum(m_resumeData.stopCondition));
902 query.bindValue(DB_COLUMN_SSL_CERTIFICATE.placeholder, QString::fromLatin1(m_resumeData.sslParameters.certificate.toPem()));
903 query.bindValue(DB_COLUMN_SSL_PRIVATE_KEY.placeholder, QString::fromLatin1(m_resumeData.sslParameters.privateKey.toPem()));
904 query.bindValue(DB_COLUMN_SSL_DH_PARAMS.placeholder, QString::fromLatin1(m_resumeData.sslParameters.dhParams));
906 if (!m_resumeData.useAutoTMM)
908 query.bindValue(DB_COLUMN_TARGET_SAVE_PATH.placeholder, Profile::instance()->toPortablePath(m_resumeData.savePath).data());
909 query.bindValue(DB_COLUMN_DOWNLOAD_PATH.placeholder, Profile::instance()->toPortablePath(m_resumeData.downloadPath).data());
912 query.bindValue(DB_COLUMN_RESUMEDATA.placeholder, bencodedResumeData);
913 if (!bencodedMetadata.isEmpty())
914 query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
916 if (!query.exec())
917 throw RuntimeError(query.lastError().text());
919 catch (const RuntimeError &err)
921 LogMsg(ResumeDataStorage::tr("Couldn't store resume data for torrent '%1'. Error: %2")
922 .arg(m_torrentID.toString(), err.message()), Log::CRITICAL);
926 RemoveJob::RemoveJob(const TorrentID &torrentID)
927 : m_torrentID {torrentID}
931 void RemoveJob::perform(QSqlDatabase db)
933 const auto deleteTorrentStatement = u"DELETE FROM %1 WHERE %2 = %3;"_s
934 .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
936 QSqlQuery query {db};
939 if (!query.prepare(deleteTorrentStatement))
940 throw RuntimeError(query.lastError().text());
942 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, m_torrentID.toString());
944 if (!query.exec())
945 throw RuntimeError(query.lastError().text());
947 catch (const RuntimeError &err)
949 LogMsg(ResumeDataStorage::tr("Couldn't delete resume data of torrent '%1'. Error: %2")
950 .arg(m_torrentID.toString(), err.message()), Log::CRITICAL);
954 StoreQueueJob::StoreQueueJob(const QList<TorrentID> &queue)
955 : m_queue {queue}
959 void StoreQueueJob::perform(QSqlDatabase db)
961 const auto updateQueuePosStatement = u"UPDATE %1 SET %2 = %3 WHERE %4 = %5;"_s
962 .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name), DB_COLUMN_QUEUE_POSITION.placeholder
963 , quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
967 QSqlQuery query {db};
969 if (!query.prepare(updateQueuePosStatement))
970 throw RuntimeError(query.lastError().text());
972 int pos = 0;
973 for (const TorrentID &torrentID : m_queue)
975 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, torrentID.toString());
976 query.bindValue(DB_COLUMN_QUEUE_POSITION.placeholder, pos++);
977 if (!query.exec())
978 throw RuntimeError(query.lastError().text());
981 catch (const RuntimeError &err)
983 LogMsg(ResumeDataStorage::tr("Couldn't store torrents queue positions. Error: %1")
984 .arg(err.message()), Log::CRITICAL);