qBittorrent
bencoderesumedatastorage.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2015, 2018 Vladimir Golovnev <[email protected]>
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 
30 
31 #include <libtorrent/bdecode.hpp>
32 #include <libtorrent/entry.hpp>
33 #include <libtorrent/read_resume_data.hpp>
34 #include <libtorrent/torrent_info.hpp>
35 #include <libtorrent/write_resume_data.hpp>
36 
37 #include <QByteArray>
38 #include <QDebug>
39 #include <QRegularExpression>
40 #include <QThread>
41 
42 #include "base/algorithm.h"
43 #include "base/exceptions.h"
44 #include "base/global.h"
45 #include "base/logger.h"
46 #include "base/profile.h"
47 #include "base/tagset.h"
48 #include "base/utils/fs.h"
49 #include "base/utils/io.h"
50 #include "base/utils/string.h"
51 #include "infohash.h"
52 #include "loadtorrentparams.h"
53 
54 namespace BitTorrent
55 {
56  class BencodeResumeDataStorage::Worker final : public QObject
57  {
58  Q_DISABLE_COPY_MOVE(Worker)
59 
60  public:
61  explicit Worker(const QDir &resumeDataDir);
62 
63  void store(const TorrentID &id, const LoadTorrentParams &resumeData) const;
64  void remove(const TorrentID &id) const;
65  void storeQueue(const QVector<TorrentID> &queue) const;
66 
67  private:
68  const QDir m_resumeDataDir;
69  };
70 }
71 
72 namespace
73 {
74  template <typename LTStr>
75  QString fromLTString(const LTStr &str)
76  {
77  return QString::fromUtf8(str.data(), static_cast<int>(str.size()));
78  }
79 
80  using ListType = lt::entry::list_type;
81 
83  {
84  ListType entryList;
85  entryList.reserve(input.size());
86  for (const QString &setValue : input)
87  entryList.emplace_back(setValue.toStdString());
88  return entryList;
89  }
90 }
91 
93  : ResumeDataStorage {parent}
94  , m_resumeDataDir {path}
95  , m_ioThread {new QThread {this}}
96  , m_asyncWorker {new Worker {m_resumeDataDir}}
97 {
98  if (!m_resumeDataDir.exists() && !m_resumeDataDir.mkpath(m_resumeDataDir.absolutePath()))
99  {
100  throw RuntimeError {tr("Cannot create torrent resume folder: \"%1\"")
101  .arg(Utils::Fs::toNativePath(m_resumeDataDir.absolutePath()))};
102  }
103 
104  const QRegularExpression filenamePattern {QLatin1String("^([A-Fa-f0-9]{40})\\.fastresume$")};
105  const QStringList filenames = m_resumeDataDir.entryList(QStringList(QLatin1String("*.fastresume")), QDir::Files, QDir::Unsorted);
106 
107  m_registeredTorrents.reserve(filenames.size());
108  for (const QString &filename : filenames)
109  {
110  const QRegularExpressionMatch rxMatch = filenamePattern.match(filename);
111  if (rxMatch.hasMatch())
112  m_registeredTorrents.append(TorrentID::fromString(rxMatch.captured(1)));
113  }
114 
115  loadQueue(m_resumeDataDir.absoluteFilePath(QLatin1String("queue")));
116 
117  qDebug() << "Registered torrents count: " << m_registeredTorrents.size();
118 
119  m_asyncWorker->moveToThread(m_ioThread);
120  connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater);
121  m_ioThread->start();
122 }
123 
125 {
126  m_ioThread->quit();
127  m_ioThread->wait();
128 }
129 
130 QVector<BitTorrent::TorrentID> BitTorrent::BencodeResumeDataStorage::registeredTorrents() const
131 {
132  return m_registeredTorrents;
133 }
134 
135 std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const
136 {
137  const QString idString = id.toString();
138  const QString fastresumePath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.fastresume").arg(idString));
139  const QString torrentFilePath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.torrent").arg(idString));
140 
141  QFile resumeDataFile {fastresumePath};
142  if (!resumeDataFile.open(QIODevice::ReadOnly))
143  {
144  LogMsg(tr("Cannot read file %1: %2").arg(fastresumePath, resumeDataFile.errorString()), Log::WARNING);
145  return std::nullopt;
146  }
147 
148  QFile metadataFile {torrentFilePath};
149  if (metadataFile.exists() && !metadataFile.open(QIODevice::ReadOnly))
150  {
151  LogMsg(tr("Cannot read file %1: %2").arg(torrentFilePath, metadataFile.errorString()), Log::WARNING);
152  return std::nullopt;
153  }
154 
155  const QByteArray data = resumeDataFile.readAll();
156  const QByteArray metadata = (metadataFile.isOpen() ? metadataFile.readAll() : "");
157 
158  return loadTorrentResumeData(data, metadata);
159 }
160 
161 std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData(
162  const QByteArray &data, const QByteArray &metadata) const
163 {
164  const QByteArray allData = ((metadata.isEmpty() || data.isEmpty())
165  ? data : (data.chopped(1) + metadata.mid(1)));
166 
167  lt::error_code ec;
168  const lt::bdecode_node root = lt::bdecode(allData, ec);
169  if (ec || (root.type() != lt::bdecode_node::dict_t))
170  return std::nullopt;
171 
172  LoadTorrentParams torrentParams;
173  torrentParams.restored = true;
174  torrentParams.category = fromLTString(root.dict_find_string_value("qBt-category"));
175  torrentParams.name = fromLTString(root.dict_find_string_value("qBt-name"));
176  torrentParams.hasSeedStatus = root.dict_find_int_value("qBt-seedStatus");
177  torrentParams.firstLastPiecePriority = root.dict_find_int_value("qBt-firstLastPiecePriority");
178  torrentParams.seedingTimeLimit = root.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME);
179 
180  torrentParams.savePath = Profile::instance()->fromPortablePath(
181  Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-savePath"))));
182  torrentParams.useAutoTMM = torrentParams.savePath.isEmpty();
183  if (!torrentParams.useAutoTMM)
184  {
186  Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-downloadPath"))));
187  }
188 
189  // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x.
190  // === BEGIN DEPRECATED CODE === //
191  const lt::bdecode_node contentLayoutNode = root.dict_find("qBt-contentLayout");
192  if (contentLayoutNode.type() == lt::bdecode_node::string_t)
193  {
194  const QString contentLayoutStr = fromLTString(contentLayoutNode.string_value());
195  torrentParams.contentLayout = Utils::String::toEnum(contentLayoutStr, TorrentContentLayout::Original);
196  }
197  else
198  {
199  const bool hasRootFolder = root.dict_find_int_value("qBt-hasRootFolder");
200  torrentParams.contentLayout = (hasRootFolder ? TorrentContentLayout::Original : TorrentContentLayout::NoSubfolder);
201  }
202  // === END DEPRECATED CODE === //
203  // === BEGIN REPLACEMENT CODE === //
204  // torrentParams.contentLayout = Utils::String::parse(
205  // fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default);
206  // === END REPLACEMENT CODE === //
207 
208  const lt::string_view ratioLimitString = root.dict_find_string_value("qBt-ratioLimit");
209  if (ratioLimitString.empty())
210  torrentParams.ratioLimit = root.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO * 1000) / 1000.0;
211  else
212  torrentParams.ratioLimit = fromLTString(ratioLimitString).toDouble();
213 
214  const lt::bdecode_node tagsNode = root.dict_find("qBt-tags");
215  if (tagsNode.type() == lt::bdecode_node::list_t)
216  {
217  for (int i = 0; i < tagsNode.list_size(); ++i)
218  {
219  const QString tag = fromLTString(tagsNode.list_string_value_at(i));
220  torrentParams.tags.insert(tag);
221  }
222  }
223 
224  lt::add_torrent_params &p = torrentParams.ltAddTorrentParams;
225 
226  p = lt::read_resume_data(root, ec);
227  p.save_path = Profile::instance()->fromPortablePath(fromLTString(p.save_path)).toStdString();
228 
229  if (p.flags & lt::torrent_flags::stop_when_ready)
230  {
231  // If torrent has "stop_when_ready" flag set then it is actually "stopped"
232  torrentParams.stopped = true;
233  torrentParams.operatingMode = TorrentOperatingMode::AutoManaged;
234  // ...but temporarily "resumed" to perform some service jobs (e.g. checking)
235  p.flags &= ~lt::torrent_flags::paused;
236  p.flags |= lt::torrent_flags::auto_managed;
237  }
238  else
239  {
240  torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed);
241  torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
242  ? TorrentOperatingMode::AutoManaged : TorrentOperatingMode::Forced;
243  }
244 
245  const bool hasMetadata = (p.ti && p.ti->is_valid());
246  if (!hasMetadata && !root.dict_find("info-hash"))
247  return std::nullopt;
248 
249  return torrentParams;
250 }
251 
253 {
254  QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData]()
255  {
256  m_asyncWorker->store(id, resumeData);
257  });
258 }
259 
261 {
262  QMetaObject::invokeMethod(m_asyncWorker, [this, id]()
263  {
264  m_asyncWorker->remove(id);
265  });
266 }
267 
268 void BitTorrent::BencodeResumeDataStorage::storeQueue(const QVector<TorrentID> &queue) const
269 {
270  QMetaObject::invokeMethod(m_asyncWorker, [this, queue]()
271  {
272  m_asyncWorker->storeQueue(queue);
273  });
274 }
275 
276 void BitTorrent::BencodeResumeDataStorage::loadQueue(const QString &queueFilename)
277 {
278  QFile queueFile {queueFilename};
279  if (!queueFile.exists())
280  return;
281 
282  if (queueFile.open(QFile::ReadOnly))
283  {
284  const QRegularExpression hashPattern {QLatin1String("^([A-Fa-f0-9]{40})$")};
285  QByteArray line;
286  int start = 0;
287  while (!(line = queueFile.readLine().trimmed()).isEmpty())
288  {
289  const QRegularExpressionMatch rxMatch = hashPattern.match(line);
290  if (rxMatch.hasMatch())
291  {
292  const auto torrentID = TorrentID::fromString(rxMatch.captured(1));
293  const int pos = m_registeredTorrents.indexOf(torrentID, start);
294  if (pos != -1)
295  {
296  std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
297  ++start;
298  }
299  }
300  }
301  }
302  else
303  {
304  LogMsg(tr("Couldn't load torrents queue from '%1'. Error: %2")
305  .arg(queueFile.fileName(), queueFile.errorString()), Log::WARNING);
306  }
307 }
308 
310  : m_resumeDataDir {resumeDataDir}
311 {
312 }
313 
315 {
316  // We need to adjust native libtorrent resume data
317  lt::add_torrent_params p = resumeData.ltAddTorrentParams;
318  p.save_path = Profile::instance()->toPortablePath(QString::fromStdString(p.save_path)).toStdString();
319  if (resumeData.stopped)
320  {
321  p.flags |= lt::torrent_flags::paused;
322  p.flags &= ~lt::torrent_flags::auto_managed;
323  }
324  else
325  {
326  // Torrent can be actually "running" but temporarily "paused" to perform some
327  // service jobs behind the scenes so we need to restore it as "running"
328  if (resumeData.operatingMode == BitTorrent::TorrentOperatingMode::AutoManaged)
329  {
330  p.flags |= lt::torrent_flags::auto_managed;
331  }
332  else
333  {
334  p.flags &= ~lt::torrent_flags::paused;
335  p.flags &= ~lt::torrent_flags::auto_managed;
336  }
337  }
338 
339  lt::entry data = lt::write_resume_data(p);
340 
341  // metadata is stored in separate .torrent file
342  if (p.ti)
343  {
344  lt::entry::dictionary_type &dataDict = data.dict();
345  lt::entry metadata {lt::entry::dictionary_t};
346  lt::entry::dictionary_type &metadataDict = metadata.dict();
347  metadataDict.insert(dataDict.extract("info"));
348  metadataDict.insert(dataDict.extract("creation date"));
349  metadataDict.insert(dataDict.extract("created by"));
350  metadataDict.insert(dataDict.extract("comment"));
351 
352  const QString torrentFilepath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.torrent").arg(id.toString()));
353  const nonstd::expected<void, QString> result = Utils::IO::saveToFile(torrentFilepath, metadata);
354  if (!result)
355  {
356  LogMsg(tr("Couldn't save torrent metadata to '%1'. Error: %2.")
357  .arg(torrentFilepath, result.error()), Log::CRITICAL);
358  return;
359  }
360  }
361 
362  data["qBt-ratioLimit"] = static_cast<int>(resumeData.ratioLimit * 1000);
363  data["qBt-seedingTimeLimit"] = resumeData.seedingTimeLimit;
364  data["qBt-category"] = resumeData.category.toStdString();
365  data["qBt-tags"] = setToEntryList(resumeData.tags);
366  data["qBt-name"] = resumeData.name.toStdString();
367  data["qBt-seedStatus"] = resumeData.hasSeedStatus;
368  data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString();
369  data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority;
370 
371  if (!resumeData.useAutoTMM)
372  {
373  data["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData.savePath).toStdString();
374  data["qBt-downloadPath"] = Profile::instance()->toPortablePath(resumeData.downloadPath).toStdString();
375  }
376 
377  const QString resumeFilepath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.fastresume").arg(id.toString()));
378  const nonstd::expected<void, QString> result = Utils::IO::saveToFile(resumeFilepath, data);
379  if (!result)
380  {
381  LogMsg(tr("Couldn't save torrent resume data to '%1'. Error: %2.")
382  .arg(resumeFilepath, result.error()), Log::CRITICAL);
383  }
384 }
385 
387 {
388  const QString resumeFilename = QString::fromLatin1("%1.fastresume").arg(id.toString());
389  Utils::Fs::forceRemove(m_resumeDataDir.absoluteFilePath(resumeFilename));
390 
391  const QString torrentFilename = QString::fromLatin1("%1.torrent").arg(id.toString());
392  Utils::Fs::forceRemove(m_resumeDataDir.absoluteFilePath(torrentFilename));
393 }
394 
395 void BitTorrent::BencodeResumeDataStorage::Worker::storeQueue(const QVector<TorrentID> &queue) const
396 {
397  QByteArray data;
398  data.reserve(((BitTorrent::TorrentID::length() * 2) + 1) * queue.size());
399  for (const BitTorrent::TorrentID &torrentID : queue)
400  data += (torrentID.toString().toLatin1() + '\n');
401 
402  const QString filepath = m_resumeDataDir.absoluteFilePath(QLatin1String("queue"));
403  const nonstd::expected<void, QString> result = Utils::IO::saveToFile(filepath, data);
404  if (!result)
405  {
406  LogMsg(tr("Couldn't save data to '%1'. Error: %2")
407  .arg(filepath, result.error()), Log::CRITICAL);
408  }
409 }
void storeQueue(const QVector< TorrentID > &queue) const
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const
QVector< TorrentID > registeredTorrents() const override
void loadQueue(const QString &queueFilename)
std::optional< LoadTorrentParams > load(const TorrentID &id) const override
void storeQueue(const QVector< TorrentID > &queue) const override
std::optional< LoadTorrentParams > loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const
void remove(const TorrentID &id) const override
BencodeResumeDataStorage(const QString &path, QObject *parent=nullptr)
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override
static const int USE_GLOBAL_SEEDING_TIME
Definition: torrent.h:107
static const qreal USE_GLOBAL_RATIO
Definition: torrent.h:104
static TorrentID fromString(const QString &hashString)
Definition: infohash.cpp:76
static constexpr int length()
Definition: digest32.h:53
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
@ WARNING
Definition: logger.h:47
@ CRITICAL
Definition: logger.h:48
QString toUniformPath(const QString &path)
Definition: fs.cpp:69
bool forceRemove(const QString &filePath)
Definition: fs.cpp:173
QString toNativePath(const QString &path)
Definition: fs.cpp:64
nonstd::expected< void, QString > saveToFile(const QString &path, const QByteArray &data)
Definition: io.cpp:69
QString fromEnum(const T &value)
Definition: string.h:67
T toEnum(const QString &serializedValue, const T &defaultValue)
Definition: string.h:77
void setValue(const QString &key, const T &value)
Definition: preferences.cpp:76
QString toString(const lt::socket_type_t socketType)
Definition: session.cpp:183
TorrentContentLayout contentLayout
lt::add_torrent_params ltAddTorrentParams
TorrentOperatingMode operatingMode