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
= 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
;
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 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();
181 names
.reserve(namesSize
);
183 values
.reserve(valuesSize
);
184 for (const Column
&column
: columns
)
186 names
.append(quoted(column
.name
) + u
',');
187 values
.append(column
.placeholder
+ u
',');
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();
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
;
298 class DBResumeDataStorage::Worker final
: public QThread
300 Q_DISABLE_COPY_MOVE(Worker
)
303 Worker(const Path
&dbPath
, QReadWriteLock
&dbLock
, QObject
*parent
= nullptr);
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
);
313 void addJob(std::unique_ptr
<Job
> job
);
315 const QString m_connectionName
= u
"ResumeDataStorageWorker"_s
;
317 QReadWriteLock
&m_dbLock
;
319 std::queue
<std::unique_ptr
<Job
>> m_jobs
;
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());
333 throw RuntimeError(db
.lastError().text());
341 const int dbVersion
= (!db
.record(DB_TABLE_TORRENTS
).contains(DB_COLUMN_DOWNLOAD_PATH
.name
) ? 1 : currentDBVersion());
342 if (dbVersion
< DB_VERSION
)
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());
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());
390 throw RuntimeError(query
.lastError().text());
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());
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());
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());
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
};
478 throw RuntimeError(query
.lastError().text());
481 throw RuntimeError(tr("Database is corrupted."));
484 const int dbVersion
= query
.value(0).toInt(&ok
);
486 throw RuntimeError(tr("Database is corrupted."));
491 void BitTorrent::DBResumeDataStorage::createDB() const
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
);
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());
567 throw RuntimeError(db
.lastError().text());
569 catch (const RuntimeError
&)
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
))
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
);
663 throw RuntimeError(query
.lastError().text());
666 throw RuntimeError(db
.lastError().text());
668 catch (const RuntimeError
&)
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());
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
)
698 void BitTorrent::DBResumeDataStorage::Worker::run()
701 auto db
= QSqlDatabase::addDatabase(u
"QSQLITE"_s
, m_connectionName
);
702 db
.setDatabaseName(m_path
.data());
704 throw RuntimeError(db
.lastError().text());
706 int64_t transactedJobsCount
= 0;
712 if (transactedJobsCount
> 0)
717 qDebug() << "Resume data changes are committed. Transacted jobs:" << transactedJobsCount
;
718 transactedJobsCount
= 0;
721 if (isInterruptionRequested())
723 m_jobsMutex
.unlock();
727 m_waitCondition
.wait(&m_jobsMutex
);
728 if (isInterruptionRequested())
730 m_jobsMutex
.unlock();
734 m_dbLock
.lockForWrite();
735 if (!db
.transaction())
737 LogMsg(tr("Couldn't begin transaction. Error: %1").arg(db
.lastError().text()), Log::WARNING
);
742 std::unique_ptr
<Job
> job
= std::move(m_jobs
.front());
744 m_jobsMutex
.unlock();
747 ++transactedJobsCount
;
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
)
780 m_jobs
.push(std::move(job
));
781 m_jobsMutex
.unlock();
783 m_waitCondition
.wakeAll();
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
;
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
;
817 p
.flags
&= ~lt::torrent_flags::paused
;
818 p
.flags
&= ~lt::torrent_flags::auto_managed
;
822 QList
<Column
> columns
{
823 DB_COLUMN_TORRENT_ID
,
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
,
838 DB_COLUMN_STOP_CONDITION
,
839 DB_COLUMN_SSL_CERTIFICATE
,
840 DB_COLUMN_SSL_PRIVATE_KEY
,
841 DB_COLUMN_SSL_DH_PARAMS
,
845 lt::entry data
= lt::write_resume_data(p
);
847 // metadata is stored in separate column
848 QByteArray bencodedMetadata
;
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
);
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
);
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());
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
)
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());
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
++);
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
);