qBittorrent
dbresumedatastorage.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2021 Vladimir Golovnev <glassez@yandex.ru>
4  *
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.
9  *
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.
14  *
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.
18  *
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.
27  */
28 
29 #include "dbresumedatastorage.h"
30 
31 #include <utility>
32 
33 #include <libtorrent/bdecode.hpp>
34 #include <libtorrent/bencode.hpp>
35 #include <libtorrent/entry.hpp>
36 #include <libtorrent/read_resume_data.hpp>
37 #include <libtorrent/torrent_info.hpp>
38 #include <libtorrent/write_resume_data.hpp>
39 
40 #include <QByteArray>
41 #include <QFile>
42 #include <QSet>
43 #include <QSqlDatabase>
44 #include <QSqlError>
45 #include <QSqlQuery>
46 #include <QThread>
47 #include <QVector>
48 
49 #include "base/exceptions.h"
50 #include "base/global.h"
51 #include "base/logger.h"
52 #include "base/profile.h"
53 #include "base/utils/fs.h"
54 #include "base/utils/string.h"
55 #include "infohash.h"
56 #include "loadtorrentparams.h"
57 
58 namespace
59 {
60  const char DB_CONNECTION_NAME[] = "ResumeDataStorage";
61 
62  const int DB_VERSION = 2;
63 
64  const char DB_TABLE_META[] = "meta";
65  const char DB_TABLE_TORRENTS[] = "torrents";
66 
67  const char META_VERSION[] = "version";
68 
69  struct Column
70  {
71  QString name;
72  QString placeholder;
73  };
74 
75  Column makeColumn(const char *columnName)
76  {
77  return {QLatin1String(columnName), (QLatin1Char(':') + QLatin1String(columnName))};
78  }
79 
81  const Column DB_COLUMN_TORRENT_ID = makeColumn("torrent_id");
82  const Column DB_COLUMN_QUEUE_POSITION = makeColumn("queue_position");
83  const Column DB_COLUMN_NAME = makeColumn("name");
84  const Column DB_COLUMN_CATEGORY = makeColumn("category");
85  const Column DB_COLUMN_TAGS = makeColumn("tags");
86  const Column DB_COLUMN_TARGET_SAVE_PATH = makeColumn("target_save_path");
87  const Column DB_COLUMN_DOWNLOAD_PATH = makeColumn("download_path");
88  const Column DB_COLUMN_CONTENT_LAYOUT = makeColumn("content_layout");
89  const Column DB_COLUMN_RATIO_LIMIT = makeColumn("ratio_limit");
90  const Column DB_COLUMN_SEEDING_TIME_LIMIT = makeColumn("seeding_time_limit");
91  const Column DB_COLUMN_HAS_OUTER_PIECES_PRIORITY = makeColumn("has_outer_pieces_priority");
92  const Column DB_COLUMN_HAS_SEED_STATUS = makeColumn("has_seed_status");
93  const Column DB_COLUMN_OPERATING_MODE = makeColumn("operating_mode");
94  const Column DB_COLUMN_STOPPED = makeColumn("stopped");
95  const Column DB_COLUMN_RESUMEDATA = makeColumn("libtorrent_resume_data");
96  const Column DB_COLUMN_METADATA = makeColumn("metadata");
97  const Column DB_COLUMN_VALUE = makeColumn("value");
98 
99  template <typename LTStr>
100  QString fromLTString(const LTStr &str)
101  {
102  return QString::fromUtf8(str.data(), static_cast<int>(str.size()));
103  }
104 
105  QString quoted(const QString &name)
106  {
107  const QLatin1Char quote {'`'};
108 
109  return (quote + name + quote);
110  }
111 
112  QString makeCreateTableStatement(const QString &tableName, const QStringList &items)
113  {
114  return QString::fromLatin1("CREATE TABLE %1 (%2)").arg(quoted(tableName), items.join(QLatin1Char(',')));
115  }
116 
117  std::pair<QString, QString> joinColumns(const QVector<Column> &columns)
118  {
119  int namesSize = columns.size();
120  int valuesSize = columns.size();
121  for (const Column &column : columns)
122  {
123  namesSize += column.name.size() + 2;
124  valuesSize += column.placeholder.size();
125  }
126 
127  QString names;
128  names.reserve(namesSize);
129  QString values;
130  values.reserve(valuesSize);
131  for (const Column &column : columns)
132  {
133  names.append(quoted(column.name) + QLatin1Char(','));
134  values.append(column.placeholder + QLatin1Char(','));
135  }
136  names.chop(1);
137  values.chop(1);
138 
139  return std::make_pair(names, values);
140  }
141 
142  QString makeInsertStatement(const QString &tableName, const QVector<Column> &columns)
143  {
144  const auto [names, values] = joinColumns(columns);
145  return QString::fromLatin1("INSERT INTO %1 (%2) VALUES (%3)")
146  .arg(quoted(tableName), names, values);
147  }
148 
149  QString makeUpdateStatement(const QString &tableName, const QVector<Column> &columns)
150  {
151  const auto [names, values] = joinColumns(columns);
152  return QString::fromLatin1("UPDATE %1 SET (%2) = (%3)")
153  .arg(quoted(tableName), names, values);
154  }
155 
156  QString makeOnConflictUpdateStatement(const Column &constraint, const QVector<Column> &columns)
157  {
158  const auto [names, values] = joinColumns(columns);
159  return QString::fromLatin1(" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)")
160  .arg(quoted(constraint.name), names, values);
161  }
162 
163  QString makeColumnDefinition(const Column &column, const char *definition)
164  {
165  return QString::fromLatin1("%1 %2").arg(quoted(column.name), QLatin1String(definition));
166  }
167 }
168 
169 namespace BitTorrent
170 {
171  class DBResumeDataStorage::Worker final : public QObject
172  {
173  Q_DISABLE_COPY_MOVE(Worker)
174 
175  public:
176  Worker(const QString &dbPath, const QString &dbConnectionName);
177 
178  void openDatabase() const;
179  void closeDatabase() const;
180 
181  void store(const TorrentID &id, const LoadTorrentParams &resumeData) const;
182  void remove(const TorrentID &id) const;
183  void storeQueue(const QVector<TorrentID> &queue) const;
184 
185  private:
186  const QString m_path;
187  const QString m_connectionName;
188  };
189 }
190 
191 BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const QString &dbPath, QObject *parent)
192  : ResumeDataStorage {parent}
193  , m_ioThread {new QThread(this)}
194 {
195  const bool needCreateDB = !QFile::exists(dbPath);
196 
197  auto db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), DB_CONNECTION_NAME);
198  db.setDatabaseName(dbPath);
199  if (!db.open())
200  throw RuntimeError(db.lastError().text());
201 
202  if (needCreateDB)
203  {
204  createDB();
205  }
206  else
207  {
208  const int dbVersion = currentDBVersion();
209  if (dbVersion == 1)
211  }
212 
213  m_asyncWorker = new Worker(dbPath, QLatin1String("ResumeDataStorageWorker"));
214  m_asyncWorker->moveToThread(m_ioThread);
215  connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater);
216  m_ioThread->start();
217 
218  RuntimeError *errPtr = nullptr;
219  QMetaObject::invokeMethod(m_asyncWorker, [this, &errPtr]()
220  {
221  try
222  {
224  }
225  catch (const RuntimeError &err)
226  {
227  errPtr = new RuntimeError(err);
228  }
229  }, Qt::BlockingQueuedConnection);
230 
231  if (errPtr)
232  throw *errPtr;
233 }
234 
236 {
237  QMetaObject::invokeMethod(m_asyncWorker, &Worker::closeDatabase);
238  QSqlDatabase::removeDatabase(DB_CONNECTION_NAME);
239 
240  m_ioThread->quit();
241  m_ioThread->wait();
242 }
243 
244 QVector<BitTorrent::TorrentID> BitTorrent::DBResumeDataStorage::registeredTorrents() const
245 {
246  const auto selectTorrentIDStatement = QString::fromLatin1("SELECT %1 FROM %2 ORDER BY %3;")
248 
249  auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
250  QSqlQuery query {db};
251 
252  if (!query.exec(selectTorrentIDStatement))
253  throw RuntimeError(query.lastError().text());
254 
255  QVector<TorrentID> registeredTorrents;
256  registeredTorrents.reserve(query.size());
257  while (query.next())
258  registeredTorrents.append(BitTorrent::TorrentID::fromString(query.value(0).toString()));
259 
260  return registeredTorrents;
261 }
262 
263 std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
264 {
265  const QString selectTorrentStatement =
266  QString(QLatin1String("SELECT * FROM %1 WHERE %2 = %3;"))
268 
269  auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
270  QSqlQuery query {db};
271  try
272  {
273  if (!query.prepare(selectTorrentStatement))
274  throw RuntimeError(query.lastError().text());
275 
276  query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
277  if (!query.exec())
278  throw RuntimeError(query.lastError().text());
279 
280  if (!query.next())
281  throw RuntimeError(tr("Not found."));
282  }
283  catch (const RuntimeError &err)
284  {
285  LogMsg(tr("Couldn't load resume data of torrent '%1'. Error: %2")
286  .arg(id.toString(), err.message()), Log::CRITICAL);
287  return std::nullopt;
288  }
289 
290  LoadTorrentParams resumeData;
291  resumeData.restored = true;
292  resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
293  resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
294  const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
295  if (!tagsData.isEmpty())
296  {
297  const QStringList tagList = tagsData.split(QLatin1Char(','));
298  resumeData.tags.insert(tagList.cbegin(), tagList.cend());
299  }
300  resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
301  resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
302  resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
303  resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
304  resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
305  query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
306  resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
307  query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
308  resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
309 
311  Utils::Fs::toUniformPath(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
312  resumeData.useAutoTMM = resumeData.savePath.isEmpty();
313  if (!resumeData.useAutoTMM)
314  {
316  Utils::Fs::toUniformPath(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
317  }
318 
319  const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
320  const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
321  const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
322  ? bencodedResumeData
323  : (bencodedResumeData.chopped(1) + bencodedMetadata.mid(1)));
324 
325  lt::error_code ec;
326  const lt::bdecode_node root = lt::bdecode(allData, ec);
327 
329  Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-downloadPath"))));
330 
331  lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
332 
333  p = lt::read_resume_data(root, ec);
334  p.save_path = Profile::instance()->fromPortablePath(fromLTString(p.save_path)).toStdString();
335 
336  return resumeData;
337 }
338 
340 {
341  QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData]()
342  {
343  m_asyncWorker->store(id, resumeData);
344  });
345 }
346 
348 {
349  QMetaObject::invokeMethod(m_asyncWorker, [this, id]()
350  {
351  m_asyncWorker->remove(id);
352  });
353 }
354 
355 void BitTorrent::DBResumeDataStorage::storeQueue(const QVector<TorrentID> &queue) const
356 {
357  QMetaObject::invokeMethod(m_asyncWorker, [this, queue]()
358  {
359  m_asyncWorker->storeQueue(queue);
360  });
361 }
362 
364 {
365  const auto selectDBVersionStatement = QString::fromLatin1("SELECT %1 FROM %2 WHERE %3 = %4;")
367 
368  auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
369  QSqlQuery query {db};
370 
371  if (!query.prepare(selectDBVersionStatement))
372  throw RuntimeError(query.lastError().text());
373 
374  query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION));
375 
376  if (!query.exec())
377  throw RuntimeError(query.lastError().text());
378 
379  if (!query.next())
380  throw RuntimeError(tr("Database is corrupted."));
381 
382  bool ok;
383  const int dbVersion = query.value(0).toInt(&ok);
384  if (!ok)
385  throw RuntimeError(tr("Database is corrupted."));
386 
387  return dbVersion;
388 }
389 
391 {
392  auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
393 
394  if (!db.transaction())
395  throw RuntimeError(db.lastError().text());
396 
397  QSqlQuery query {db};
398 
399  try
400  {
401  const QStringList tableMetaItems = {
402  makeColumnDefinition(DB_COLUMN_ID, "INTEGER PRIMARY KEY"),
403  makeColumnDefinition(DB_COLUMN_NAME, "TEXT NOT NULL UNIQUE"),
405  };
406  const QString createTableMetaQuery = makeCreateTableStatement(DB_TABLE_META, tableMetaItems);
407  if (!query.exec(createTableMetaQuery))
408  throw RuntimeError(query.lastError().text());
409 
410  const QString insertMetaVersionQuery = makeInsertStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
411  if (!query.prepare(insertMetaVersionQuery))
412  throw RuntimeError(query.lastError().text());
413 
414  query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION));
415  query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
416 
417  if (!query.exec())
418  throw RuntimeError(query.lastError().text());
419 
420  const QStringList tableTorrentsItems = {
421  makeColumnDefinition(DB_COLUMN_ID, "INTEGER PRIMARY KEY"),
422  makeColumnDefinition(DB_COLUMN_TORRENT_ID, "BLOB NOT NULL UNIQUE"),
423  makeColumnDefinition(DB_COLUMN_QUEUE_POSITION, "INTEGER NOT NULL DEFAULT -1"),
429  makeColumnDefinition(DB_COLUMN_RATIO_LIMIT, "INTEGER NOT NULL"),
432  makeColumnDefinition(DB_COLUMN_HAS_SEED_STATUS, "INTEGER NOT NULL"),
434  makeColumnDefinition(DB_COLUMN_STOPPED, "INTEGER NOT NULL"),
435  makeColumnDefinition(DB_COLUMN_RESUMEDATA, "BLOB NOT NULL"),
437  };
438  const QString createTableTorrentsQuery = makeCreateTableStatement(DB_TABLE_TORRENTS, tableTorrentsItems);
439  if (!query.exec(createTableTorrentsQuery))
440  throw RuntimeError(query.lastError().text());
441 
442  if (!db.commit())
443  throw RuntimeError(db.lastError().text());
444  }
445  catch (const RuntimeError &)
446  {
447  db.rollback();
448  throw;
449  }
450 }
451 
453 {
454  auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
455 
456  if (!db.transaction())
457  throw RuntimeError(db.lastError().text());
458 
459  QSqlQuery query {db};
460 
461  try
462  {
463  const auto alterTableTorrentsQuery = QString::fromLatin1("ALTER TABLE %1 ADD %2")
465  if (!query.exec(alterTableTorrentsQuery))
466  throw RuntimeError(query.lastError().text());
467 
468  const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
469  if (!query.prepare(updateMetaVersionQuery))
470  throw RuntimeError(query.lastError().text());
471 
472  query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION));
473  query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
474 
475  if (!query.exec())
476  throw RuntimeError(query.lastError().text());
477 
478  if (!db.commit())
479  throw RuntimeError(db.lastError().text());
480  }
481  catch (const RuntimeError &)
482  {
483  db.rollback();
484  throw;
485  }
486 }
487 
488 BitTorrent::DBResumeDataStorage::Worker::Worker(const QString &dbPath, const QString &dbConnectionName)
489  : m_path {dbPath}
490  , m_connectionName {dbConnectionName}
491 {
492 }
493 
495 {
496  auto db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), m_connectionName);
497  db.setDatabaseName(m_path);
498  if (!db.open())
499  throw RuntimeError(db.lastError().text());
500 }
501 
503 {
504  QSqlDatabase::removeDatabase(m_connectionName);
505 }
506 
508 {
509  // We need to adjust native libtorrent resume data
510  lt::add_torrent_params p = resumeData.ltAddTorrentParams;
511  p.save_path = Profile::instance()->toPortablePath(QString::fromStdString(p.save_path)).toStdString();
512  if (resumeData.stopped)
513  {
514  p.flags |= lt::torrent_flags::paused;
515  p.flags &= ~lt::torrent_flags::auto_managed;
516  }
517  else
518  {
519  // Torrent can be actually "running" but temporarily "paused" to perform some
520  // service jobs behind the scenes so we need to restore it as "running"
521  if (resumeData.operatingMode == BitTorrent::TorrentOperatingMode::AutoManaged)
522  {
523  p.flags |= lt::torrent_flags::auto_managed;
524  }
525  else
526  {
527  p.flags &= ~lt::torrent_flags::paused;
528  p.flags &= ~lt::torrent_flags::auto_managed;
529  }
530  }
531 
532  QVector<Column> columns {
546  };
547 
548  lt::entry data = lt::write_resume_data(p);
549 
550  // metadata is stored in separate column
551  QByteArray bencodedMetadata;
552  if (p.ti)
553  {
554  lt::entry::dictionary_type &dataDict = data.dict();
555  lt::entry metadata {lt::entry::dictionary_t};
556  lt::entry::dictionary_type &metadataDict = metadata.dict();
557  metadataDict.insert(dataDict.extract("info"));
558  metadataDict.insert(dataDict.extract("creation date"));
559  metadataDict.insert(dataDict.extract("created by"));
560  metadataDict.insert(dataDict.extract("comment"));
561 
562  try
563  {
564  bencodedMetadata.reserve(512 * 1024);
565  lt::bencode(std::back_inserter(bencodedMetadata), metadata);
566  }
567  catch (const std::exception &err)
568  {
569  LogMsg(tr("Couldn't save torrent metadata. Error: %1.")
570  .arg(QString::fromLocal8Bit(err.what())), Log::CRITICAL);
571  return;
572  }
573 
574  columns.append(DB_COLUMN_METADATA);
575  }
576 
577  QByteArray bencodedResumeData;
578  bencodedResumeData.reserve(256 * 1024);
579  lt::bencode(std::back_inserter(bencodedResumeData), data);
580 
581  const QString insertTorrentStatement = makeInsertStatement(DB_TABLE_TORRENTS, columns)
583  auto db = QSqlDatabase::database(m_connectionName);
584  QSqlQuery query {db};
585 
586  try
587  {
588  if (!query.prepare(insertTorrentStatement))
589  throw RuntimeError(query.lastError().text());
590 
591  query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
592  query.bindValue(DB_COLUMN_NAME.placeholder, resumeData.name);
593  query.bindValue(DB_COLUMN_CATEGORY.placeholder, resumeData.category);
594  query.bindValue(DB_COLUMN_TAGS.placeholder, (resumeData.tags.isEmpty()
595  ? QVariant(QVariant::String) : resumeData.tags.join(QLatin1String(","))));
597  query.bindValue(DB_COLUMN_RATIO_LIMIT.placeholder, static_cast<int>(resumeData.ratioLimit * 1000));
598  query.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT.placeholder, resumeData.seedingTimeLimit);
600  query.bindValue(DB_COLUMN_HAS_SEED_STATUS.placeholder, resumeData.hasSeedStatus);
602  query.bindValue(DB_COLUMN_STOPPED.placeholder, resumeData.stopped);
603 
604  if (!resumeData.useAutoTMM)
605  {
608  }
609 
610  query.bindValue(DB_COLUMN_RESUMEDATA.placeholder, bencodedResumeData);
611  if (!bencodedMetadata.isEmpty())
612  query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
613 
614  if (!query.exec())
615  throw RuntimeError(query.lastError().text());
616  }
617  catch (const RuntimeError &err)
618  {
619  LogMsg(tr("Couldn't store resume data for torrent '%1'. Error: %2")
620  .arg(id.toString(), err.message()), Log::CRITICAL);
621  }
622 }
623 
625 {
626  const auto deleteTorrentStatement = QString::fromLatin1("DELETE FROM %1 WHERE %2 = %3;")
628 
629  auto db = QSqlDatabase::database(m_connectionName);
630  QSqlQuery query {db};
631 
632  try
633  {
634  if (!query.prepare(deleteTorrentStatement))
635  throw RuntimeError(query.lastError().text());
636 
637  query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
638  if (!query.exec())
639  throw RuntimeError(query.lastError().text());
640  }
641  catch (const RuntimeError &err)
642  {
643  LogMsg(tr("Couldn't delete resume data of torrent '%1'. Error: %2")
644  .arg(id.toString(), err.message()), Log::CRITICAL);
645  }
646 }
647 
648 void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector<TorrentID> &queue) const
649 {
650  const auto updateQueuePosStatement = QString::fromLatin1("UPDATE %1 SET %2 = %3 WHERE %4 = %5;")
653 
654  auto db = QSqlDatabase::database(m_connectionName);
655 
656  try
657  {
658  if (!db.transaction())
659  throw RuntimeError(db.lastError().text());
660 
661  QSqlQuery query {db};
662 
663  try
664  {
665  if (!query.prepare(updateQueuePosStatement))
666  throw RuntimeError(query.lastError().text());
667 
668  int pos = 0;
669  for (const TorrentID &torrentID : queue)
670  {
671  query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, torrentID.toString());
672  query.bindValue(DB_COLUMN_QUEUE_POSITION.placeholder, pos++);
673  if (!query.exec())
674  throw RuntimeError(query.lastError().text());
675  }
676 
677  if (!db.commit())
678  throw RuntimeError(db.lastError().text());
679  }
680  catch (const RuntimeError &)
681  {
682  db.rollback();
683  throw;
684  }
685  }
686  catch (const RuntimeError &err)
687  {
688  LogMsg(tr("Couldn't store torrents queue positions. Error: %1")
689  .arg(err.message()), Log::CRITICAL);
690  }
691 }
void remove(const TorrentID &id) const
void storeQueue(const QVector< TorrentID > &queue) const
Worker(const QString &dbPath, const QString &dbConnectionName)
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const
QVector< TorrentID > registeredTorrents() const override
void storeQueue(const QVector< TorrentID > &queue) const override
void remove(const TorrentID &id) const override
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override
std::optional< LoadTorrentParams > load(const TorrentID &id) const override
DBResumeDataStorage(const QString &dbPath, QObject *parent=nullptr)
static TorrentID fromString(const QString &hashString)
Definition: infohash.cpp:76
QString message() const noexcept
Definition: exceptions.cpp:36
QString join(const QString &separator) const
Definition: orderedset.h:80
bool isEmpty() const
Definition: orderedset.h:74
QString fromPortablePath(const QString &portablePath) const
Definition: profile.cpp:126
QString toPortablePath(const QString &absolutePath) const
Definition: profile.cpp:121
static const Profile * instance()
Definition: profile.cpp:67
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
@ CRITICAL
Definition: logger.h:48
QString toUniformPath(const QString &path)
Definition: fs.cpp:69
QString fromEnum(const T &value)
Definition: string.h:67
QString makeOnConflictUpdateStatement(const Column &constraint, const QVector< Column > &columns)
QString makeColumnDefinition(const Column &column, const char *definition)
QString makeInsertStatement(const QString &tableName, const QVector< Column > &columns)
QString makeCreateTableStatement(const QString &tableName, const QStringList &items)
QString makeUpdateStatement(const QString &tableName, const QVector< Column > &columns)
std::pair< QString, QString > joinColumns(const QVector< Column > &columns)
QString toString(const lt::socket_type_t socketType)
Definition: session.cpp:183
TorrentContentLayout contentLayout
lt::add_torrent_params ltAddTorrentParams
TorrentOperatingMode operatingMode