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
);
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
);
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 alterTableTorrentsQuery
= u
"ALTER TABLE %1 ADD %2"_s
615 .arg(quoted(DB_TABLE_TORRENTS
), makeColumnDefinition(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
, "INTEGER NOT NULL DEFAULT -2"));
616 if (!query
.exec(alterTableTorrentsQuery
))
617 throw RuntimeError(query
.lastError().text());
620 const QString updateMetaVersionQuery
= makeUpdateStatement(DB_TABLE_META
, {DB_COLUMN_NAME
, DB_COLUMN_VALUE
});
621 if (!query
.prepare(updateMetaVersionQuery
))
622 throw RuntimeError(query
.lastError().text());
624 query
.bindValue(DB_COLUMN_NAME
.placeholder
, META_VERSION
);
625 query
.bindValue(DB_COLUMN_VALUE
.placeholder
, DB_VERSION
);
628 throw RuntimeError(query
.lastError().text());
631 throw RuntimeError(db
.lastError().text());
633 catch (const RuntimeError
&)
640 void BitTorrent::DBResumeDataStorage::enableWALMode() const
642 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
643 QSqlQuery query
{db
};
645 if (!query
.exec(u
"PRAGMA journal_mode = WAL;"_s
))
646 throw RuntimeError(query
.lastError().text());
649 throw RuntimeError(tr("Couldn't obtain query result."));
651 const QString result
= query
.value(0).toString();
652 if (result
.compare(u
"WAL"_s
, Qt::CaseInsensitive
) != 0)
653 throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
656 BitTorrent::DBResumeDataStorage::Worker::Worker(const Path
&dbPath
, QReadWriteLock
&dbLock
)
662 void BitTorrent::DBResumeDataStorage::Worker::run()
665 auto db
= QSqlDatabase::addDatabase(u
"QSQLITE"_s
, m_connectionName
);
666 db
.setDatabaseName(m_path
.data());
668 throw RuntimeError(db
.lastError().text());
670 int64_t transactedJobsCount
= 0;
676 if (transactedJobsCount
> 0)
681 qDebug() << "Resume data changes are committed. Transacted jobs:" << transactedJobsCount
;
682 transactedJobsCount
= 0;
685 if (isInterruptionRequested())
687 m_jobsMutex
.unlock();
691 m_waitCondition
.wait(&m_jobsMutex
);
692 if (isInterruptionRequested())
694 m_jobsMutex
.unlock();
698 m_dbLock
.lockForWrite();
699 if (!db
.transaction())
701 LogMsg(tr("Couldn't begin transaction. Error: %1").arg(db
.lastError().text()), Log::WARNING
);
706 std::unique_ptr
<Job
> job
= std::move(m_jobs
.front());
708 m_jobsMutex
.unlock();
711 ++transactedJobsCount
;
717 QSqlDatabase::removeDatabase(m_connectionName
);
720 void DBResumeDataStorage::Worker::requestInterruption()
722 QThread::requestInterruption();
723 m_waitCondition
.wakeAll();
726 void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
)
728 addJob(std::make_unique
<StoreJob
>(id
, resumeData
));
731 void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID
&id
)
733 addJob(std::make_unique
<RemoveJob
>(id
));
736 void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector
<TorrentID
> &queue
)
738 addJob(std::make_unique
<StoreQueueJob
>(queue
));
741 void BitTorrent::DBResumeDataStorage::Worker::addJob(std::unique_ptr
<Job
> job
)
744 m_jobs
.push(std::move(job
));
745 m_jobsMutex
.unlock();
747 m_waitCondition
.wakeAll();
752 using namespace BitTorrent
;
754 StoreJob::StoreJob(const TorrentID
&torrentID
, const LoadTorrentParams
&resumeData
)
755 : m_torrentID
{torrentID
}
756 , m_resumeData
{resumeData
}
760 void StoreJob::perform(QSqlDatabase db
)
762 // We need to adjust native libtorrent resume data
763 lt::add_torrent_params p
= m_resumeData
.ltAddTorrentParams
;
764 p
.save_path
= Profile::instance()->toPortablePath(Path(p
.save_path
))
765 .toString().toStdString();
766 if (m_resumeData
.stopped
)
768 p
.flags
|= lt::torrent_flags::paused
;
769 p
.flags
&= ~lt::torrent_flags::auto_managed
;
773 // Torrent can be actually "running" but temporarily "paused" to perform some
774 // service jobs behind the scenes so we need to restore it as "running"
775 if (m_resumeData
.operatingMode
== BitTorrent::TorrentOperatingMode::AutoManaged
)
777 p
.flags
|= lt::torrent_flags::auto_managed
;
781 p
.flags
&= ~lt::torrent_flags::paused
;
782 p
.flags
&= ~lt::torrent_flags::auto_managed
;
786 QVector
<Column
> columns
{
787 DB_COLUMN_TORRENT_ID
,
791 DB_COLUMN_TARGET_SAVE_PATH
,
792 DB_COLUMN_DOWNLOAD_PATH
,
793 DB_COLUMN_CONTENT_LAYOUT
,
794 DB_COLUMN_RATIO_LIMIT
,
795 DB_COLUMN_SEEDING_TIME_LIMIT
,
796 DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
,
797 DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
,
798 DB_COLUMN_HAS_SEED_STATUS
,
799 DB_COLUMN_OPERATING_MODE
,
801 DB_COLUMN_STOP_CONDITION
,
805 lt::entry data
= lt::write_resume_data(p
);
807 // metadata is stored in separate column
808 QByteArray bencodedMetadata
;
811 lt::entry::dictionary_type
&dataDict
= data
.dict();
812 lt::entry metadata
{lt::entry::dictionary_t
};
813 lt::entry::dictionary_type
&metadataDict
= metadata
.dict();
814 metadataDict
.insert(dataDict
.extract("info"));
815 metadataDict
.insert(dataDict
.extract("creation date"));
816 metadataDict
.insert(dataDict
.extract("created by"));
817 metadataDict
.insert(dataDict
.extract("comment"));
821 bencodedMetadata
.reserve(512 * 1024);
822 lt::bencode(std::back_inserter(bencodedMetadata
), metadata
);
824 catch (const std::exception
&err
)
826 LogMsg(ResumeDataStorage::tr("Couldn't save torrent metadata. Error: %1.")
827 .arg(QString::fromLocal8Bit(err
.what())), Log::CRITICAL
);
831 columns
.append(DB_COLUMN_METADATA
);
834 QByteArray bencodedResumeData
;
835 bencodedResumeData
.reserve(256 * 1024);
836 lt::bencode(std::back_inserter(bencodedResumeData
), data
);
838 const QString insertTorrentStatement
= makeInsertStatement(DB_TABLE_TORRENTS
, columns
)
839 + makeOnConflictUpdateStatement(DB_COLUMN_TORRENT_ID
, columns
);
840 QSqlQuery query
{db
};
844 if (!query
.prepare(insertTorrentStatement
))
845 throw RuntimeError(query
.lastError().text());
847 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, m_torrentID
.toString());
848 query
.bindValue(DB_COLUMN_NAME
.placeholder
, m_resumeData
.name
);
849 query
.bindValue(DB_COLUMN_CATEGORY
.placeholder
, m_resumeData
.category
);
850 query
.bindValue(DB_COLUMN_TAGS
.placeholder
, (m_resumeData
.tags
.isEmpty()
851 ? QString() : m_resumeData
.tags
.join(u
","_s
)));
852 query
.bindValue(DB_COLUMN_CONTENT_LAYOUT
.placeholder
, Utils::String::fromEnum(m_resumeData
.contentLayout
));
853 query
.bindValue(DB_COLUMN_RATIO_LIMIT
.placeholder
, static_cast<int>(m_resumeData
.ratioLimit
* 1000));
854 query
.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT
.placeholder
, m_resumeData
.seedingTimeLimit
);
855 query
.bindValue(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT
.placeholder
, m_resumeData
.inactiveSeedingTimeLimit
);
856 query
.bindValue(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
.placeholder
, m_resumeData
.firstLastPiecePriority
);
857 query
.bindValue(DB_COLUMN_HAS_SEED_STATUS
.placeholder
, m_resumeData
.hasFinishedStatus
);
858 query
.bindValue(DB_COLUMN_OPERATING_MODE
.placeholder
, Utils::String::fromEnum(m_resumeData
.operatingMode
));
859 query
.bindValue(DB_COLUMN_STOPPED
.placeholder
, m_resumeData
.stopped
);
860 query
.bindValue(DB_COLUMN_STOP_CONDITION
.placeholder
, Utils::String::fromEnum(m_resumeData
.stopCondition
));
862 if (!m_resumeData
.useAutoTMM
)
864 query
.bindValue(DB_COLUMN_TARGET_SAVE_PATH
.placeholder
, Profile::instance()->toPortablePath(m_resumeData
.savePath
).data());
865 query
.bindValue(DB_COLUMN_DOWNLOAD_PATH
.placeholder
, Profile::instance()->toPortablePath(m_resumeData
.downloadPath
).data());
868 query
.bindValue(DB_COLUMN_RESUMEDATA
.placeholder
, bencodedResumeData
);
869 if (!bencodedMetadata
.isEmpty())
870 query
.bindValue(DB_COLUMN_METADATA
.placeholder
, bencodedMetadata
);
873 throw RuntimeError(query
.lastError().text());
875 catch (const RuntimeError
&err
)
877 LogMsg(ResumeDataStorage::tr("Couldn't store resume data for torrent '%1'. Error: %2")
878 .arg(m_torrentID
.toString(), err
.message()), Log::CRITICAL
);
882 RemoveJob::RemoveJob(const TorrentID
&torrentID
)
883 : m_torrentID
{torrentID
}
887 void RemoveJob::perform(QSqlDatabase db
)
889 const auto deleteTorrentStatement
= u
"DELETE FROM %1 WHERE %2 = %3;"_s
890 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
892 QSqlQuery query
{db
};
895 if (!query
.prepare(deleteTorrentStatement
))
896 throw RuntimeError(query
.lastError().text());
898 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, m_torrentID
.toString());
901 throw RuntimeError(query
.lastError().text());
903 catch (const RuntimeError
&err
)
905 LogMsg(ResumeDataStorage::tr("Couldn't delete resume data of torrent '%1'. Error: %2")
906 .arg(m_torrentID
.toString(), err
.message()), Log::CRITICAL
);
910 StoreQueueJob::StoreQueueJob(const QVector
<TorrentID
> &queue
)
915 void StoreQueueJob::perform(QSqlDatabase db
)
917 const auto updateQueuePosStatement
= u
"UPDATE %1 SET %2 = %3 WHERE %4 = %5;"_s
918 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
), DB_COLUMN_QUEUE_POSITION
.placeholder
919 , quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
923 QSqlQuery query
{db
};
925 if (!query
.prepare(updateQueuePosStatement
))
926 throw RuntimeError(query
.lastError().text());
929 for (const TorrentID
&torrentID
: m_queue
)
931 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, torrentID
.toString());
932 query
.bindValue(DB_COLUMN_QUEUE_POSITION
.placeholder
, pos
++);
934 throw RuntimeError(query
.lastError().text());
937 catch (const RuntimeError
&err
)
939 LogMsg(ResumeDataStorage::tr("Couldn't store torrents queue positions. Error: %1")
940 .arg(err
.message()), Log::CRITICAL
);