2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2021 Vladimir Golovnev <glassez@yandex.ru>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 #include "dbresumedatastorage.h"
31 #include <libtorrent/bdecode.hpp>
32 #include <libtorrent/bencode.hpp>
33 #include <libtorrent/entry.hpp>
34 #include <libtorrent/read_resume_data.hpp>
35 #include <libtorrent/torrent_info.hpp>
36 #include <libtorrent/write_resume_data.hpp>
41 #include <QSqlDatabase>
47 #include "base/exceptions.h"
48 #include "base/global.h"
49 #include "base/logger.h"
50 #include "base/profile.h"
51 #include "base/utils/fs.h"
52 #include "base/utils/string.h"
54 #include "loadtorrentparams.h"
58 const char DB_CONNECTION_NAME
[] = "ResumeDataStorage";
60 const int DB_VERSION
= 1;
62 const char DB_TABLE_META
[] = "meta";
63 const char DB_TABLE_TORRENTS
[] = "torrents";
71 Column
makeColumn(const char *columnName
)
73 return {QLatin1String(columnName
), (QLatin1Char(':') + QLatin1String(columnName
))};
76 const Column DB_COLUMN_ID
= makeColumn("id");
77 const Column DB_COLUMN_TORRENT_ID
= makeColumn("torrent_id");
78 const Column DB_COLUMN_QUEUE_POSITION
= makeColumn("queue_position");
79 const Column DB_COLUMN_NAME
= makeColumn("name");
80 const Column DB_COLUMN_CATEGORY
= makeColumn("category");
81 const Column DB_COLUMN_TAGS
= makeColumn("tags");
82 const Column DB_COLUMN_TARGET_SAVE_PATH
= makeColumn("target_save_path");
83 const Column DB_COLUMN_CONTENT_LAYOUT
= makeColumn("content_layout");
84 const Column DB_COLUMN_RATIO_LIMIT
= makeColumn("ratio_limit");
85 const Column DB_COLUMN_SEEDING_TIME_LIMIT
= makeColumn("seeding_time_limit");
86 const Column DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
= makeColumn("has_outer_pieces_priority");
87 const Column DB_COLUMN_HAS_SEED_STATUS
= makeColumn("has_seed_status");
88 const Column DB_COLUMN_OPERATING_MODE
= makeColumn("operating_mode");
89 const Column DB_COLUMN_STOPPED
= makeColumn("stopped");
90 const Column DB_COLUMN_RESUMEDATA
= makeColumn("libtorrent_resume_data");
91 const Column DB_COLUMN_METADATA
= makeColumn("metadata");
92 const Column DB_COLUMN_VALUE
= makeColumn("value");
94 template <typename LTStr
>
95 QString
fromLTString(const LTStr
&str
)
97 return QString::fromUtf8(str
.data(), static_cast<int>(str
.size()));
100 QString
quoted(const QString
&name
)
102 const QLatin1Char quote
{'`'};
104 return (quote
+ name
+ quote
);
107 QString
makeCreateTableStatement(const QString
&tableName
, const QStringList
&items
)
109 return QString::fromLatin1("CREATE TABLE %1 (%2)").arg(quoted(tableName
), items
.join(QLatin1Char(',')));
112 QString
makeInsertStatement(const QString
&tableName
, const QVector
<Column
> &columns
)
115 names
.reserve(columns
.size());
117 values
.reserve(columns
.size());
118 for (const Column
&column
: columns
)
120 names
.append(quoted(column
.name
));
121 values
.append(column
.placeholder
);
124 const QString jointNames
= names
.join(QLatin1Char(','));
125 const QString jointValues
= values
.join(QLatin1Char(','));
127 return QString::fromLatin1("INSERT INTO %1 (%2) VALUES (%3)")
128 .arg(quoted(tableName
), jointNames
, jointValues
);
131 QString
makeOnConflictUpdateStatement(const Column
&constraint
, const QVector
<Column
> &columns
)
134 names
.reserve(columns
.size());
136 values
.reserve(columns
.size());
137 for (const Column
&column
: columns
)
139 names
.append(quoted(column
.name
));
140 values
.append(column
.placeholder
);
143 const QString jointNames
= names
.join(QLatin1Char(','));
144 const QString jointValues
= values
.join(QLatin1Char(','));
146 return QString::fromLatin1(" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)")
147 .arg(quoted(constraint
.name
), jointNames
, jointValues
);
150 QString
makeColumnDefinition(const Column
&column
, const char *definition
)
152 return QString::fromLatin1("%1 %2").arg(quoted(column
.name
), QLatin1String(definition
));
158 class DBResumeDataStorage::Worker final
: public QObject
160 Q_DISABLE_COPY_MOVE(Worker
)
163 Worker(const QString
&dbPath
, const QString
&dbConnectionName
);
165 void openDatabase() const;
166 void closeDatabase() const;
168 void store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
) const;
169 void remove(const TorrentID
&id
) const;
170 void storeQueue(const QVector
<TorrentID
> &queue
) const;
173 const QString m_path
;
174 const QString m_connectionName
;
178 BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const QString
&dbPath
, QObject
*parent
)
179 : ResumeDataStorage
{parent
}
180 , m_ioThread
{new QThread(this)}
182 const bool needCreateDB
= !QFile::exists(dbPath
);
184 auto db
= QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), DB_CONNECTION_NAME
);
185 db
.setDatabaseName(dbPath
);
187 throw RuntimeError(db
.lastError().text());
192 m_asyncWorker
= new Worker(dbPath
, QLatin1String("ResumeDataStorageWorker"));
193 m_asyncWorker
->moveToThread(m_ioThread
);
194 connect(m_ioThread
, &QThread::finished
, m_asyncWorker
, &QObject::deleteLater
);
197 RuntimeError
*errPtr
= nullptr;
198 QMetaObject::invokeMethod(m_asyncWorker
, [this, &errPtr
]()
202 m_asyncWorker
->openDatabase();
204 catch (const RuntimeError
&err
)
206 errPtr
= new RuntimeError(err
);
208 }, Qt::BlockingQueuedConnection
);
214 BitTorrent::DBResumeDataStorage::~DBResumeDataStorage()
216 QMetaObject::invokeMethod(m_asyncWorker
, &Worker::closeDatabase
);
217 QSqlDatabase::removeDatabase(DB_CONNECTION_NAME
);
223 QVector
<BitTorrent::TorrentID
> BitTorrent::DBResumeDataStorage::registeredTorrents() const
225 const auto selectTorrentIDStatement
= QString::fromLatin1("SELECT %1 FROM %2 ORDER BY %3;")
226 .arg(quoted(DB_COLUMN_TORRENT_ID
.name
), quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
));
228 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
229 QSqlQuery query
{db
};
231 if (!query
.exec(selectTorrentIDStatement
))
232 throw RuntimeError(query
.lastError().text());
234 QVector
<TorrentID
> registeredTorrents
;
235 registeredTorrents
.reserve(query
.size());
237 registeredTorrents
.append(BitTorrent::TorrentID::fromString(query
.value(0).toString()));
239 return registeredTorrents
;
242 std::optional
<BitTorrent::LoadTorrentParams
> BitTorrent::DBResumeDataStorage::load(const TorrentID
&id
) const
244 const QString selectTorrentStatement
=
245 QString(QLatin1String("SELECT * FROM %1 WHERE %2 = %3;"))
246 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
248 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
249 QSqlQuery query
{db
};
252 if (!query
.prepare(selectTorrentStatement
))
253 throw RuntimeError(query
.lastError().text());
255 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, id
.toString());
257 throw RuntimeError(query
.lastError().text());
260 throw RuntimeError(tr("Not found."));
262 catch (const RuntimeError
&err
)
264 LogMsg(tr("Couldn't load resume data of torrent '%1'. Error: %2")
265 .arg(id
.toString(), err
.message()), Log::CRITICAL
);
269 LoadTorrentParams resumeData
;
270 resumeData
.restored
= true;
271 resumeData
.name
= query
.value(DB_COLUMN_NAME
.name
).toString();
272 resumeData
.category
= query
.value(DB_COLUMN_CATEGORY
.name
).toString();
273 const QString tagsData
= query
.value(DB_COLUMN_TAGS
.name
).toString();
274 if (!tagsData
.isEmpty())
276 const QStringList tagList
= tagsData
.split(QLatin1Char(','));
277 resumeData
.tags
.insert(tagList
.cbegin(), tagList
.cend());
279 resumeData
.savePath
= Profile::instance()->fromPortablePath(
280 Utils::Fs::toUniformPath(query
.value(DB_COLUMN_TARGET_SAVE_PATH
.name
).toString()));
281 resumeData
.hasSeedStatus
= query
.value(DB_COLUMN_HAS_SEED_STATUS
.name
).toBool();
282 resumeData
.firstLastPiecePriority
= query
.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
.name
).toBool();
283 resumeData
.ratioLimit
= query
.value(DB_COLUMN_RATIO_LIMIT
.name
).toInt() / 1000.0;
284 resumeData
.seedingTimeLimit
= query
.value(DB_COLUMN_SEEDING_TIME_LIMIT
.name
).toInt();
285 resumeData
.contentLayout
= Utils::String::toEnum
<TorrentContentLayout
>(
286 query
.value(DB_COLUMN_CONTENT_LAYOUT
.name
).toString(), TorrentContentLayout::Original
);
287 resumeData
.operatingMode
= Utils::String::toEnum
<TorrentOperatingMode
>(
288 query
.value(DB_COLUMN_OPERATING_MODE
.name
).toString(), TorrentOperatingMode::AutoManaged
);
289 resumeData
.stopped
= query
.value(DB_COLUMN_STOPPED
.name
).toBool();
291 const QByteArray bencodedResumeData
= query
.value(DB_COLUMN_RESUMEDATA
.name
).toByteArray();
292 const QByteArray bencodedMetadata
= query
.value(DB_COLUMN_METADATA
.name
).toByteArray();
293 const QByteArray allData
= ((bencodedMetadata
.isEmpty() || bencodedResumeData
.isEmpty())
295 : (bencodedResumeData
.chopped(1) + bencodedMetadata
.mid(1)));
298 const lt::bdecode_node root
= lt::bdecode(allData
, ec
);
300 lt::add_torrent_params
&p
= resumeData
.ltAddTorrentParams
;
302 p
= lt::read_resume_data(root
, ec
);
303 p
.save_path
= Profile::instance()->fromPortablePath(fromLTString(p
.save_path
)).toStdString();
308 void BitTorrent::DBResumeDataStorage::store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
) const
310 QMetaObject::invokeMethod(m_asyncWorker
, [this, id
, resumeData
]()
312 m_asyncWorker
->store(id
, resumeData
);
316 void BitTorrent::DBResumeDataStorage::remove(const BitTorrent::TorrentID
&id
) const
318 QMetaObject::invokeMethod(m_asyncWorker
, [this, id
]()
320 m_asyncWorker
->remove(id
);
324 void BitTorrent::DBResumeDataStorage::storeQueue(const QVector
<TorrentID
> &queue
) const
326 QMetaObject::invokeMethod(m_asyncWorker
, [this, queue
]()
328 m_asyncWorker
->storeQueue(queue
);
332 void BitTorrent::DBResumeDataStorage::createDB() const
334 auto db
= QSqlDatabase::database(DB_CONNECTION_NAME
);
336 if (!db
.transaction())
337 throw RuntimeError(db
.lastError().text());
339 QSqlQuery query
{db
};
343 const QStringList tableMetaItems
= {
344 makeColumnDefinition(DB_COLUMN_ID
, "INTEGER PRIMARY KEY"),
345 makeColumnDefinition(DB_COLUMN_NAME
, "TEXT NOT NULL UNIQUE"),
346 makeColumnDefinition(DB_COLUMN_VALUE
, "BLOB")
348 const QString createTableMetaQuery
= makeCreateTableStatement(DB_TABLE_META
, tableMetaItems
);
349 if (!query
.exec(createTableMetaQuery
))
350 throw RuntimeError(query
.lastError().text());
352 const QString insertMetaVersionQuery
= makeInsertStatement(DB_TABLE_META
, {DB_COLUMN_NAME
, DB_COLUMN_VALUE
});
353 if (!query
.prepare(insertMetaVersionQuery
))
354 throw RuntimeError(query
.lastError().text());
356 query
.bindValue(DB_COLUMN_NAME
.placeholder
, QString::fromLatin1("version"));
357 query
.bindValue(DB_COLUMN_VALUE
.placeholder
, DB_VERSION
);
360 throw RuntimeError(query
.lastError().text());
362 const QStringList tableTorrentsItems
= {
363 makeColumnDefinition(DB_COLUMN_ID
, "INTEGER PRIMARY KEY"),
364 makeColumnDefinition(DB_COLUMN_TORRENT_ID
, "BLOB NOT NULL UNIQUE"),
365 makeColumnDefinition(DB_COLUMN_QUEUE_POSITION
, "INTEGER NOT NULL DEFAULT -1"),
366 makeColumnDefinition(DB_COLUMN_NAME
, "TEXT"),
367 makeColumnDefinition(DB_COLUMN_CATEGORY
, "TEXT"),
368 makeColumnDefinition(DB_COLUMN_TAGS
, "TEXT"),
369 makeColumnDefinition(DB_COLUMN_TARGET_SAVE_PATH
, "TEXT"),
370 makeColumnDefinition(DB_COLUMN_CONTENT_LAYOUT
, "TEXT NOT NULL"),
371 makeColumnDefinition(DB_COLUMN_RATIO_LIMIT
, "INTEGER NOT NULL"),
372 makeColumnDefinition(DB_COLUMN_SEEDING_TIME_LIMIT
, "INTEGER NOT NULL"),
373 makeColumnDefinition(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
, "INTEGER NOT NULL"),
374 makeColumnDefinition(DB_COLUMN_HAS_SEED_STATUS
, "INTEGER NOT NULL"),
375 makeColumnDefinition(DB_COLUMN_OPERATING_MODE
, "TEXT NOT NULL"),
376 makeColumnDefinition(DB_COLUMN_STOPPED
, "INTEGER NOT NULL"),
377 makeColumnDefinition(DB_COLUMN_RESUMEDATA
, "BLOB NOT NULL"),
378 makeColumnDefinition(DB_COLUMN_METADATA
, "BLOB")
380 const QString createTableTorrentsQuery
= makeCreateTableStatement(DB_TABLE_TORRENTS
, tableTorrentsItems
);
381 if (!query
.exec(createTableTorrentsQuery
))
382 throw RuntimeError(query
.lastError().text());
385 throw RuntimeError(db
.lastError().text());
387 catch (const RuntimeError
&)
394 BitTorrent::DBResumeDataStorage::Worker::Worker(const QString
&dbPath
, const QString
&dbConnectionName
)
396 , m_connectionName
{dbConnectionName
}
400 void BitTorrent::DBResumeDataStorage::Worker::openDatabase() const
402 auto db
= QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), m_connectionName
);
403 db
.setDatabaseName(m_path
);
405 throw RuntimeError(db
.lastError().text());
408 void BitTorrent::DBResumeDataStorage::Worker::closeDatabase() const
410 QSqlDatabase::removeDatabase(m_connectionName
);
413 void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
) const
415 // We need to adjust native libtorrent resume data
416 lt::add_torrent_params p
= resumeData
.ltAddTorrentParams
;
417 p
.save_path
= Profile::instance()->toPortablePath(QString::fromStdString(p
.save_path
)).toStdString();
418 if (resumeData
.stopped
)
420 p
.flags
|= lt::torrent_flags::paused
;
421 p
.flags
&= ~lt::torrent_flags::auto_managed
;
425 // Torrent can be actually "running" but temporarily "paused" to perform some
426 // service jobs behind the scenes so we need to restore it as "running"
427 if (resumeData
.operatingMode
== BitTorrent::TorrentOperatingMode::AutoManaged
)
429 p
.flags
|= lt::torrent_flags::auto_managed
;
433 p
.flags
&= ~lt::torrent_flags::paused
;
434 p
.flags
&= ~lt::torrent_flags::auto_managed
;
438 QVector
<Column
> columns
{
439 DB_COLUMN_TORRENT_ID
,
443 DB_COLUMN_TARGET_SAVE_PATH
,
444 DB_COLUMN_CONTENT_LAYOUT
,
445 DB_COLUMN_RATIO_LIMIT
,
446 DB_COLUMN_SEEDING_TIME_LIMIT
,
447 DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
,
448 DB_COLUMN_HAS_SEED_STATUS
,
449 DB_COLUMN_OPERATING_MODE
,
454 lt::entry data
= lt::write_resume_data(p
);
456 // metadata is stored in separate column
457 QByteArray bencodedMetadata
;
460 lt::entry::dictionary_type
&dataDict
= data
.dict();
461 lt::entry metadata
{lt::entry::dictionary_t
};
462 lt::entry::dictionary_type
&metadataDict
= metadata
.dict();
463 metadataDict
.insert(dataDict
.extract("info"));
464 metadataDict
.insert(dataDict
.extract("creation date"));
465 metadataDict
.insert(dataDict
.extract("created by"));
466 metadataDict
.insert(dataDict
.extract("comment"));
470 bencodedMetadata
.reserve(512 * 1024);
471 lt::bencode(std::back_inserter(bencodedMetadata
), metadata
);
473 catch (const std::exception
&err
)
475 LogMsg(tr("Couldn't save torrent metadata. Error: %1.")
476 .arg(QString::fromLocal8Bit(err
.what())), Log::CRITICAL
);
480 columns
.append(DB_COLUMN_METADATA
);
483 QByteArray bencodedResumeData
;
484 bencodedResumeData
.reserve(256 * 1024);
485 lt::bencode(std::back_inserter(bencodedResumeData
), data
);
487 const QString insertTorrentStatement
= makeInsertStatement(DB_TABLE_TORRENTS
, columns
)
488 + makeOnConflictUpdateStatement(DB_COLUMN_TORRENT_ID
, columns
);
489 auto db
= QSqlDatabase::database(m_connectionName
);
490 QSqlQuery query
{db
};
494 if (!query
.prepare(insertTorrentStatement
))
495 throw RuntimeError(query
.lastError().text());
497 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, id
.toString());
498 query
.bindValue(DB_COLUMN_NAME
.placeholder
, resumeData
.name
);
499 query
.bindValue(DB_COLUMN_CATEGORY
.placeholder
, resumeData
.category
);
500 query
.bindValue(DB_COLUMN_TAGS
.placeholder
, (resumeData
.tags
.isEmpty()
501 ? QVariant(QVariant::String
) : resumeData
.tags
.join(QLatin1String(","))));
502 query
.bindValue(DB_COLUMN_TARGET_SAVE_PATH
.placeholder
, Profile::instance()->toPortablePath(resumeData
.savePath
));
503 query
.bindValue(DB_COLUMN_CONTENT_LAYOUT
.placeholder
, Utils::String::fromEnum(resumeData
.contentLayout
));
504 query
.bindValue(DB_COLUMN_RATIO_LIMIT
.placeholder
, static_cast<int>(resumeData
.ratioLimit
* 1000));
505 query
.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT
.placeholder
, resumeData
.seedingTimeLimit
);
506 query
.bindValue(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY
.placeholder
, resumeData
.firstLastPiecePriority
);
507 query
.bindValue(DB_COLUMN_HAS_SEED_STATUS
.placeholder
, resumeData
.hasSeedStatus
);
508 query
.bindValue(DB_COLUMN_OPERATING_MODE
.placeholder
, Utils::String::fromEnum(resumeData
.operatingMode
));
509 query
.bindValue(DB_COLUMN_STOPPED
.placeholder
, resumeData
.stopped
);
510 query
.bindValue(DB_COLUMN_RESUMEDATA
.placeholder
, bencodedResumeData
);
511 if (!bencodedMetadata
.isEmpty())
512 query
.bindValue(DB_COLUMN_METADATA
.placeholder
, bencodedMetadata
);
515 throw RuntimeError(query
.lastError().text());
517 catch (const RuntimeError
&err
)
519 LogMsg(tr("Couldn't store resume data for torrent '%1'. Error: %2")
520 .arg(id
.toString(), err
.message()), Log::CRITICAL
);
524 void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID
&id
) const
526 const auto deleteTorrentStatement
= QString::fromLatin1("DELETE FROM %1 WHERE %2 = %3;")
527 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
529 auto db
= QSqlDatabase::database(m_connectionName
);
530 QSqlQuery query
{db
};
534 if (!query
.prepare(deleteTorrentStatement
))
535 throw RuntimeError(query
.lastError().text());
537 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, id
.toString());
539 throw RuntimeError(query
.lastError().text());
541 catch (const RuntimeError
&err
)
543 LogMsg(tr("Couldn't delete resume data of torrent '%1'. Error: %2")
544 .arg(id
.toString(), err
.message()), Log::CRITICAL
);
548 void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector
<TorrentID
> &queue
) const
550 const auto updateQueuePosStatement
= QString::fromLatin1("UPDATE %1 SET %2 = %3 WHERE %4 = %5;")
551 .arg(quoted(DB_TABLE_TORRENTS
), quoted(DB_COLUMN_QUEUE_POSITION
.name
), DB_COLUMN_QUEUE_POSITION
.placeholder
552 , quoted(DB_COLUMN_TORRENT_ID
.name
), DB_COLUMN_TORRENT_ID
.placeholder
);
554 auto db
= QSqlDatabase::database(m_connectionName
);
558 if (!db
.transaction())
559 throw RuntimeError(db
.lastError().text());
561 QSqlQuery query
{db
};
565 if (!query
.prepare(updateQueuePosStatement
))
566 throw RuntimeError(query
.lastError().text());
569 for (const TorrentID
&torrentID
: queue
)
571 query
.bindValue(DB_COLUMN_TORRENT_ID
.placeholder
, torrentID
.toString());
572 query
.bindValue(DB_COLUMN_QUEUE_POSITION
.placeholder
, pos
++);
574 throw RuntimeError(query
.lastError().text());
578 throw RuntimeError(db
.lastError().text());
580 catch (const RuntimeError
&)
586 catch (const RuntimeError
&err
)
588 LogMsg(tr("Couldn't store torrents queue positions. Error: %1")
589 .arg(err
.message()), Log::CRITICAL
);