Enable customizing the save statistics time interval
[qBittorrent.git] / src / base / bittorrent / bencoderesumedatastorage.cpp
blob43dd414cdbbe734d9e265d6f301144a38e21a68e
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2015-2022 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 "bencoderesumedatastorage.h"
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>
37 #include <QByteArray>
38 #include <QDebug>
39 #include <QFile>
40 #include <QRegularExpression>
41 #include <QThread>
43 #include "base/exceptions.h"
44 #include "base/global.h"
45 #include "base/logger.h"
46 #include "base/preferences.h"
47 #include "base/profile.h"
48 #include "base/tagset.h"
49 #include "base/utils/fs.h"
50 #include "base/utils/io.h"
51 #include "base/utils/sslkey.h"
52 #include "base/utils/string.h"
53 #include "infohash.h"
54 #include "loadtorrentparams.h"
56 namespace BitTorrent
58 class BencodeResumeDataStorage::Worker final : public QObject
60 Q_DISABLE_COPY_MOVE(Worker)
62 public:
63 explicit Worker(const Path &resumeDataDir);
65 void store(const TorrentID &id, const LoadTorrentParams &resumeData) const;
66 void remove(const TorrentID &id) const;
67 void storeQueue(const QList<TorrentID> &queue) const;
69 private:
70 const Path m_resumeDataDir;
74 namespace
76 const char KEY_SSL_CERTIFICATE[] = "qBt-sslCertificate";
77 const char KEY_SSL_PRIVATE_KEY[] = "qBt-sslPrivateKey";
78 const char KEY_SSL_DH_PARAMS[] = "qBt-sslDhParams";
80 template <typename LTStr>
81 QString fromLTString(const LTStr &str)
83 return QString::fromUtf8(str.data(), static_cast<qsizetype>(str.size()));
86 template <typename LTStr>
87 QByteArray toByteArray(const LTStr &str)
89 return {str.data(), static_cast<qsizetype>(str.size())};
92 using ListType = lt::entry::list_type;
94 ListType setToEntryList(const TagSet &input)
96 ListType entryList;
97 entryList.reserve(input.size());
98 for (const Tag &setValue : input)
99 entryList.emplace_back(setValue.toString().toStdString());
100 return entryList;
104 BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path, QObject *parent)
105 : ResumeDataStorage(path, parent)
106 , m_ioThread {new QThread}
107 , m_asyncWorker {new Worker(path)}
109 Q_ASSERT(path.isAbsolute());
111 if (!path.exists() && !Utils::Fs::mkpath(path))
113 throw RuntimeError(tr("Cannot create torrent resume folder: \"%1\"")
114 .arg(path.toString()));
117 const QRegularExpression filenamePattern {u"^([A-Fa-f0-9]{40})\\.fastresume$"_s};
118 const QStringList filenames = QDir(path.data()).entryList({u"*.fastresume"_s}, QDir::Files);
120 m_registeredTorrents.reserve(filenames.size());
121 for (const QString &filename : filenames)
123 const QRegularExpressionMatch rxMatch = filenamePattern.match(filename);
124 if (rxMatch.hasMatch())
125 m_registeredTorrents.append(TorrentID::fromString(rxMatch.captured(1)));
128 loadQueue(path / Path(u"queue"_s));
130 qDebug() << "Registered torrents count: " << m_registeredTorrents.size();
132 m_asyncWorker->moveToThread(m_ioThread.get());
133 connect(m_ioThread.get(), &QThread::finished, m_asyncWorker, &QObject::deleteLater);
134 m_ioThread->start();
137 QList<BitTorrent::TorrentID> BitTorrent::BencodeResumeDataStorage::registeredTorrents() const
139 return m_registeredTorrents;
142 BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const
144 const QString idString = id.toString();
145 const Path fastresumePath = path() / Path(idString + u".fastresume");
146 const Path torrentFilePath = path() / Path(idString + u".torrent");
147 const qint64 torrentSizeLimit = Preferences::instance()->getTorrentFileSizeLimit();
149 const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, torrentSizeLimit);
150 if (!resumeDataReadResult)
151 return nonstd::make_unexpected(resumeDataReadResult.error().message);
153 const auto metadataReadResult = Utils::IO::readFile(torrentFilePath, torrentSizeLimit);
154 if (!metadataReadResult)
156 if (metadataReadResult.error().status != Utils::IO::ReadError::NotExist)
157 return nonstd::make_unexpected(metadataReadResult.error().message);
160 const QByteArray data = resumeDataReadResult.value();
161 const QByteArray metadata = metadataReadResult.value_or(QByteArray());
162 return loadTorrentResumeData(data, metadata);
165 void BitTorrent::BencodeResumeDataStorage::doLoadAll() const
167 qDebug() << "Loading torrents count: " << m_registeredTorrents.size();
169 emit const_cast<BencodeResumeDataStorage *>(this)->loadStarted(m_registeredTorrents);
171 for (const TorrentID &torrentID : asConst(m_registeredTorrents))
172 onResumeDataLoaded(torrentID, load(torrentID));
174 emit const_cast<BencodeResumeDataStorage *>(this)->loadFinished();
177 void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
179 const int lineMaxLength = 48;
181 QFile queueFile {queueFilename.data()};
182 if (!queueFile.exists())
183 return;
185 if (!queueFile.open(QFile::ReadOnly))
187 LogMsg(tr("Couldn't load torrents queue: %1").arg(queueFile.errorString()), Log::WARNING);
188 return;
191 const QRegularExpression hashPattern {u"^([A-Fa-f0-9]{40})$"_s};
192 int start = 0;
193 while (true)
195 const auto line = QString::fromLatin1(queueFile.readLine(lineMaxLength).trimmed());
196 if (line.isEmpty())
197 break;
199 const QRegularExpressionMatch rxMatch = hashPattern.match(line);
200 if (rxMatch.hasMatch())
202 const auto torrentID = BitTorrent::TorrentID::fromString(rxMatch.captured(1));
203 const int pos = m_registeredTorrents.indexOf(torrentID, start);
204 if (pos != -1)
206 std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
207 ++start;
213 BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const
215 const auto *pref = Preferences::instance();
217 lt::error_code ec;
218 const lt::bdecode_node resumeDataRoot = lt::bdecode(data, ec
219 , nullptr, pref->getBdecodeDepthLimit(), pref->getBdecodeTokenLimit());
220 if (ec)
221 return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
223 if (resumeDataRoot.type() != lt::bdecode_node::dict_t)
224 return nonstd::make_unexpected(tr("Cannot parse resume data: invalid format"));
226 LoadTorrentParams torrentParams;
227 torrentParams.category = fromLTString(resumeDataRoot.dict_find_string_value("qBt-category"));
228 torrentParams.name = fromLTString(resumeDataRoot.dict_find_string_value("qBt-name"));
229 torrentParams.hasFinishedStatus = resumeDataRoot.dict_find_int_value("qBt-seedStatus");
230 torrentParams.firstLastPiecePriority = resumeDataRoot.dict_find_int_value("qBt-firstLastPiecePriority");
231 torrentParams.seedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME);
232 torrentParams.inactiveSeedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-inactiveSeedingTimeLimit", Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME);
233 torrentParams.shareLimitAction = Utils::String::toEnum(
234 fromLTString(resumeDataRoot.dict_find_string_value("qBt-shareLimitAction")), ShareLimitAction::Default);
236 torrentParams.savePath = Profile::instance()->fromPortablePath(
237 Path(fromLTString(resumeDataRoot.dict_find_string_value("qBt-savePath"))));
238 torrentParams.useAutoTMM = torrentParams.savePath.isEmpty();
239 if (!torrentParams.useAutoTMM)
241 torrentParams.downloadPath = Profile::instance()->fromPortablePath(
242 Path(fromLTString(resumeDataRoot.dict_find_string_value("qBt-downloadPath"))));
245 // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x.
246 // === BEGIN DEPRECATED CODE === //
247 const lt::bdecode_node contentLayoutNode = resumeDataRoot.dict_find("qBt-contentLayout");
248 if (contentLayoutNode.type() == lt::bdecode_node::string_t)
250 const QString contentLayoutStr = fromLTString(contentLayoutNode.string_value());
251 torrentParams.contentLayout = Utils::String::toEnum(contentLayoutStr, TorrentContentLayout::Original);
253 else
255 const bool hasRootFolder = resumeDataRoot.dict_find_int_value("qBt-hasRootFolder");
256 torrentParams.contentLayout = (hasRootFolder ? TorrentContentLayout::Original : TorrentContentLayout::NoSubfolder);
258 // === END DEPRECATED CODE === //
259 // === BEGIN REPLACEMENT CODE === //
260 // torrentParams.contentLayout = Utils::String::parse(
261 // fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default);
262 // === END REPLACEMENT CODE === //
264 torrentParams.stopCondition = Utils::String::toEnum(
265 fromLTString(resumeDataRoot.dict_find_string_value("qBt-stopCondition")), Torrent::StopCondition::None);
266 torrentParams.sslParameters =
268 .certificate = QSslCertificate(toByteArray(resumeDataRoot.dict_find_string_value(KEY_SSL_CERTIFICATE))),
269 .privateKey = Utils::SSLKey::load(toByteArray(resumeDataRoot.dict_find_string_value(KEY_SSL_PRIVATE_KEY))),
270 .dhParams = toByteArray(resumeDataRoot.dict_find_string_value(KEY_SSL_DH_PARAMS))
273 const lt::string_view ratioLimitString = resumeDataRoot.dict_find_string_value("qBt-ratioLimit");
274 if (ratioLimitString.empty())
275 torrentParams.ratioLimit = resumeDataRoot.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO * 1000) / 1000.0;
276 else
277 torrentParams.ratioLimit = fromLTString(ratioLimitString).toDouble();
279 const lt::bdecode_node tagsNode = resumeDataRoot.dict_find("qBt-tags");
280 if (tagsNode.type() == lt::bdecode_node::list_t)
282 for (int i = 0; i < tagsNode.list_size(); ++i)
284 const Tag tag {fromLTString(tagsNode.list_string_value_at(i))};
285 torrentParams.tags.insert(tag);
289 lt::add_torrent_params &p = torrentParams.ltAddTorrentParams;
291 p = lt::read_resume_data(resumeDataRoot, ec);
293 if (!metadata.isEmpty())
295 const auto *pref = Preferences::instance();
296 const lt::bdecode_node torentInfoRoot = lt::bdecode(metadata, ec
297 , nullptr, pref->getBdecodeDepthLimit(), pref->getBdecodeTokenLimit());
298 if (ec)
299 return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
301 if (torentInfoRoot.type() != lt::bdecode_node::dict_t)
302 return nonstd::make_unexpected(tr("Cannot parse torrent info: invalid format"));
304 const auto torrentInfo = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
305 if (ec)
306 return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
308 p.ti = torrentInfo;
310 #ifdef QBT_USES_LIBTORRENT2
311 if (((p.info_hashes.has_v1() && (p.info_hashes.v1 != p.ti->info_hashes().v1))
312 || (p.info_hashes.has_v2() && (p.info_hashes.v2 != p.ti->info_hashes().v2))))
313 #else
314 if (!p.info_hash.is_all_zeros() && (p.info_hash != p.ti->info_hash()))
315 #endif
317 return nonstd::make_unexpected(tr("Mismatching info-hash detected in resume data"));
321 p.save_path = Profile::instance()->fromPortablePath(
322 Path(fromLTString(p.save_path))).toString().toStdString();
324 torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed);
325 torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
326 ? TorrentOperatingMode::AutoManaged : TorrentOperatingMode::Forced;
328 if (p.flags & lt::torrent_flags::stop_when_ready)
330 p.flags &= ~lt::torrent_flags::stop_when_ready;
331 torrentParams.stopCondition = Torrent::StopCondition::FilesChecked;
334 const bool hasMetadata = (p.ti && p.ti->is_valid());
335 if (!hasMetadata && !resumeDataRoot.dict_find("info-hash"))
336 return nonstd::make_unexpected(tr("Resume data is invalid: neither metadata nor info-hash was found"));
338 return torrentParams;
341 void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
343 QMetaObject::invokeMethod(m_asyncWorker, [this, id, resumeData]()
345 m_asyncWorker->store(id, resumeData);
349 void BitTorrent::BencodeResumeDataStorage::remove(const TorrentID &id) const
351 QMetaObject::invokeMethod(m_asyncWorker, [this, id]()
353 m_asyncWorker->remove(id);
357 void BitTorrent::BencodeResumeDataStorage::storeQueue(const QList<TorrentID> &queue) const
359 QMetaObject::invokeMethod(m_asyncWorker, [this, queue]()
361 m_asyncWorker->storeQueue(queue);
365 BitTorrent::BencodeResumeDataStorage::Worker::Worker(const Path &resumeDataDir)
366 : m_resumeDataDir {resumeDataDir}
370 void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
372 // We need to adjust native libtorrent resume data
373 lt::add_torrent_params p = resumeData.ltAddTorrentParams;
374 p.save_path = Profile::instance()->toPortablePath(Path(p.save_path))
375 .toString().toStdString();
376 if (resumeData.stopped)
378 p.flags |= lt::torrent_flags::paused;
379 p.flags &= ~lt::torrent_flags::auto_managed;
381 else
383 // Torrent can be actually "running" but temporarily "paused" to perform some
384 // service jobs behind the scenes so we need to restore it as "running"
385 if (resumeData.operatingMode == BitTorrent::TorrentOperatingMode::AutoManaged)
387 p.flags |= lt::torrent_flags::auto_managed;
389 else
391 p.flags &= ~lt::torrent_flags::paused;
392 p.flags &= ~lt::torrent_flags::auto_managed;
396 lt::entry data = lt::write_resume_data(p);
398 // metadata is stored in separate .torrent file
399 if (p.ti)
401 lt::entry::dictionary_type &dataDict = data.dict();
402 lt::entry metadata {lt::entry::dictionary_t};
403 lt::entry::dictionary_type &metadataDict = metadata.dict();
404 metadataDict.insert(dataDict.extract("info"));
405 metadataDict.insert(dataDict.extract("creation date"));
406 metadataDict.insert(dataDict.extract("created by"));
407 metadataDict.insert(dataDict.extract("comment"));
409 const Path torrentFilepath = m_resumeDataDir / Path(u"%1.torrent"_s.arg(id.toString()));
410 const nonstd::expected<void, QString> result = Utils::IO::saveToFile(torrentFilepath, metadata);
411 if (!result)
413 LogMsg(tr("Couldn't save torrent metadata to '%1'. Error: %2.")
414 .arg(torrentFilepath.toString(), result.error()), Log::CRITICAL);
415 return;
419 data["qBt-ratioLimit"] = static_cast<int>(resumeData.ratioLimit * 1000);
420 data["qBt-seedingTimeLimit"] = resumeData.seedingTimeLimit;
421 data["qBt-inactiveSeedingTimeLimit"] = resumeData.inactiveSeedingTimeLimit;
422 data["qBt-shareLimitAction"] = Utils::String::fromEnum(resumeData.shareLimitAction).toStdString();
424 data["qBt-category"] = resumeData.category.toStdString();
425 data["qBt-tags"] = setToEntryList(resumeData.tags);
426 data["qBt-name"] = resumeData.name.toStdString();
427 data["qBt-seedStatus"] = resumeData.hasFinishedStatus;
428 data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString();
429 data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority;
430 data["qBt-stopCondition"] = Utils::String::fromEnum(resumeData.stopCondition).toStdString();
432 if (!resumeData.sslParameters.certificate.isNull())
433 data[KEY_SSL_CERTIFICATE] = resumeData.sslParameters.certificate.toPem().toStdString();
434 if (!resumeData.sslParameters.privateKey.isNull())
435 data[KEY_SSL_PRIVATE_KEY] = resumeData.sslParameters.privateKey.toPem().toStdString();
436 if (!resumeData.sslParameters.dhParams.isEmpty())
437 data[KEY_SSL_DH_PARAMS] = resumeData.sslParameters.dhParams.toStdString();
439 if (!resumeData.useAutoTMM)
441 data["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData.savePath).data().toStdString();
442 data["qBt-downloadPath"] = Profile::instance()->toPortablePath(resumeData.downloadPath).data().toStdString();
445 const Path resumeFilepath = m_resumeDataDir / Path(u"%1.fastresume"_s.arg(id.toString()));
446 const nonstd::expected<void, QString> result = Utils::IO::saveToFile(resumeFilepath, data);
447 if (!result)
449 LogMsg(tr("Couldn't save torrent resume data to '%1'. Error: %2.")
450 .arg(resumeFilepath.toString(), result.error()), Log::CRITICAL);
454 void BitTorrent::BencodeResumeDataStorage::Worker::remove(const TorrentID &id) const
456 const Path resumeFilename {u"%1.fastresume"_s.arg(id.toString())};
457 Utils::Fs::removeFile(m_resumeDataDir / resumeFilename);
459 const Path torrentFilename {u"%1.torrent"_s.arg(id.toString())};
460 Utils::Fs::removeFile(m_resumeDataDir / torrentFilename);
463 void BitTorrent::BencodeResumeDataStorage::Worker::storeQueue(const QList<TorrentID> &queue) const
465 QByteArray data;
466 data.reserve(((BitTorrent::TorrentID::length() * 2) + 1) * queue.size());
467 for (const BitTorrent::TorrentID &torrentID : queue)
468 data += (torrentID.toString().toLatin1() + '\n');
470 const Path filepath = m_resumeDataDir / Path(u"queue"_s);
471 const nonstd::expected<void, QString> result = Utils::IO::saveToFile(filepath, data);
472 if (!result)
474 LogMsg(tr("Couldn't save data to '%1'. Error: %2")
475 .arg(filepath.toString(), result.error()), Log::CRITICAL);