Enable customizing the save statistics time interval
[qBittorrent.git] / src / base / bittorrent / dbresumedatastorage.cpp
blobc42d68d122f0f851726a3bd538e08e7a7e6762d9
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 = 7;
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 char *columnName)
125 const QString name = QString::fromLatin1(columnName);
126 return {.name = name, .placeholder = (u':' + name)};
129 const Column DB_COLUMN_ID = makeColumn("id");
130 const Column DB_COLUMN_TORRENT_ID = makeColumn("torrent_id");
131 const Column DB_COLUMN_QUEUE_POSITION = makeColumn("queue_position");
132 const Column DB_COLUMN_NAME = makeColumn("name");
133 const Column DB_COLUMN_CATEGORY = makeColumn("category");
134 const Column DB_COLUMN_TAGS = makeColumn("tags");
135 const Column DB_COLUMN_TARGET_SAVE_PATH = makeColumn("target_save_path");
136 const Column DB_COLUMN_DOWNLOAD_PATH = makeColumn("download_path");
137 const Column DB_COLUMN_CONTENT_LAYOUT = makeColumn("content_layout");
138 const Column DB_COLUMN_RATIO_LIMIT = makeColumn("ratio_limit");
139 const Column DB_COLUMN_SEEDING_TIME_LIMIT = makeColumn("seeding_time_limit");
140 const Column DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT = makeColumn("inactive_seeding_time_limit");
141 const Column DB_COLUMN_SHARE_LIMIT_ACTION = makeColumn("share_limit_action");
142 const Column DB_COLUMN_HAS_OUTER_PIECES_PRIORITY = makeColumn("has_outer_pieces_priority");
143 const Column DB_COLUMN_HAS_SEED_STATUS = makeColumn("has_seed_status");
144 const Column DB_COLUMN_OPERATING_MODE = makeColumn("operating_mode");
145 const Column DB_COLUMN_STOPPED = makeColumn("stopped");
146 const Column DB_COLUMN_STOP_CONDITION = makeColumn("stop_condition");
147 const Column DB_COLUMN_SSL_CERTIFICATE = makeColumn("ssl_certificate");
148 const Column DB_COLUMN_SSL_PRIVATE_KEY = makeColumn("ssl_private_key");
149 const Column DB_COLUMN_SSL_DH_PARAMS = makeColumn("ssl_dh_params");
150 const Column DB_COLUMN_RESUMEDATA = makeColumn("libtorrent_resume_data");
151 const Column DB_COLUMN_METADATA = makeColumn("metadata");
152 const Column DB_COLUMN_VALUE = makeColumn("value");
154 template <typename LTStr>
155 QString fromLTString(const LTStr &str)
157 return QString::fromUtf8(str.data(), static_cast<qsizetype>(str.size()));
160 QString quoted(const QString &name)
162 const QChar quote = u'`';
163 return (quote + name + quote);
166 QString makeCreateTableStatement(const QString &tableName, const QStringList &items)
168 return u"CREATE TABLE %1 (%2)"_s.arg(quoted(tableName), items.join(u','));
171 std::pair<QString, QString> joinColumns(const QList<Column> &columns)
173 int namesSize = columns.size();
174 int valuesSize = columns.size();
175 for (const Column &column : columns)
177 namesSize += column.name.size() + 2;
178 valuesSize += column.placeholder.size();
181 QString names;
182 names.reserve(namesSize);
183 QString values;
184 values.reserve(valuesSize);
185 for (const Column &column : columns)
187 names.append(quoted(column.name) + u',');
188 values.append(column.placeholder + u',');
190 names.chop(1);
191 values.chop(1);
193 return std::make_pair(names, values);
196 QString makeInsertStatement(const QString &tableName, const QList<Column> &columns)
198 const auto [names, values] = joinColumns(columns);
199 return u"INSERT INTO %1 (%2) VALUES (%3)"_s
200 .arg(quoted(tableName), names, values);
203 QString makeUpdateStatement(const QString &tableName, const QList<Column> &columns)
205 const auto [names, values] = joinColumns(columns);
206 return u"UPDATE %1 SET (%2) = (%3)"_s
207 .arg(quoted(tableName), names, values);
210 QString makeOnConflictUpdateStatement(const Column &constraint, const QList<Column> &columns)
212 const auto [names, values] = joinColumns(columns);
213 return u" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)"_s
214 .arg(quoted(constraint.name), names, values);
217 QString makeColumnDefinition(const Column &column, const char *definition)
219 return u"%1 %2"_s.arg(quoted(column.name), QString::fromLatin1(definition));
222 LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
224 LoadTorrentParams resumeData;
225 resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
226 resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
227 const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
228 if (!tagsData.isEmpty())
230 const QStringList tagList = tagsData.split(u',');
231 resumeData.tags.insert(tagList.cbegin(), tagList.cend());
233 resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
234 resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
235 resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
236 resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
237 resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
238 resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
239 query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
240 resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
241 query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
242 resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
243 query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
244 resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
245 resumeData.stopCondition = Utils::String::toEnum(
246 query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
247 resumeData.sslParameters =
249 .certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
250 .privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
251 .dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
254 resumeData.savePath = Profile::instance()->fromPortablePath(
255 Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
256 resumeData.useAutoTMM = resumeData.savePath.isEmpty();
257 if (!resumeData.useAutoTMM)
259 resumeData.downloadPath = Profile::instance()->fromPortablePath(
260 Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
263 const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
264 const auto *pref = Preferences::instance();
265 const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
266 const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
268 lt::error_code ec;
269 const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec
270 , nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
272 lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
274 p = lt::read_resume_data(resumeDataRoot, ec);
276 if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
277 ; !bencodedMetadata.isEmpty())
279 const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
280 , nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
281 p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
284 p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
285 .toString().toStdString();
287 if (p.flags & lt::torrent_flags::stop_when_ready)
289 p.flags &= ~lt::torrent_flags::stop_when_ready;
290 resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
293 return resumeData;
297 namespace BitTorrent
299 class DBResumeDataStorage::Worker final : public QThread
301 Q_DISABLE_COPY_MOVE(Worker)
303 public:
304 Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent = nullptr);
306 void run() override;
307 void requestInterruption();
309 void store(const TorrentID &id, const LoadTorrentParams &resumeData);
310 void remove(const TorrentID &id);
311 void storeQueue(const QList<TorrentID> &queue);
313 private:
314 void addJob(std::unique_ptr<Job> job);
316 const QString m_connectionName = u"ResumeDataStorageWorker"_s;
317 const Path m_path;
318 QReadWriteLock &m_dbLock;
320 std::queue<std::unique_ptr<Job>> m_jobs;
321 QMutex m_jobsMutex;
322 QWaitCondition m_waitCondition;
326 BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject *parent)
327 : ResumeDataStorage(dbPath, parent)
328 , m_ioThread {new QThread}
330 const bool needCreateDB = !dbPath.exists();
332 auto db = QSqlDatabase::addDatabase(u"QSQLITE"_s, DB_CONNECTION_NAME);
333 db.setDatabaseName(dbPath.data());
334 if (!db.open())
335 throw RuntimeError(db.lastError().text());
337 if (needCreateDB)
339 createDB();
341 else
343 const int dbVersion = (!db.record(DB_TABLE_TORRENTS).contains(DB_COLUMN_DOWNLOAD_PATH.name) ? 1 : currentDBVersion());
344 if (dbVersion < DB_VERSION)
345 updateDB(dbVersion);
348 m_asyncWorker = new Worker(dbPath, m_dbLock, this);
349 m_asyncWorker->start();
352 BitTorrent::DBResumeDataStorage::~DBResumeDataStorage()
354 m_asyncWorker->requestInterruption();
355 m_asyncWorker->wait();
356 QSqlDatabase::removeDatabase(DB_CONNECTION_NAME);
359 QList<BitTorrent::TorrentID> BitTorrent::DBResumeDataStorage::registeredTorrents() const
361 const auto selectTorrentIDStatement = u"SELECT %1 FROM %2 ORDER BY %3;"_s
362 .arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
364 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
365 QSqlQuery query {db};
367 if (!query.exec(selectTorrentIDStatement))
368 throw RuntimeError(query.lastError().text());
370 QList<TorrentID> registeredTorrents;
371 registeredTorrents.reserve(query.size());
372 while (query.next())
373 registeredTorrents.append(BitTorrent::TorrentID::fromString(query.value(0).toString()));
375 return registeredTorrents;
378 BitTorrent::LoadResumeDataResult BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
380 const QString selectTorrentStatement = u"SELECT * FROM %1 WHERE %2 = %3;"_s
381 .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
383 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
384 QSqlQuery query {db};
387 if (!query.prepare(selectTorrentStatement))
388 throw RuntimeError(query.lastError().text());
390 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
391 if (!query.exec())
392 throw RuntimeError(query.lastError().text());
394 if (!query.next())
395 throw RuntimeError(tr("Not found."));
397 catch (const RuntimeError &err)
399 return nonstd::make_unexpected(tr("Couldn't load resume data of torrent '%1'. Error: %2")
400 .arg(id.toString(), err.message()));
403 return parseQueryResultRow(query);
406 void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
408 m_asyncWorker->store(id, resumeData);
411 void BitTorrent::DBResumeDataStorage::remove(const BitTorrent::TorrentID &id) const
413 m_asyncWorker->remove(id);
416 void BitTorrent::DBResumeDataStorage::storeQueue(const QList<TorrentID> &queue) const
418 m_asyncWorker->storeQueue(queue);
421 void BitTorrent::DBResumeDataStorage::doLoadAll() const
423 const QString connectionName = u"ResumeDataStorageLoadAll"_s;
426 auto db = QSqlDatabase::addDatabase(u"QSQLITE"_s, connectionName);
427 db.setDatabaseName(path().data());
428 if (!db.open())
429 throw RuntimeError(db.lastError().text());
431 QSqlQuery query {db};
433 const auto selectTorrentIDStatement = u"SELECT %1 FROM %2 ORDER BY %3;"_s
434 .arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
436 const QReadLocker locker {&m_dbLock};
438 if (!query.exec(selectTorrentIDStatement))
439 throw RuntimeError(query.lastError().text());
441 QList<TorrentID> registeredTorrents;
442 registeredTorrents.reserve(query.size());
443 while (query.next())
444 registeredTorrents.append(TorrentID::fromString(query.value(0).toString()));
446 emit const_cast<DBResumeDataStorage *>(this)->loadStarted(registeredTorrents);
448 const auto selectStatement = u"SELECT * FROM %1 ORDER BY %2;"_s.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
449 if (!query.exec(selectStatement))
450 throw RuntimeError(query.lastError().text());
452 while (query.next())
454 const auto torrentID = TorrentID::fromString(query.value(DB_COLUMN_TORRENT_ID.name).toString());
455 onResumeDataLoaded(torrentID, parseQueryResultRow(query));
459 emit const_cast<DBResumeDataStorage *>(this)->loadFinished();
461 QSqlDatabase::removeDatabase(connectionName);
464 int BitTorrent::DBResumeDataStorage::currentDBVersion() const
466 const auto selectDBVersionStatement = u"SELECT %1 FROM %2 WHERE %3 = %4;"_s
467 .arg(quoted(DB_COLUMN_VALUE.name), quoted(DB_TABLE_META), quoted(DB_COLUMN_NAME.name), DB_COLUMN_NAME.placeholder);
469 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
470 QSqlQuery query {db};
472 if (!query.prepare(selectDBVersionStatement))
473 throw RuntimeError(query.lastError().text());
475 query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
477 const QReadLocker locker {&m_dbLock};
479 if (!query.exec())
480 throw RuntimeError(query.lastError().text());
482 if (!query.next())
483 throw RuntimeError(tr("Database is corrupted."));
485 bool ok;
486 const int dbVersion = query.value(0).toInt(&ok);
487 if (!ok)
488 throw RuntimeError(tr("Database is corrupted."));
490 return dbVersion;
493 void BitTorrent::DBResumeDataStorage::createDB() const
497 enableWALMode();
499 catch (const RuntimeError &err)
501 LogMsg(tr("Couldn't enable Write-Ahead Logging (WAL) journaling mode. Error: %1.")
502 .arg(err.message()), Log::WARNING);
505 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
507 if (!db.transaction())
508 throw RuntimeError(db.lastError().text());
510 QSqlQuery query {db};
514 const QStringList tableMetaItems = {
515 makeColumnDefinition(DB_COLUMN_ID, "INTEGER PRIMARY KEY"),
516 makeColumnDefinition(DB_COLUMN_NAME, "TEXT NOT NULL UNIQUE"),
517 makeColumnDefinition(DB_COLUMN_VALUE, "BLOB")
519 const QString createTableMetaQuery = makeCreateTableStatement(DB_TABLE_META, tableMetaItems);
520 if (!query.exec(createTableMetaQuery))
521 throw RuntimeError(query.lastError().text());
523 const QString insertMetaVersionQuery = makeInsertStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
524 if (!query.prepare(insertMetaVersionQuery))
525 throw RuntimeError(query.lastError().text());
527 query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
528 query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
530 if (!query.exec())
531 throw RuntimeError(query.lastError().text());
533 const QStringList tableTorrentsItems = {
534 makeColumnDefinition(DB_COLUMN_ID, "INTEGER PRIMARY KEY"),
535 makeColumnDefinition(DB_COLUMN_TORRENT_ID, "BLOB NOT NULL UNIQUE"),
536 makeColumnDefinition(DB_COLUMN_QUEUE_POSITION, "INTEGER NOT NULL DEFAULT -1"),
537 makeColumnDefinition(DB_COLUMN_NAME, "TEXT"),
538 makeColumnDefinition(DB_COLUMN_CATEGORY, "TEXT"),
539 makeColumnDefinition(DB_COLUMN_TAGS, "TEXT"),
540 makeColumnDefinition(DB_COLUMN_TARGET_SAVE_PATH, "TEXT"),
541 makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, "TEXT"),
542 makeColumnDefinition(DB_COLUMN_CONTENT_LAYOUT, "TEXT NOT NULL"),
543 makeColumnDefinition(DB_COLUMN_RATIO_LIMIT, "INTEGER NOT NULL"),
544 makeColumnDefinition(DB_COLUMN_SEEDING_TIME_LIMIT, "INTEGER NOT NULL"),
545 makeColumnDefinition(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT, "INTEGER NOT NULL"),
546 makeColumnDefinition(DB_COLUMN_SHARE_LIMIT_ACTION, "TEXT NOT NULL DEFAULT `Default`"),
547 makeColumnDefinition(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY, "INTEGER NOT NULL"),
548 makeColumnDefinition(DB_COLUMN_HAS_SEED_STATUS, "INTEGER NOT NULL"),
549 makeColumnDefinition(DB_COLUMN_OPERATING_MODE, "TEXT NOT NULL"),
550 makeColumnDefinition(DB_COLUMN_STOPPED, "INTEGER NOT NULL"),
551 makeColumnDefinition(DB_COLUMN_STOP_CONDITION, "TEXT NOT NULL DEFAULT `None`"),
552 makeColumnDefinition(DB_COLUMN_SSL_CERTIFICATE, "TEXT"),
553 makeColumnDefinition(DB_COLUMN_SSL_PRIVATE_KEY, "TEXT"),
554 makeColumnDefinition(DB_COLUMN_SSL_DH_PARAMS, "TEXT"),
555 makeColumnDefinition(DB_COLUMN_RESUMEDATA, "BLOB NOT NULL"),
556 makeColumnDefinition(DB_COLUMN_METADATA, "BLOB")
558 const QString createTableTorrentsQuery = makeCreateTableStatement(DB_TABLE_TORRENTS, tableTorrentsItems);
559 if (!query.exec(createTableTorrentsQuery))
560 throw RuntimeError(query.lastError().text());
562 const QString torrentsQueuePositionIndexName = u"%1_%2_INDEX"_s.arg(DB_TABLE_TORRENTS, DB_COLUMN_QUEUE_POSITION.name);
563 const QString createTorrentsQueuePositionIndexQuery = u"CREATE INDEX %1 ON %2 (%3)"_s
564 .arg(quoted(torrentsQueuePositionIndexName), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
565 if (!query.exec(createTorrentsQueuePositionIndexQuery))
566 throw RuntimeError(query.lastError().text());
568 if (!db.commit())
569 throw RuntimeError(db.lastError().text());
571 catch (const RuntimeError &)
573 db.rollback();
574 throw;
578 void BitTorrent::DBResumeDataStorage::updateDB(const int fromVersion) const
580 Q_ASSERT(fromVersion > 0);
581 Q_ASSERT(fromVersion != DB_VERSION);
583 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
585 const QWriteLocker locker {&m_dbLock};
587 if (!db.transaction())
588 throw RuntimeError(db.lastError().text());
590 QSqlQuery query {db};
594 const auto addColumn = [&query](const QString &table, const Column &column, const char *definition)
596 const auto testQuery = u"SELECT COUNT(%1) FROM %2;"_s.arg(quoted(column.name), quoted(table));
597 if (query.exec(testQuery))
598 return;
600 const auto alterTableQuery = u"ALTER TABLE %1 ADD %2"_s.arg(quoted(table), makeColumnDefinition(column, definition));
601 if (!query.exec(alterTableQuery))
602 throw RuntimeError(query.lastError().text());
605 if (fromVersion <= 1)
606 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_DOWNLOAD_PATH, "TEXT");
608 if (fromVersion <= 2)
609 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_STOP_CONDITION, "TEXT NOT NULL DEFAULT `None`");
611 if (fromVersion <= 3)
613 const QString torrentsQueuePositionIndexName = u"%1_%2_INDEX"_s.arg(DB_TABLE_TORRENTS, DB_COLUMN_QUEUE_POSITION.name);
614 const QString createTorrentsQueuePositionIndexQuery = u"CREATE INDEX IF NOT EXISTS %1 ON %2 (%3)"_s
615 .arg(quoted(torrentsQueuePositionIndexName), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
616 if (!query.exec(createTorrentsQueuePositionIndexQuery))
617 throw RuntimeError(query.lastError().text());
620 if (fromVersion <= 4)
621 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT, "INTEGER NOT NULL DEFAULT -2");
623 if (fromVersion <= 5)
625 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SSL_CERTIFICATE, "TEXT");
626 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SSL_PRIVATE_KEY, "TEXT");
627 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SSL_DH_PARAMS, "TEXT");
630 if (fromVersion <= 6)
631 addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SHARE_LIMIT_ACTION, "TEXTNOT NULL DEFAULT `Default`");
633 const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
634 if (!query.prepare(updateMetaVersionQuery))
635 throw RuntimeError(query.lastError().text());
637 query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
638 query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
640 if (!query.exec())
641 throw RuntimeError(query.lastError().text());
643 if (!db.commit())
644 throw RuntimeError(db.lastError().text());
646 catch (const RuntimeError &)
648 db.rollback();
649 throw;
653 void BitTorrent::DBResumeDataStorage::enableWALMode() const
655 auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
656 QSqlQuery query {db};
658 if (!query.exec(u"PRAGMA journal_mode = WAL;"_s))
659 throw RuntimeError(query.lastError().text());
661 if (!query.next())
662 throw RuntimeError(tr("Couldn't obtain query result."));
664 const QString result = query.value(0).toString();
665 if (result.compare(u"WAL"_s, Qt::CaseInsensitive) != 0)
666 throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
669 BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent)
670 : QThread(parent)
671 , m_path {dbPath}
672 , m_dbLock {dbLock}
676 void BitTorrent::DBResumeDataStorage::Worker::run()
679 auto db = QSqlDatabase::addDatabase(u"QSQLITE"_s, m_connectionName);
680 db.setDatabaseName(m_path.data());
681 if (!db.open())
682 throw RuntimeError(db.lastError().text());
684 int64_t transactedJobsCount = 0;
685 while (true)
687 m_jobsMutex.lock();
688 if (m_jobs.empty())
690 if (transactedJobsCount > 0)
692 db.commit();
693 m_dbLock.unlock();
695 qDebug() << "Resume data changes are committed. Transacted jobs:" << transactedJobsCount;
696 transactedJobsCount = 0;
699 if (isInterruptionRequested())
701 m_jobsMutex.unlock();
702 break;
705 m_waitCondition.wait(&m_jobsMutex);
706 if (isInterruptionRequested())
708 m_jobsMutex.unlock();
709 break;
712 m_dbLock.lockForWrite();
713 if (!db.transaction())
715 LogMsg(tr("Couldn't begin transaction. Error: %1").arg(db.lastError().text()), Log::WARNING);
716 m_dbLock.unlock();
717 break;
720 std::unique_ptr<Job> job = std::move(m_jobs.front());
721 m_jobs.pop();
722 m_jobsMutex.unlock();
724 job->perform(db);
725 ++transactedJobsCount;
728 db.close();
731 QSqlDatabase::removeDatabase(m_connectionName);
734 void DBResumeDataStorage::Worker::requestInterruption()
736 QThread::requestInterruption();
737 m_waitCondition.wakeAll();
740 void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const LoadTorrentParams &resumeData)
742 addJob(std::make_unique<StoreJob>(id, resumeData));
745 void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id)
747 addJob(std::make_unique<RemoveJob>(id));
750 void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QList<TorrentID> &queue)
752 addJob(std::make_unique<StoreQueueJob>(queue));
755 void BitTorrent::DBResumeDataStorage::Worker::addJob(std::unique_ptr<Job> job)
757 m_jobsMutex.lock();
758 m_jobs.push(std::move(job));
759 m_jobsMutex.unlock();
761 m_waitCondition.wakeAll();
764 namespace
766 using namespace BitTorrent;
768 StoreJob::StoreJob(const TorrentID &torrentID, const LoadTorrentParams &resumeData)
769 : m_torrentID {torrentID}
770 , m_resumeData {resumeData}
774 void StoreJob::perform(QSqlDatabase db)
776 // We need to adjust native libtorrent resume data
777 lt::add_torrent_params p = m_resumeData.ltAddTorrentParams;
778 p.save_path = Profile::instance()->toPortablePath(Path(p.save_path))
779 .toString().toStdString();
780 if (m_resumeData.stopped)
782 p.flags |= lt::torrent_flags::paused;
783 p.flags &= ~lt::torrent_flags::auto_managed;
785 else
787 // Torrent can be actually "running" but temporarily "paused" to perform some
788 // service jobs behind the scenes so we need to restore it as "running"
789 if (m_resumeData.operatingMode == BitTorrent::TorrentOperatingMode::AutoManaged)
791 p.flags |= lt::torrent_flags::auto_managed;
793 else
795 p.flags &= ~lt::torrent_flags::paused;
796 p.flags &= ~lt::torrent_flags::auto_managed;
800 QList<Column> columns {
801 DB_COLUMN_TORRENT_ID,
802 DB_COLUMN_NAME,
803 DB_COLUMN_CATEGORY,
804 DB_COLUMN_TAGS,
805 DB_COLUMN_TARGET_SAVE_PATH,
806 DB_COLUMN_DOWNLOAD_PATH,
807 DB_COLUMN_CONTENT_LAYOUT,
808 DB_COLUMN_RATIO_LIMIT,
809 DB_COLUMN_SEEDING_TIME_LIMIT,
810 DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT,
811 DB_COLUMN_SHARE_LIMIT_ACTION,
812 DB_COLUMN_HAS_OUTER_PIECES_PRIORITY,
813 DB_COLUMN_HAS_SEED_STATUS,
814 DB_COLUMN_OPERATING_MODE,
815 DB_COLUMN_STOPPED,
816 DB_COLUMN_STOP_CONDITION,
817 DB_COLUMN_SSL_CERTIFICATE,
818 DB_COLUMN_SSL_PRIVATE_KEY,
819 DB_COLUMN_SSL_DH_PARAMS,
820 DB_COLUMN_RESUMEDATA
823 lt::entry data = lt::write_resume_data(p);
825 // metadata is stored in separate column
826 QByteArray bencodedMetadata;
827 if (p.ti)
829 lt::entry::dictionary_type &dataDict = data.dict();
830 lt::entry metadata {lt::entry::dictionary_t};
831 lt::entry::dictionary_type &metadataDict = metadata.dict();
832 metadataDict.insert(dataDict.extract("info"));
833 metadataDict.insert(dataDict.extract("creation date"));
834 metadataDict.insert(dataDict.extract("created by"));
835 metadataDict.insert(dataDict.extract("comment"));
839 bencodedMetadata.reserve(512 * 1024);
840 lt::bencode(std::back_inserter(bencodedMetadata), metadata);
842 catch (const std::exception &err)
844 LogMsg(ResumeDataStorage::tr("Couldn't save torrent metadata. Error: %1.")
845 .arg(QString::fromLocal8Bit(err.what())), Log::CRITICAL);
846 return;
849 columns.append(DB_COLUMN_METADATA);
852 QByteArray bencodedResumeData;
853 bencodedResumeData.reserve(256 * 1024);
854 lt::bencode(std::back_inserter(bencodedResumeData), data);
856 const QString insertTorrentStatement = makeInsertStatement(DB_TABLE_TORRENTS, columns)
857 + makeOnConflictUpdateStatement(DB_COLUMN_TORRENT_ID, columns);
858 QSqlQuery query {db};
862 if (!query.prepare(insertTorrentStatement))
863 throw RuntimeError(query.lastError().text());
865 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, m_torrentID.toString());
866 query.bindValue(DB_COLUMN_NAME.placeholder, m_resumeData.name);
867 query.bindValue(DB_COLUMN_CATEGORY.placeholder, m_resumeData.category);
868 query.bindValue(DB_COLUMN_TAGS.placeholder, (m_resumeData.tags.isEmpty()
869 ? QString() : Utils::String::joinIntoString(m_resumeData.tags, u","_s)));
870 query.bindValue(DB_COLUMN_CONTENT_LAYOUT.placeholder, Utils::String::fromEnum(m_resumeData.contentLayout));
871 query.bindValue(DB_COLUMN_RATIO_LIMIT.placeholder, static_cast<int>(m_resumeData.ratioLimit * 1000));
872 query.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT.placeholder, m_resumeData.seedingTimeLimit);
873 query.bindValue(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.placeholder, m_resumeData.inactiveSeedingTimeLimit);
874 query.bindValue(DB_COLUMN_SHARE_LIMIT_ACTION.placeholder, Utils::String::fromEnum(m_resumeData.shareLimitAction));
875 query.bindValue(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.placeholder, m_resumeData.firstLastPiecePriority);
876 query.bindValue(DB_COLUMN_HAS_SEED_STATUS.placeholder, m_resumeData.hasFinishedStatus);
877 query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(m_resumeData.operatingMode));
878 query.bindValue(DB_COLUMN_STOPPED.placeholder, m_resumeData.stopped);
879 query.bindValue(DB_COLUMN_STOP_CONDITION.placeholder, Utils::String::fromEnum(m_resumeData.stopCondition));
880 query.bindValue(DB_COLUMN_SSL_CERTIFICATE.placeholder, QString::fromLatin1(m_resumeData.sslParameters.certificate.toPem()));
881 query.bindValue(DB_COLUMN_SSL_PRIVATE_KEY.placeholder, QString::fromLatin1(m_resumeData.sslParameters.privateKey.toPem()));
882 query.bindValue(DB_COLUMN_SSL_DH_PARAMS.placeholder, QString::fromLatin1(m_resumeData.sslParameters.dhParams));
884 if (!m_resumeData.useAutoTMM)
886 query.bindValue(DB_COLUMN_TARGET_SAVE_PATH.placeholder, Profile::instance()->toPortablePath(m_resumeData.savePath).data());
887 query.bindValue(DB_COLUMN_DOWNLOAD_PATH.placeholder, Profile::instance()->toPortablePath(m_resumeData.downloadPath).data());
890 query.bindValue(DB_COLUMN_RESUMEDATA.placeholder, bencodedResumeData);
891 if (!bencodedMetadata.isEmpty())
892 query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
894 if (!query.exec())
895 throw RuntimeError(query.lastError().text());
897 catch (const RuntimeError &err)
899 LogMsg(ResumeDataStorage::tr("Couldn't store resume data for torrent '%1'. Error: %2")
900 .arg(m_torrentID.toString(), err.message()), Log::CRITICAL);
904 RemoveJob::RemoveJob(const TorrentID &torrentID)
905 : m_torrentID {torrentID}
909 void RemoveJob::perform(QSqlDatabase db)
911 const auto deleteTorrentStatement = u"DELETE FROM %1 WHERE %2 = %3;"_s
912 .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
914 QSqlQuery query {db};
917 if (!query.prepare(deleteTorrentStatement))
918 throw RuntimeError(query.lastError().text());
920 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, m_torrentID.toString());
922 if (!query.exec())
923 throw RuntimeError(query.lastError().text());
925 catch (const RuntimeError &err)
927 LogMsg(ResumeDataStorage::tr("Couldn't delete resume data of torrent '%1'. Error: %2")
928 .arg(m_torrentID.toString(), err.message()), Log::CRITICAL);
932 StoreQueueJob::StoreQueueJob(const QList<TorrentID> &queue)
933 : m_queue {queue}
937 void StoreQueueJob::perform(QSqlDatabase db)
939 const auto updateQueuePosStatement = u"UPDATE %1 SET %2 = %3 WHERE %4 = %5;"_s
940 .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name), DB_COLUMN_QUEUE_POSITION.placeholder
941 , quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
945 QSqlQuery query {db};
947 if (!query.prepare(updateQueuePosStatement))
948 throw RuntimeError(query.lastError().text());
950 int pos = 0;
951 for (const TorrentID &torrentID : m_queue)
953 query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, torrentID.toString());
954 query.bindValue(DB_COLUMN_QUEUE_POSITION.placeholder, pos++);
955 if (!query.exec())
956 throw RuntimeError(query.lastError().text());
959 catch (const RuntimeError &err)
961 LogMsg(ResumeDataStorage::tr("Couldn't store torrents queue positions. Error: %1")
962 .arg(err.message()), Log::CRITICAL);