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>
46 #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/string.h"
63 #include "loadtorrentparams.h"
67 const QString DB_CONNECTION_NAME
= u
"ResumeDataStorage"_s
;
69 const int DB_VERSION
= 5;
71 const QString DB_TABLE_META
= u
"meta"_s
;
72 const QString DB_TABLE_TORRENTS
= u
"torrents"_s
;
74 const QString META_VERSION
= u
"version"_s
;
76 using namespace BitTorrent
;
81 virtual ~Job() = default;
82 virtual void perform(QSqlDatabase db
) = 0;
85 class StoreJob final
: public Job
88 StoreJob(const TorrentID
&torrentID
, const LoadTorrentParams
&resumeData
);
89 void perform(QSqlDatabase db
) override
;
92 const TorrentID m_torrentID
;
93 const LoadTorrentParams m_resumeData
;
96 class RemoveJob final
: public Job
99 explicit RemoveJob(const TorrentID
&torrentID
);
100 void perform(QSqlDatabase db
) override
;
103 const TorrentID m_torrentID
;
106 class StoreQueueJob final
: public Job
109 explicit StoreQueueJob(const QVector
<TorrentID
> &queue
);
110 void perform(QSqlDatabase db
) override
;
113 const QVector
<TorrentID
> m_queue
;
122 Column
makeColumn(const char *columnName
)
124 return {QString::fromLatin1(columnName
), (u
':' + QString::fromLatin1(columnName
))};
127 const Column DB_COLUMN_ID
= makeColumn("id");
128 const Column DB_COLUMN_TORRENT_ID
= makeColumn("torrent_id");
129 const Column DB_COLUMN_QUEUE_POSITION
= makeColumn("queue_position");
130 const Column DB_COLUMN_NAME
= makeColumn("name");
131 const Column DB_COLUMN_CATEGORY
= makeColumn("category");
132 const Column DB_COLUMN_TAGS
= makeColumn("tags");
133 const Column DB_COLUMN_TARGET_SAVE_PATH
= makeColumn("target_save_path");
134 const Column DB_COLUMN_DOWNLOAD_PATH
= makeColumn("download_path");
135 const Column DB_COLUMN_CONTENT_LAYOUT
= makeColumn("content_layout");
136 const Column DB_COLUMN_RATIO_LIMIT
= makeColumn("ratio_limit");
137 const Column DB_COLUMN_SEEDING_TIME_LIMIT
= makeColumn("seeding_time_limit");
138 const Column DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
= makeColumn("inactive_seeding_time_limit");
139 const Column DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
= makeColumn("has_outer_pieces_priority");
140 const Column DB_COLUMN_HAS_SEED_STATUS
= makeColumn("has_seed_status");
141 const Column DB_COLUMN_OPERATING_MODE
= makeColumn("operating_mode");
142 const Column DB_COLUMN_STOPPED
= makeColumn("stopped");
143 const Column DB_COLUMN_STOP_CONDITION
= makeColumn("stop_condition");
144 const Column DB_COLUMN_RESUMEDATA
= makeColumn("libtorrent_resume_data");
145 const Column DB_COLUMN_METADATA
= makeColumn("metadata");
146 const Column DB_COLUMN_VALUE
= makeColumn("value");
148 template <typename LTStr
>
149 QString
fromLTString(const LTStr
&str
)
151 return QString::fromUtf8(str
.data(), static_cast<int>(str
.size()));
154 QString
quoted(const QString
&name
)
156 const QChar quote
= u
'`';
158 return (quote
+ name
+ quote
);
161 QString
makeCreateTableStatement(const QString
&tableName
, const QStringList
&items
)
163 return u
"CREATE TABLE %1 (%2)"_s
.arg(quoted(tableName
), items
.join(u
','));
166 std::pair
<QString
, QString
> joinColumns(const QVector
<Column
> &columns
)
168 int namesSize
= columns
.size();
169 int valuesSize
= columns
.size();
170 for (const Column
&column
: columns
)
172 namesSize
+= column
.name
.size() + 2;
173 valuesSize
+= column
.placeholder
.size();
177 names
.reserve(namesSize
);
179 values
.reserve(valuesSize
);
180 for (const Column
&column
: columns
)
182 names
.append(quoted(column
.name
) + u
',');
183 values
.append(column
.placeholder
+ u
',');
188 return std::make_pair(names
, values
);
191 QString
makeInsertStatement(const QString
&tableName
, const QVector
<Column
> &columns
)
193 const auto [names
, values
] = joinColumns(columns
);
194 return u
"INSERT INTO %1 (%2) VALUES (%3)"_s
195 .arg(quoted(tableName
), names
, values
);
198 QString
makeUpdateStatement(const QString
&tableName
, const QVector
<Column
> &columns
)
200 const auto [names
, values
] = joinColumns(columns
);
201 return u
"UPDATE %1 SET (%2) = (%3)"_s
202 .arg(quoted(tableName
), names
, values
);
205 QString
makeOnConflictUpdateStatement(const Column
&constraint
, const QVector
<Column
> &columns
)
207 const auto [names
, values
] = joinColumns(columns
);
208 return u
" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)"_s
209 .arg(quoted(constraint
.name
), names
, values
);
212 QString
makeColumnDefinition(const Column
&column
, const char *definition
)
214 return u
"%1 %2"_s
.arg(quoted(column
.name
), QString::fromLatin1(definition
));
217 LoadTorrentParams
parseQueryResultRow(const QSqlQuery
&query
)
219 LoadTorrentParams resumeData
;
220 resumeData
.name
= query
.value(DB_COLUMN_NAME
.name
).toString();
221 resumeData
.category
= query
.value(DB_COLUMN_CATEGORY
.name
).toString();
222 const QString tagsData
= query
.value(DB_COLUMN_TAGS
.name
).toString();
223 if (!tagsData
.isEmpty())
225 const QStringList tagList
= tagsData
.split(u
',');
226 resumeData
.tags
.insert(tagList
.cbegin(), tagList
.cend());
228 resumeData
.hasFinishedStatus
= query
.value(DB_COLUMN_HAS_SEED_STATUS
.name
).toBool();
229 resumeData
.firstLastPiecePriority
= query
.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
.name
).toBool();
230 resumeData
.ratioLimit
= query
.value(DB_COLUMN_RATIO_LIMIT
.name
).toInt() / 1000.0;
231 resumeData
.seedingTimeLimit
= query
.value(DB_COLUMN_SEEDING_TIME_LIMIT
.name
).toInt();
232 resumeData
.inactiveSeedingTimeLimit
= query
.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
.name
).toInt();
233 resumeData
.contentLayout
= Utils::String::toEnum
<TorrentContentLayout
>(
234 query
.value(DB_COLUMN_CONTENT_LAYOUT
.name
).toString(), TorrentContentLayout::Original
);
235 resumeData
.operatingMode
= Utils::String::toEnum
<TorrentOperatingMode
>(
236 query
.value(DB_COLUMN_OPERATING_MODE
.name
).toString(), TorrentOperatingMode::AutoManaged
);
237 resumeData
.stopped
= query
.value(DB_COLUMN_STOPPED
.name
).toBool();
238 resumeData
.stopCondition
= Utils::String::toEnum(
239 query
.value(DB_COLUMN_STOP_CONDITION
.name
).toString(), Torrent::StopCondition::None
);
241 resumeData
.savePath
= Profile::instance()->fromPortablePath(
242 Path(query
.value(DB_COLUMN_TARGET_SAVE_PATH
.name
).toString()));
243 resumeData
.useAutoTMM
= resumeData
.savePath
.isEmpty();
244 if (!resumeData
.useAutoTMM
)
246 resumeData
.downloadPath
= Profile::instance()->fromPortablePath(
247 Path(query
.value(DB_COLUMN_DOWNLOAD_PATH
.name
).toString()));
250 const QByteArray bencodedResumeData
= query
.value(DB_COLUMN_RESUMEDATA
.name
).toByteArray();
251 const auto *pref
= Preferences::instance();
252 const int bdecodeDepthLimit
= pref
->getBdecodeDepthLimit();
253 const int bdecodeTokenLimit
= pref
->getBdecodeTokenLimit();
256 const lt::bdecode_node resumeDataRoot
= lt::bdecode(bencodedResumeData
, ec
257 , nullptr, bdecodeDepthLimit
, bdecodeTokenLimit
);
259 lt::add_torrent_params
&p
= resumeData
.ltAddTorrentParams
;
261 p
= lt::read_resume_data(resumeDataRoot
, ec
);
263 if (const QByteArray bencodedMetadata
= query
.value(DB_COLUMN_METADATA
.name
).toByteArray()
264 ; !bencodedMetadata
.isEmpty())
266 const lt::bdecode_node torentInfoRoot
= lt::bdecode(bencodedMetadata
, ec
267 , nullptr, bdecodeDepthLimit
, bdecodeTokenLimit
);
268 p
.ti
= std::make_shared
<lt::torrent_info
>(torentInfoRoot
, ec
);
271 p
.save_path
= Profile::instance()->fromPortablePath(Path(fromLTString(p
.save_path
)))
272 .toString().toStdString();
274 if (p
.flags
& lt::torrent_flags::stop_when_ready
)
276 p
.flags
&= ~lt::torrent_flags::stop_when_ready
;
277 resumeData
.stopCondition
= Torrent::StopCondition::FilesChecked
;
286 class DBResumeDataStorage::Worker final
: public QThread
288 Q_DISABLE_COPY_MOVE(Worker
)
291 Worker(const Path
&dbPath
, QReadWriteLock
&dbLock
, QObject
*parent
= nullptr);
294 void requestInterruption();
296 void store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
);
297 void remove(const TorrentID
&id
);
298 void storeQueue(const QVector
<TorrentID
> &queue
);
301 void addJob(std::unique_ptr
<Job
> job
);
303 const QString m_connectionName
= u
"ResumeDataStorageWorker"_s
;
305 QReadWriteLock
&m_dbLock
;
307 std::queue
<std::unique_ptr
<Job
>> m_jobs
;
309 QWaitCondition m_waitCondition
;
313 BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path
&dbPath
, QObject
*parent
)
314 : ResumeDataStorage(dbPath
, parent
)
315 , m_ioThread
{new QThread
}
317 const bool needCreateDB
= !dbPath
.exists();
319 auto db
= QSqlDatabase::addDatabase(u
"QSQLITE"_s
, DB_CONNECTION_NAME
);
320 db
.setDatabaseName(dbPath
.data());
322 throw RuntimeError(db
.lastError().text());
330 const int dbVersion
= (!db
.record(DB_TABLE_TORRENTS
).contains(DB_COLUMN_DOWNLOAD_PATH
.name
) ? 1 : currentDBVersion());
331 if (dbVersion
< DB_VERSION
)
335 m_asyncWorker
= new Worker(dbPath
, m_dbLock
, this);
336 m_asyncWorker
->start();
339 BitTorrent::DBResumeDataStorage::~DBResumeDataStorage()
341 m_asyncWorker
->requestInterruption();
342 m_asyncWorker
->wait();
343 QSqlDatabase::removeDatabase(DB_CONNECTION_NAME
);
346 QVector
<BitTorrent::TorrentID
> BitTorrent::DBResumeDataStorage::registeredTorrents() const
348 const auto selectTorrentIDStatement
= u
"SELECT %1 FROM %2 ORDER BY %3;"_s
349 .arg(quoted(DB_COLUMN_TORRENT_ID
.name
), quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
));
351 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
352 QSqlQuery query
{db
};
354 if (!query
.exec(selectTorrentIDStatement
))
355 throw RuntimeError(query
.lastError().text());
357 QVector
<TorrentID
> registeredTorrents
;
358 registeredTorrents
.reserve(query
.size());
360 registeredTorrents
.append(BitTorrent::TorrentID::fromString(query
.value(0).toString()));
362 return registeredTorrents
;
365 BitTorrent::LoadResumeDataResult
BitTorrent::DBResumeDataStorage::load(const TorrentID
&id
) const
367 const QString selectTorrentStatement
= u
"SELECT * FROM %1 WHERE %2 = %3;"_s
368 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
370 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
371 QSqlQuery query
{db
};
374 if (!query
.prepare(selectTorrentStatement
))
375 throw RuntimeError(query
.lastError().text());
377 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, id
.toString());
379 throw RuntimeError(query
.lastError().text());
382 throw RuntimeError(tr("Not found."));
384 catch (const RuntimeError
&err
)
386 return nonstd::make_unexpected(tr("Couldn't load resume data of torrent '%1'. Error: %2")
387 .arg(id
.toString(), err
.message()));
390 return parseQueryResultRow(query
);
393 void BitTorrent::DBResumeDataStorage::store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
) const
395 m_asyncWorker
->store(id
, resumeData
);
398 void BitTorrent::DBResumeDataStorage::remove(const BitTorrent::TorrentID
&id
) const
400 m_asyncWorker
->remove(id
);
403 void BitTorrent::DBResumeDataStorage::storeQueue(const QVector
<TorrentID
> &queue
) const
405 m_asyncWorker
->storeQueue(queue
);
408 void BitTorrent::DBResumeDataStorage::doLoadAll() const
410 const QString connectionName
= u
"ResumeDataStorageLoadAll"_s
;
413 auto db
= QSqlDatabase::addDatabase(u
"QSQLITE"_s
, connectionName
);
414 db
.setDatabaseName(path().data());
416 throw RuntimeError(db
.lastError().text());
418 QSqlQuery query
{db
};
420 const auto selectTorrentIDStatement
= u
"SELECT %1 FROM %2 ORDER BY %3;"_s
421 .arg(quoted(DB_COLUMN_TORRENT_ID
.name
), quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
));
423 const QReadLocker locker
{&m_dbLock
};
425 if (!query
.exec(selectTorrentIDStatement
))
426 throw RuntimeError(query
.lastError().text());
428 QVector
<TorrentID
> registeredTorrents
;
429 registeredTorrents
.reserve(query
.size());
431 registeredTorrents
.append(TorrentID::fromString(query
.value(0).toString()));
433 emit
const_cast<DBResumeDataStorage
*>(this)->loadStarted(registeredTorrents
);
435 const auto selectStatement
= u
"SELECT * FROM %1 ORDER BY %2;"_s
.arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
));
436 if (!query
.exec(selectStatement
))
437 throw RuntimeError(query
.lastError().text());
441 const auto torrentID
= TorrentID::fromString(query
.value(DB_COLUMN_TORRENT_ID
.name
).toString());
442 onResumeDataLoaded(torrentID
, parseQueryResultRow(query
));
446 emit
const_cast<DBResumeDataStorage
*>(this)->loadFinished();
448 QSqlDatabase::removeDatabase(connectionName
);
451 int BitTorrent::DBResumeDataStorage::currentDBVersion() const
453 const auto selectDBVersionStatement
= u
"SELECT %1 FROM %2 WHERE %3 = %4;"_s
454 .arg(quoted(DB_COLUMN_VALUE
.name
), quoted(DB_TABLE_META
), quoted(DB_COLUMN_NAME
.name
), DB_COLUMN_NAME
.placeholder
);
456 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
457 QSqlQuery query
{db
};
459 if (!query
.prepare(selectDBVersionStatement
))
460 throw RuntimeError(query
.lastError().text());
462 query
.bindValue(DB_COLUMN_NAME
.placeholder
, META_VERSION
);
464 const QReadLocker locker
{&m_dbLock
};
467 throw RuntimeError(query
.lastError().text());
470 throw RuntimeError(tr("Database is corrupted."));
473 const int dbVersion
= query
.value(0).toInt(&ok
);
475 throw RuntimeError(tr("Database is corrupted."));
480 void BitTorrent::DBResumeDataStorage::createDB() const
486 catch (const RuntimeError
&err
)
488 LogMsg(tr("Couldn't enable Write-Ahead Logging (WAL) journaling mode. Error: %1.")
489 .arg(err
.message()), Log::WARNING
);
492 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
494 if (!db
.transaction())
495 throw RuntimeError(db
.lastError().text());
497 QSqlQuery query
{db
};
501 const QStringList tableMetaItems
= {
502 makeColumnDefinition(DB_COLUMN_ID
, "INTEGER PRIMARY KEY"),
503 makeColumnDefinition(DB_COLUMN_NAME
, "TEXT NOT NULL UNIQUE"),
504 makeColumnDefinition(DB_COLUMN_VALUE
, "BLOB")
506 const QString createTableMetaQuery
= makeCreateTableStatement(DB_TABLE_META
, tableMetaItems
);
507 if (!query
.exec(createTableMetaQuery
))
508 throw RuntimeError(query
.lastError().text());
510 const QString insertMetaVersionQuery
= makeInsertStatement(DB_TABLE_META
, {DB_COLUMN_NAME
, DB_COLUMN_VALUE
});
511 if (!query
.prepare(insertMetaVersionQuery
))
512 throw RuntimeError(query
.lastError().text());
514 query
.bindValue(DB_COLUMN_NAME
.placeholder
, META_VERSION
);
515 query
.bindValue(DB_COLUMN_VALUE
.placeholder
, DB_VERSION
);
518 throw RuntimeError(query
.lastError().text());
520 const QStringList tableTorrentsItems
= {
521 makeColumnDefinition(DB_COLUMN_ID
, "INTEGER PRIMARY KEY"),
522 makeColumnDefinition(DB_COLUMN_TORRENT_ID
, "BLOB NOT NULL UNIQUE"),
523 makeColumnDefinition(DB_COLUMN_QUEUE_POSITION
, "INTEGER NOT NULL DEFAULT -1"),
524 makeColumnDefinition(DB_COLUMN_NAME
, "TEXT"),
525 makeColumnDefinition(DB_COLUMN_CATEGORY
, "TEXT"),
526 makeColumnDefinition(DB_COLUMN_TAGS
, "TEXT"),
527 makeColumnDefinition(DB_COLUMN_TARGET_SAVE_PATH
, "TEXT"),
528 makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH
, "TEXT"),
529 makeColumnDefinition(DB_COLUMN_CONTENT_LAYOUT
, "TEXT NOT NULL"),
530 makeColumnDefinition(DB_COLUMN_RATIO_LIMIT
, "INTEGER NOT NULL"),
531 makeColumnDefinition(DB_COLUMN_SEEDING_TIME_LIMIT
, "INTEGER NOT NULL"),
532 makeColumnDefinition(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
, "INTEGER NOT NULL"),
533 makeColumnDefinition(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
, "INTEGER NOT NULL"),
534 makeColumnDefinition(DB_COLUMN_HAS_SEED_STATUS
, "INTEGER NOT NULL"),
535 makeColumnDefinition(DB_COLUMN_OPERATING_MODE
, "TEXT NOT NULL"),
536 makeColumnDefinition(DB_COLUMN_STOPPED
, "INTEGER NOT NULL"),
537 makeColumnDefinition(DB_COLUMN_STOP_CONDITION
, "TEXT NOT NULL DEFAULT `None`"),
538 makeColumnDefinition(DB_COLUMN_RESUMEDATA
, "BLOB NOT NULL"),
539 makeColumnDefinition(DB_COLUMN_METADATA
, "BLOB")
541 const QString createTableTorrentsQuery
= makeCreateTableStatement(DB_TABLE_TORRENTS
, tableTorrentsItems
);
542 if (!query
.exec(createTableTorrentsQuery
))
543 throw RuntimeError(query
.lastError().text());
545 const QString torrentsQueuePositionIndexName
= u
"%1_%2_INDEX"_s
.arg(DB_TABLE_TORRENTS
, DB_COLUMN_QUEUE_POSITION
.name
);
546 const QString createTorrentsQueuePositionIndexQuery
= u
"CREATE INDEX %1 ON %2 (%3)"_s
547 .arg(quoted(torrentsQueuePositionIndexName
), quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
));
548 if (!query
.exec(createTorrentsQueuePositionIndexQuery
))
549 throw RuntimeError(query
.lastError().text());
552 throw RuntimeError(db
.lastError().text());
554 catch (const RuntimeError
&)
561 void BitTorrent::DBResumeDataStorage::updateDB(const int fromVersion
) const
563 Q_ASSERT(fromVersion
> 0);
564 Q_ASSERT(fromVersion
!= DB_VERSION
);
566 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
568 const QWriteLocker locker
{&m_dbLock
};
570 if (!db
.transaction())
571 throw RuntimeError(db
.lastError().text());
573 QSqlQuery query
{db
};
577 if (fromVersion
== 1)
579 const auto testQuery
= u
"SELECT COUNT(%1) FROM %2;"_s
580 .arg(quoted(DB_COLUMN_DOWNLOAD_PATH
.name
), quoted(DB_TABLE_TORRENTS
));
581 if (!query
.exec(testQuery
))
583 const auto alterTableTorrentsQuery
= u
"ALTER TABLE %1 ADD %2"_s
584 .arg(quoted(DB_TABLE_TORRENTS
), makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH
, "TEXT"));
585 if (!query
.exec(alterTableTorrentsQuery
))
586 throw RuntimeError(query
.lastError().text());
590 if (fromVersion
<= 2)
592 const auto testQuery
= u
"SELECT COUNT(%1) FROM %2;"_s
593 .arg(quoted(DB_COLUMN_STOP_CONDITION
.name
), quoted(DB_TABLE_TORRENTS
));
594 if (!query
.exec(testQuery
))
596 const auto alterTableTorrentsQuery
= u
"ALTER TABLE %1 ADD %2"_s
597 .arg(quoted(DB_TABLE_TORRENTS
), makeColumnDefinition(DB_COLUMN_STOP_CONDITION
, "TEXT NOT NULL DEFAULT `None`"));
598 if (!query
.exec(alterTableTorrentsQuery
))
599 throw RuntimeError(query
.lastError().text());
603 if (fromVersion
<= 3)
605 const QString torrentsQueuePositionIndexName
= u
"%1_%2_INDEX"_s
.arg(DB_TABLE_TORRENTS
, DB_COLUMN_QUEUE_POSITION
.name
);
606 const QString createTorrentsQueuePositionIndexQuery
= u
"CREATE INDEX IF NOT EXISTS %1 ON %2 (%3)"_s
607 .arg(quoted(torrentsQueuePositionIndexName
), quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
));
608 if (!query
.exec(createTorrentsQueuePositionIndexQuery
))
609 throw RuntimeError(query
.lastError().text());
612 if (fromVersion
<= 4)
614 const auto testQuery
= u
"SELECT COUNT(%1) FROM %2;"_s
615 .arg(quoted(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
.name
), quoted(DB_TABLE_TORRENTS
));
616 if (!query
.exec(testQuery
))
618 const auto alterTableTorrentsQuery
= u
"ALTER TABLE %1 ADD %2"_s
619 .arg(quoted(DB_TABLE_TORRENTS
), makeColumnDefinition(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
, "INTEGER NOT NULL DEFAULT -2"));
620 if (!query
.exec(alterTableTorrentsQuery
))
621 throw RuntimeError(query
.lastError().text());
625 const QString updateMetaVersionQuery
= makeUpdateStatement(DB_TABLE_META
, {DB_COLUMN_NAME
, DB_COLUMN_VALUE
});
626 if (!query
.prepare(updateMetaVersionQuery
))
627 throw RuntimeError(query
.lastError().text());
629 query
.bindValue(DB_COLUMN_NAME
.placeholder
, META_VERSION
);
630 query
.bindValue(DB_COLUMN_VALUE
.placeholder
, DB_VERSION
);
633 throw RuntimeError(query
.lastError().text());
636 throw RuntimeError(db
.lastError().text());
638 catch (const RuntimeError
&)
645 void BitTorrent::DBResumeDataStorage::enableWALMode() const
647 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
648 QSqlQuery query
{db
};
650 if (!query
.exec(u
"PRAGMA journal_mode = WAL;"_s
))
651 throw RuntimeError(query
.lastError().text());
654 throw RuntimeError(tr("Couldn't obtain query result."));
656 const QString result
= query
.value(0).toString();
657 if (result
.compare(u
"WAL"_s
, Qt::CaseInsensitive
) != 0)
658 throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
661 BitTorrent::DBResumeDataStorage::Worker::Worker(const Path
&dbPath
, QReadWriteLock
&dbLock
, QObject
*parent
)
668 void BitTorrent::DBResumeDataStorage::Worker::run()
671 auto db
= QSqlDatabase::addDatabase(u
"QSQLITE"_s
, m_connectionName
);
672 db
.setDatabaseName(m_path
.data());
674 throw RuntimeError(db
.lastError().text());
676 int64_t transactedJobsCount
= 0;
682 if (transactedJobsCount
> 0)
687 qDebug() << "Resume data changes are committed. Transacted jobs:" << transactedJobsCount
;
688 transactedJobsCount
= 0;
691 if (isInterruptionRequested())
693 m_jobsMutex
.unlock();
697 m_waitCondition
.wait(&m_jobsMutex
);
698 if (isInterruptionRequested())
700 m_jobsMutex
.unlock();
704 m_dbLock
.lockForWrite();
705 if (!db
.transaction())
707 LogMsg(tr("Couldn't begin transaction. Error: %1").arg(db
.lastError().text()), Log::WARNING
);
712 std::unique_ptr
<Job
> job
= std::move(m_jobs
.front());
714 m_jobsMutex
.unlock();
717 ++transactedJobsCount
;
723 QSqlDatabase::removeDatabase(m_connectionName
);
726 void DBResumeDataStorage::Worker::requestInterruption()
728 QThread::requestInterruption();
729 m_waitCondition
.wakeAll();
732 void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
)
734 addJob(std::make_unique
<StoreJob
>(id
, resumeData
));
737 void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID
&id
)
739 addJob(std::make_unique
<RemoveJob
>(id
));
742 void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector
<TorrentID
> &queue
)
744 addJob(std::make_unique
<StoreQueueJob
>(queue
));
747 void BitTorrent::DBResumeDataStorage::Worker::addJob(std::unique_ptr
<Job
> job
)
750 m_jobs
.push(std::move(job
));
751 m_jobsMutex
.unlock();
753 m_waitCondition
.wakeAll();
758 using namespace BitTorrent
;
760 StoreJob::StoreJob(const TorrentID
&torrentID
, const LoadTorrentParams
&resumeData
)
761 : m_torrentID
{torrentID
}
762 , m_resumeData
{resumeData
}
766 void StoreJob::perform(QSqlDatabase db
)
768 // We need to adjust native libtorrent resume data
769 lt::add_torrent_params p
= m_resumeData
.ltAddTorrentParams
;
770 p
.save_path
= Profile::instance()->toPortablePath(Path(p
.save_path
))
771 .toString().toStdString();
772 if (m_resumeData
.stopped
)
774 p
.flags
|= lt::torrent_flags::paused
;
775 p
.flags
&= ~lt::torrent_flags::auto_managed
;
779 // Torrent can be actually "running" but temporarily "paused" to perform some
780 // service jobs behind the scenes so we need to restore it as "running"
781 if (m_resumeData
.operatingMode
== BitTorrent::TorrentOperatingMode::AutoManaged
)
783 p
.flags
|= lt::torrent_flags::auto_managed
;
787 p
.flags
&= ~lt::torrent_flags::paused
;
788 p
.flags
&= ~lt::torrent_flags::auto_managed
;
792 QVector
<Column
> columns
{
793 DB_COLUMN_TORRENT_ID
,
797 DB_COLUMN_TARGET_SAVE_PATH
,
798 DB_COLUMN_DOWNLOAD_PATH
,
799 DB_COLUMN_CONTENT_LAYOUT
,
800 DB_COLUMN_RATIO_LIMIT
,
801 DB_COLUMN_SEEDING_TIME_LIMIT
,
802 DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
,
803 DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
,
804 DB_COLUMN_HAS_SEED_STATUS
,
805 DB_COLUMN_OPERATING_MODE
,
807 DB_COLUMN_STOP_CONDITION
,
811 lt::entry data
= lt::write_resume_data(p
);
813 // metadata is stored in separate column
814 QByteArray bencodedMetadata
;
817 lt::entry::dictionary_type
&dataDict
= data
.dict();
818 lt::entry metadata
{lt::entry::dictionary_t
};
819 lt::entry::dictionary_type
&metadataDict
= metadata
.dict();
820 metadataDict
.insert(dataDict
.extract("info"));
821 metadataDict
.insert(dataDict
.extract("creation date"));
822 metadataDict
.insert(dataDict
.extract("created by"));
823 metadataDict
.insert(dataDict
.extract("comment"));
827 bencodedMetadata
.reserve(512 * 1024);
828 lt::bencode(std::back_inserter(bencodedMetadata
), metadata
);
830 catch (const std::exception
&err
)
832 LogMsg(ResumeDataStorage::tr("Couldn't save torrent metadata. Error: %1.")
833 .arg(QString::fromLocal8Bit(err
.what())), Log::CRITICAL
);
837 columns
.append(DB_COLUMN_METADATA
);
840 QByteArray bencodedResumeData
;
841 bencodedResumeData
.reserve(256 * 1024);
842 lt::bencode(std::back_inserter(bencodedResumeData
), data
);
844 const QString insertTorrentStatement
= makeInsertStatement(DB_TABLE_TORRENTS
, columns
)
845 + makeOnConflictUpdateStatement(DB_COLUMN_TORRENT_ID
, columns
);
846 QSqlQuery query
{db
};
850 if (!query
.prepare(insertTorrentStatement
))
851 throw RuntimeError(query
.lastError().text());
853 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, m_torrentID
.toString());
854 query
.bindValue(DB_COLUMN_NAME
.placeholder
, m_resumeData
.name
);
855 query
.bindValue(DB_COLUMN_CATEGORY
.placeholder
, m_resumeData
.category
);
856 query
.bindValue(DB_COLUMN_TAGS
.placeholder
, (m_resumeData
.tags
.isEmpty()
857 ? QString() : m_resumeData
.tags
.join(u
","_s
)));
858 query
.bindValue(DB_COLUMN_CONTENT_LAYOUT
.placeholder
, Utils::String::fromEnum(m_resumeData
.contentLayout
));
859 query
.bindValue(DB_COLUMN_RATIO_LIMIT
.placeholder
, static_cast<int>(m_resumeData
.ratioLimit
* 1000));
860 query
.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT
.placeholder
, m_resumeData
.seedingTimeLimit
);
861 query
.bindValue(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
.placeholder
, m_resumeData
.inactiveSeedingTimeLimit
);
862 query
.bindValue(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
.placeholder
, m_resumeData
.firstLastPiecePriority
);
863 query
.bindValue(DB_COLUMN_HAS_SEED_STATUS
.placeholder
, m_resumeData
.hasFinishedStatus
);
864 query
.bindValue(DB_COLUMN_OPERATING_MODE
.placeholder
, Utils::String::fromEnum(m_resumeData
.operatingMode
));
865 query
.bindValue(DB_COLUMN_STOPPED
.placeholder
, m_resumeData
.stopped
);
866 query
.bindValue(DB_COLUMN_STOP_CONDITION
.placeholder
, Utils::String::fromEnum(m_resumeData
.stopCondition
));
868 if (!m_resumeData
.useAutoTMM
)
870 query
.bindValue(DB_COLUMN_TARGET_SAVE_PATH
.placeholder
, Profile::instance()->toPortablePath(m_resumeData
.savePath
).data());
871 query
.bindValue(DB_COLUMN_DOWNLOAD_PATH
.placeholder
, Profile::instance()->toPortablePath(m_resumeData
.downloadPath
).data());
874 query
.bindValue(DB_COLUMN_RESUMEDATA
.placeholder
, bencodedResumeData
);
875 if (!bencodedMetadata
.isEmpty())
876 query
.bindValue(DB_COLUMN_METADATA
.placeholder
, bencodedMetadata
);
879 throw RuntimeError(query
.lastError().text());
881 catch (const RuntimeError
&err
)
883 LogMsg(ResumeDataStorage::tr("Couldn't store resume data for torrent '%1'. Error: %2")
884 .arg(m_torrentID
.toString(), err
.message()), Log::CRITICAL
);
888 RemoveJob::RemoveJob(const TorrentID
&torrentID
)
889 : m_torrentID
{torrentID
}
893 void RemoveJob::perform(QSqlDatabase db
)
895 const auto deleteTorrentStatement
= u
"DELETE FROM %1 WHERE %2 = %3;"_s
896 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
898 QSqlQuery query
{db
};
901 if (!query
.prepare(deleteTorrentStatement
))
902 throw RuntimeError(query
.lastError().text());
904 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, m_torrentID
.toString());
907 throw RuntimeError(query
.lastError().text());
909 catch (const RuntimeError
&err
)
911 LogMsg(ResumeDataStorage::tr("Couldn't delete resume data of torrent '%1'. Error: %2")
912 .arg(m_torrentID
.toString(), err
.message()), Log::CRITICAL
);
916 StoreQueueJob::StoreQueueJob(const QVector
<TorrentID
> &queue
)
921 void StoreQueueJob::perform(QSqlDatabase db
)
923 const auto updateQueuePosStatement
= u
"UPDATE %1 SET %2 = %3 WHERE %4 = %5;"_s
924 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
), DB_COLUMN_QUEUE_POSITION
.placeholder
925 , quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
929 QSqlQuery query
{db
};
931 if (!query
.prepare(updateQueuePosStatement
))
932 throw RuntimeError(query
.lastError().text());
935 for (const TorrentID
&torrentID
: m_queue
)
937 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, torrentID
.toString());
938 query
.bindValue(DB_COLUMN_QUEUE_POSITION
.placeholder
, pos
++);
940 throw RuntimeError(query
.lastError().text());
943 catch (const RuntimeError
&err
)
945 LogMsg(ResumeDataStorage::tr("Couldn't store torrents queue positions. Error: %1")
946 .arg(err
.message()), Log::CRITICAL
);