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"
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>
47 #include <QSqlDatabase>
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"
64 #include "loadtorrentparams.h"
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
;
82 virtual ~Job() = default;
83 virtual void perform(QSqlDatabase db
) = 0;
86 class StoreJob final
: public Job
89 StoreJob(const TorrentID
&torrentID
, const LoadTorrentParams
&resumeData
);
90 void perform(QSqlDatabase db
) override
;
93 const TorrentID m_torrentID
;
94 const LoadTorrentParams m_resumeData
;
97 class RemoveJob final
: public Job
100 explicit RemoveJob(const TorrentID
&torrentID
);
101 void perform(QSqlDatabase db
) override
;
104 const TorrentID m_torrentID
;
107 class StoreQueueJob final
: public Job
110 explicit StoreQueueJob(const QList
<TorrentID
> &queue
);
111 void perform(QSqlDatabase db
) override
;
114 const QList
<TorrentID
> m_queue
;
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();
182 names
.reserve(namesSize
);
184 values
.reserve(valuesSize
);
185 for (const Column
&column
: columns
)
187 names
.append(quoted(column
.name
) + u
',');
188 values
.append(column
.placeholder
+ u
',');
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();
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
;
299 class DBResumeDataStorage::Worker final
: public QThread
301 Q_DISABLE_COPY_MOVE(Worker
)
304 Worker(const Path
&dbPath
, QReadWriteLock
&dbLock
, QObject
*parent
= nullptr);
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
);
314 void addJob(std::unique_ptr
<Job
> job
);
316 const QString m_connectionName
= u
"ResumeDataStorageWorker"_s
;
318 QReadWriteLock
&m_dbLock
;
320 std::queue
<std::unique_ptr
<Job
>> m_jobs
;
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());
335 throw RuntimeError(db
.lastError().text());
343 const int dbVersion
= (!db
.record(DB_TABLE_TORRENTS
).contains(DB_COLUMN_DOWNLOAD_PATH
.name
) ? 1 : currentDBVersion());
344 if (dbVersion
< DB_VERSION
)
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());
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());
392 throw RuntimeError(query
.lastError().text());
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());
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());
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());
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
};
480 throw RuntimeError(query
.lastError().text());
483 throw RuntimeError(tr("Database is corrupted."));
486 const int dbVersion
= query
.value(0).toInt(&ok
);
488 throw RuntimeError(tr("Database is corrupted."));
493 void BitTorrent::DBResumeDataStorage::createDB() const
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
);
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());
569 throw RuntimeError(db
.lastError().text());
571 catch (const RuntimeError
&)
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
))
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
);
641 throw RuntimeError(query
.lastError().text());
644 throw RuntimeError(db
.lastError().text());
646 catch (const RuntimeError
&)
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());
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
)
676 void BitTorrent::DBResumeDataStorage::Worker::run()
679 auto db
= QSqlDatabase::addDatabase(u
"QSQLITE"_s
, m_connectionName
);
680 db
.setDatabaseName(m_path
.data());
682 throw RuntimeError(db
.lastError().text());
684 int64_t transactedJobsCount
= 0;
690 if (transactedJobsCount
> 0)
695 qDebug() << "Resume data changes are committed. Transacted jobs:" << transactedJobsCount
;
696 transactedJobsCount
= 0;
699 if (isInterruptionRequested())
701 m_jobsMutex
.unlock();
705 m_waitCondition
.wait(&m_jobsMutex
);
706 if (isInterruptionRequested())
708 m_jobsMutex
.unlock();
712 m_dbLock
.lockForWrite();
713 if (!db
.transaction())
715 LogMsg(tr("Couldn't begin transaction. Error: %1").arg(db
.lastError().text()), Log::WARNING
);
720 std::unique_ptr
<Job
> job
= std::move(m_jobs
.front());
722 m_jobsMutex
.unlock();
725 ++transactedJobsCount
;
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
)
758 m_jobs
.push(std::move(job
));
759 m_jobsMutex
.unlock();
761 m_waitCondition
.wakeAll();
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
;
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
;
795 p
.flags
&= ~lt::torrent_flags::paused
;
796 p
.flags
&= ~lt::torrent_flags::auto_managed
;
800 QList
<Column
> columns
{
801 DB_COLUMN_TORRENT_ID
,
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
,
816 DB_COLUMN_STOP_CONDITION
,
817 DB_COLUMN_SSL_CERTIFICATE
,
818 DB_COLUMN_SSL_PRIVATE_KEY
,
819 DB_COLUMN_SSL_DH_PARAMS
,
823 lt::entry data
= lt::write_resume_data(p
);
825 // metadata is stored in separate column
826 QByteArray bencodedMetadata
;
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
);
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
);
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());
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
)
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());
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
++);
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
);