diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 482e3c31e..a56c24984 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(qbt_base STATIC bittorrent/filesearcher.h bittorrent/filterparserthread.h bittorrent/infohash.h + bittorrent/loadtorrentparams.h bittorrent/ltqhash.h bittorrent/ltunderlyingtype.h bittorrent/magneturi.h @@ -20,7 +21,7 @@ add_library(qbt_base STATIC bittorrent/peeraddress.h bittorrent/peerinfo.h bittorrent/portforwarderimpl.h - bittorrent/resumedatasavingmanager.h + bittorrent/resumedatastorage.h bittorrent/session.h bittorrent/sessionstatus.h bittorrent/speedmonitor.h @@ -104,7 +105,7 @@ add_library(qbt_base STATIC bittorrent/peeraddress.cpp bittorrent/peerinfo.cpp bittorrent/portforwarderimpl.cpp - bittorrent/resumedatasavingmanager.cpp + bittorrent/resumedatastorage.cpp bittorrent/session.cpp bittorrent/speedmonitor.cpp bittorrent/statistics.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 95cf69246..48359c637 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -11,6 +11,7 @@ HEADERS += \ $$PWD/bittorrent/filesearcher.h \ $$PWD/bittorrent/filterparserthread.h \ $$PWD/bittorrent/infohash.h \ + $$PWD/bittorrent/loadtorrentparams.h \ $$PWD/bittorrent/ltqhash.h \ $$PWD/bittorrent/ltunderlyingtype.h \ $$PWD/bittorrent/magneturi.h \ @@ -19,7 +20,7 @@ HEADERS += \ $$PWD/bittorrent/peeraddress.h \ $$PWD/bittorrent/peerinfo.h \ $$PWD/bittorrent/portforwarderimpl.h \ - $$PWD/bittorrent/resumedatasavingmanager.h \ + $$PWD/bittorrent/resumedatastorage.h \ $$PWD/bittorrent/session.h \ $$PWD/bittorrent/sessionstatus.h \ $$PWD/bittorrent/speedmonitor.h \ @@ -104,7 +105,7 @@ SOURCES += \ $$PWD/bittorrent/peeraddress.cpp \ $$PWD/bittorrent/peerinfo.cpp \ $$PWD/bittorrent/portforwarderimpl.cpp \ - $$PWD/bittorrent/resumedatasavingmanager.cpp \ + $$PWD/bittorrent/resumedatastorage.cpp \ $$PWD/bittorrent/session.cpp \ $$PWD/bittorrent/speedmonitor.cpp \ $$PWD/bittorrent/statistics.cpp \ diff --git a/src/base/bittorrent/resumedatasavingmanager.h b/src/base/bittorrent/loadtorrentparams.h similarity index 63% rename from src/base/bittorrent/resumedatasavingmanager.h rename to src/base/bittorrent/loadtorrentparams.h index 47a9ca562..4192303c8 100644 --- a/src/base/bittorrent/resumedatasavingmanager.h +++ b/src/base/bittorrent/loadtorrentparams.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015, 2018 Vladimir Golovnev + * Copyright (C) 2021 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,28 +28,33 @@ #pragma once -#include +#include -#include +#include +#include -#include -#include +#include "torrent.h" +#include "torrentcontentlayout.h" -class QByteArray; - -class ResumeDataSavingManager : public QObject +namespace BitTorrent { - Q_OBJECT - Q_DISABLE_COPY(ResumeDataSavingManager) - -public: - explicit ResumeDataSavingManager(const QString &resumeFolderPath); - -public slots: - void save(const QString &filename, const QByteArray &data) const; - void save(const QString &filename, const std::shared_ptr &data) const; - void remove(const QString &filename) const; - -private: - const QDir m_resumeDataDir; -}; + struct LoadTorrentParams + { + lt::add_torrent_params ltAddTorrentParams {}; + + QString name; + QString category; + QSet tags; + QString savePath; + TorrentContentLayout contentLayout = TorrentContentLayout::Original; + bool firstLastPiecePriority = false; + bool hasSeedStatus = false; + bool forced = false; + bool paused = false; + + qreal ratioLimit = Torrent::USE_GLOBAL_RATIO; + int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME; + + bool restored = false; // is existing torrent job? + }; +} diff --git a/src/base/bittorrent/resumedatasavingmanager.cpp b/src/base/bittorrent/resumedatasavingmanager.cpp deleted file mode 100644 index 461900783..000000000 --- a/src/base/bittorrent/resumedatasavingmanager.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015, 2018 Vladimir Golovnev - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -#include "resumedatasavingmanager.h" - -#include -#include - -#include -#include - -#include "base/logger.h" -#include "base/utils/fs.h" -#include "base/utils/io.h" - -ResumeDataSavingManager::ResumeDataSavingManager(const QString &resumeFolderPath) - : m_resumeDataDir(resumeFolderPath) -{ -} - -void ResumeDataSavingManager::save(const QString &filename, const QByteArray &data) const -{ - const QString filepath = m_resumeDataDir.absoluteFilePath(filename); - - QSaveFile file {filepath}; - if (!file.open(QIODevice::WriteOnly) || (file.write(data) != data.size()) || !file.commit()) - { - LogMsg(tr("Couldn't save data to '%1'. Error: %2") - .arg(filepath, file.errorString()), Log::CRITICAL); - } -} - -void ResumeDataSavingManager::save(const QString &filename, const std::shared_ptr &data) const -{ - const QString filepath = m_resumeDataDir.absoluteFilePath(filename); - - QSaveFile file {filepath}; - if (!file.open(QIODevice::WriteOnly)) - { - LogMsg(tr("Couldn't save data to '%1'. Error: %2") - .arg(filepath, file.errorString()), Log::CRITICAL); - return; - } - - lt::bencode(Utils::IO::FileDeviceOutputIterator {file}, *data); - if ((file.error() != QFileDevice::NoError) || !file.commit()) - { - LogMsg(tr("Couldn't save data to '%1'. Error: %2") - .arg(filepath, file.errorString()), Log::CRITICAL); - } -} - -void ResumeDataSavingManager::remove(const QString &filename) const -{ - const QString filepath = m_resumeDataDir.absoluteFilePath(filename); - - Utils::Fs::forceRemove(filepath); -} diff --git a/src/base/bittorrent/resumedatastorage.cpp b/src/base/bittorrent/resumedatastorage.cpp new file mode 100644 index 000000000..7984a2971 --- /dev/null +++ b/src/base/bittorrent/resumedatastorage.cpp @@ -0,0 +1,306 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015, 2018 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "resumedatastorage.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "base/algorithm.h" +#include "base/global.h" +#include "base/logger.h" +#include "base/profile.h" +#include "base/utils/fs.h" +#include "base/utils/io.h" +#include "base/utils/string.h" +#include "torrentinfo.h" + +namespace +{ + template + QString fromLTString(const LTStr &str) + { + return QString::fromUtf8(str.data(), static_cast(str.size())); + } + + using ListType = lt::entry::list_type; + + ListType setToEntryList(const QSet &input) + { + ListType entryList; + entryList.reserve(input.size()); + for (const QString &setValue : input) + entryList.emplace_back(setValue.toStdString()); + return entryList; + } +} + +BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const QString &resumeFolderPath) + : m_resumeDataDir {resumeFolderPath} +{ + const QRegularExpression filenamePattern {QLatin1String("^([A-Fa-f0-9]{40})\\.fastresume$")}; + const QStringList filenames = m_resumeDataDir.entryList(QStringList(QLatin1String("*.fastresume")), QDir::Files, QDir::Unsorted); + + m_registeredTorrents.reserve(filenames.size()); + for (const QString &filename : filenames) + { + const QRegularExpressionMatch rxMatch = filenamePattern.match(filename); + if (rxMatch.hasMatch()) + m_registeredTorrents.append(TorrentID::fromString(rxMatch.captured(1))); + } + + QFile queueFile {m_resumeDataDir.absoluteFilePath(QLatin1String("queue"))}; + if (queueFile.open(QFile::ReadOnly)) + { + const QRegularExpression hashPattern {QLatin1String("^([A-Fa-f0-9]{40})$")}; + QByteArray line; + int start = 0; + while (!(line = queueFile.readLine().trimmed()).isEmpty()) + { + const QRegularExpressionMatch rxMatch = hashPattern.match(line); + if (rxMatch.hasMatch()) + { + const auto torrentID = TorrentID::fromString(rxMatch.captured(1)); + const int pos = m_registeredTorrents.indexOf(torrentID, start); + if (pos != -1) + { + std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]); + ++start; + } + } + } + } + else + { + LogMsg(tr("Couldn't load torrents queue from '%1'. Error: %2") + .arg(queueFile.fileName(), queueFile.errorString()), Log::WARNING); + } + + qDebug("Registered torrents count: %d", m_registeredTorrents.size()); +} + +QVector BitTorrent::BencodeResumeDataStorage::registeredTorrents() const +{ + return m_registeredTorrents; +} + +std::optional BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const +{ + const QString idString = id.toString(); + const QString fastresumePath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.fastresume").arg(idString)); + const QString torrentFilePath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.torrent").arg(idString)); + + const auto readFile = [](const QString &path, QByteArray &buf) -> bool + { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) + { + LogMsg(tr("Cannot read file %1: %2").arg(path, file.errorString()), Log::WARNING); + return false; + } + + buf = file.readAll(); + return true; + }; + + QByteArray data; + if (!readFile(fastresumePath, data)) + return std::nullopt; + + const TorrentInfo metadata = TorrentInfo::loadFromFile(torrentFilePath); + + return loadTorrentResumeData(data, metadata); +} + +void BitTorrent::BencodeResumeDataStorage::storeQueue(const QVector &queue) const +{ + QByteArray data; + data.reserve(((TorrentID::length() * 2) + 1) * queue.size()); + for (const TorrentID &torrentID : queue) + data += (torrentID.toString().toLatin1() + '\n'); + + const QString filepath = m_resumeDataDir.absoluteFilePath(QLatin1String("queue")); + QSaveFile file {filepath}; + if (!file.open(QIODevice::WriteOnly) || (file.write(data) != data.size()) || !file.commit()) + { + LogMsg(tr("Couldn't save data to '%1'. Error: %2") + .arg(filepath, file.errorString()), Log::CRITICAL); + } +} + +std::optional BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData( + const QByteArray &data, const TorrentInfo &metadata) const +{ + lt::error_code ec; + const lt::bdecode_node root = lt::bdecode(data, ec); + if (ec || (root.type() != lt::bdecode_node::dict_t)) return std::nullopt; + + LoadTorrentParams torrentParams; + torrentParams.restored = true; + torrentParams.category = fromLTString(root.dict_find_string_value("qBt-category")); + torrentParams.name = fromLTString(root.dict_find_string_value("qBt-name")); + torrentParams.savePath = Profile::instance()->fromPortablePath( + Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-savePath")))); + torrentParams.hasSeedStatus = root.dict_find_int_value("qBt-seedStatus"); + torrentParams.firstLastPiecePriority = root.dict_find_int_value("qBt-firstLastPiecePriority"); + torrentParams.seedingTimeLimit = root.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME); + + // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x. + // === BEGIN DEPRECATED CODE === // + const lt::bdecode_node contentLayoutNode = root.dict_find("qBt-contentLayout"); + if (contentLayoutNode.type() == lt::bdecode_node::string_t) + { + const QString contentLayoutStr = fromLTString(contentLayoutNode.string_value()); + torrentParams.contentLayout = Utils::String::toEnum(contentLayoutStr, TorrentContentLayout::Original); + } + else + { + const bool hasRootFolder = root.dict_find_int_value("qBt-hasRootFolder"); + torrentParams.contentLayout = (hasRootFolder ? TorrentContentLayout::Original : TorrentContentLayout::NoSubfolder); + } + // === END DEPRECATED CODE === // + // === BEGIN REPLACEMENT CODE === // + // torrentParams.contentLayout = Utils::String::parse( + // fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default); + // === END REPLACEMENT CODE === // + + const lt::string_view ratioLimitString = root.dict_find_string_value("qBt-ratioLimit"); + if (ratioLimitString.empty()) + torrentParams.ratioLimit = root.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO * 1000) / 1000.0; + else + torrentParams.ratioLimit = fromLTString(ratioLimitString).toDouble(); + + const lt::bdecode_node tagsNode = root.dict_find("qBt-tags"); + if (tagsNode.type() == lt::bdecode_node::list_t) + { + for (int i = 0; i < tagsNode.list_size(); ++i) + { + const QString tag = fromLTString(tagsNode.list_string_value_at(i)); + torrentParams.tags.insert(tag); + } + } + + lt::add_torrent_params &p = torrentParams.ltAddTorrentParams; + + p = lt::read_resume_data(root, ec); + p.save_path = Profile::instance()->fromPortablePath(fromLTString(p.save_path)).toStdString(); + if (metadata.isValid()) + p.ti = metadata.nativeInfo(); + + if (p.flags & lt::torrent_flags::stop_when_ready) + { + // If torrent has "stop_when_ready" flag set then it is actually "stopped" + torrentParams.paused = true; + torrentParams.forced = false; + // ...but temporarily "resumed" to perform some service jobs (e.g. checking) + p.flags &= ~lt::torrent_flags::paused; + p.flags |= lt::torrent_flags::auto_managed; + } + else + { + torrentParams.paused = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed); + torrentParams.forced = !(p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed); + } + + const bool hasMetadata = (p.ti && p.ti->is_valid()); + if (!hasMetadata && !root.dict_find("info-hash")) + return std::nullopt; + + return torrentParams; +} + +void BitTorrent::BencodeResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const +{ + // We need to adjust native libtorrent resume data + lt::add_torrent_params p = resumeData.ltAddTorrentParams; + p.save_path = Profile::instance()->toPortablePath(QString::fromStdString(p.save_path)).toStdString(); + if (resumeData.paused) + { + p.flags |= lt::torrent_flags::paused; + p.flags &= ~lt::torrent_flags::auto_managed; + } + else + { + // Torrent can be actually "running" but temporarily "paused" to perform some + // service jobs behind the scenes so we need to restore it as "running" + if (!resumeData.forced) + { + p.flags |= lt::torrent_flags::auto_managed; + } + else + { + p.flags &= ~lt::torrent_flags::paused; + p.flags &= ~lt::torrent_flags::auto_managed; + } + } + + lt::entry data = lt::write_resume_data(p); + + data["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData.savePath).toStdString(); + data["qBt-ratioLimit"] = static_cast(resumeData.ratioLimit * 1000); + data["qBt-seedingTimeLimit"] = resumeData.seedingTimeLimit; + data["qBt-category"] = resumeData.category.toStdString(); + data["qBt-tags"] = setToEntryList(resumeData.tags); + data["qBt-name"] = resumeData.name.toStdString(); + data["qBt-seedStatus"] = resumeData.hasSeedStatus; + data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString(); + data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority; + + const QString filepath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.fastresume").arg(id.toString())); + + QSaveFile file {filepath}; + if (!file.open(QIODevice::WriteOnly)) + { + LogMsg(tr("Couldn't save data to '%1'. Error: %2") + .arg(filepath, file.errorString()), Log::CRITICAL); + return; + } + + lt::bencode(Utils::IO::FileDeviceOutputIterator {file}, data); + if ((file.error() != QFileDevice::NoError) || !file.commit()) + { + LogMsg(tr("Couldn't save data to '%1'. Error: %2") + .arg(filepath, file.errorString()), Log::CRITICAL); + } +} + +void BitTorrent::BencodeResumeDataStorage::remove(const TorrentID &id) const +{ + const QString resumeFilename = QString::fromLatin1("%1.fastresume").arg(id.toString()); + Utils::Fs::forceRemove(m_resumeDataDir.absoluteFilePath(resumeFilename)); + + const QString torrentFilename = QString::fromLatin1("%1.torrent").arg(id.toString()); + Utils::Fs::forceRemove(m_resumeDataDir.absoluteFilePath(torrentFilename)); +} diff --git a/src/base/bittorrent/resumedatastorage.h b/src/base/bittorrent/resumedatastorage.h new file mode 100644 index 000000000..bc7e2fcc8 --- /dev/null +++ b/src/base/bittorrent/resumedatastorage.h @@ -0,0 +1,81 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015, 2018 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include + +#include +#include +#include + +#include "infohash.h" +#include "loadtorrentparams.h" + +class QByteArray; + +namespace BitTorrent +{ + class ResumeDataStorage : public QObject + { + Q_OBJECT + Q_DISABLE_COPY(ResumeDataStorage) + + public: + using QObject::QObject; + + virtual QVector registeredTorrents() const = 0; + virtual std::optional load(const TorrentID &id) const = 0; + virtual void store(const TorrentID &id, const LoadTorrentParams &resumeData) const = 0; + virtual void remove(const TorrentID &id) const = 0; + virtual void storeQueue(const QVector &queue) const = 0; + }; + + class BencodeResumeDataStorage final : public ResumeDataStorage + { + Q_OBJECT + Q_DISABLE_COPY(BencodeResumeDataStorage) + + public: + explicit BencodeResumeDataStorage(const QString &resumeFolderPath); + + QVector registeredTorrents() const override; + std::optional load(const TorrentID &id) const override; + void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override; + void remove(const TorrentID &id) const override; + void storeQueue(const QVector &queue) const override; + + private: + std::optional loadTorrentResumeData(const QByteArray &data, const TorrentInfo &metadata) const; + + const QDir m_resumeDataDir; + QVector m_registeredTorrents; + }; +} diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 99439a360..77b76dd9f 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -42,21 +42,16 @@ #endif #include -#include -#include -#include #include #include #include #include #include #include -#include #include #include #include #include -#include #include #include @@ -96,25 +91,19 @@ #include "magneturi.h" #include "nativesessionextension.h" #include "portforwarderimpl.h" -#include "resumedatasavingmanager.h" +#include "resumedatastorage.h" #include "statistics.h" #include "torrentimpl.h" #include "tracker.h" #include "trackerentry.h" -static const char PEER_ID[] = "qB"; -static const char RESUME_FOLDER[] = "BT_backup"; -static const char USER_AGENT[] = "qBittorrent/" QBT_VERSION_2; - using namespace BitTorrent; namespace { - template - QString fromLTString(const LTStr &str) - { - return QString::fromUtf8(str.data(), static_cast(str.size())); - } + const char PEER_ID[] = "qB"; + const char RESUME_FOLDER[] = "BT_backup"; + const char USER_AGENT[] = "qBittorrent/" QBT_VERSION_2; void torrentQueuePositionUp(const lt::torrent_handle &handle) { @@ -305,17 +294,6 @@ namespace }; } - using ListType = lt::entry::list_type; - - ListType setToEntryList(const QSet &input) - { - ListType entryList; - entryList.reserve(input.size()); - for (const QString &setValue : input) - entryList.emplace_back(setValue.toStdString()); - return entryList; - } - #ifdef Q_OS_WIN QString convertIfaceNameToGuid(const QString &name) { @@ -471,7 +449,7 @@ Session::Session(QObject *parent) if (port() < 0) m_port = Utils::Random::rand(1024, 65535); - initResumeFolder(); + initResumeDataStorage(); m_recentErroredTorrentsTimer->setSingleShot(true); m_recentErroredTorrentsTimer->setInterval(1000); @@ -513,10 +491,6 @@ Session::Session(QObject *parent) connect(m_networkManager, &QNetworkConfigurationManager::configurationRemoved, this, &Session::networkConfigurationChange); connect(m_networkManager, &QNetworkConfigurationManager::configurationChanged, this, &Session::networkConfigurationChange); - m_resumeDataSavingManager = new ResumeDataSavingManager {m_resumeFolderPath}; - m_resumeDataSavingManager->moveToThread(m_ioThread); - connect(m_ioThread, &QThread::finished, m_resumeDataSavingManager, &QObject::deleteLater); - m_fileSearcher = new FileSearcher; m_fileSearcher->moveToThread(m_ioThread); connect(m_ioThread, &QThread::finished, m_fileSearcher, &QObject::deleteLater); @@ -1865,12 +1839,9 @@ bool Session::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption } // Remove it from torrent resume directory - const QString resumedataFile = QString::fromLatin1("%1.fastresume").arg(torrent->id().toString()); - const QString metadataFile = QString::fromLatin1("%1.torrent").arg(torrent->id().toString()); - QMetaObject::invokeMethod(m_resumeDataSavingManager, [this, resumedataFile, metadataFile]() + QMetaObject::invokeMethod(m_resumeDataStorage, [this, torrentID = torrent->id()]() { - m_resumeDataSavingManager->remove(resumedataFile); - m_resumeDataSavingManager->remove(metadataFile); + m_resumeDataStorage->remove(torrentID); }); delete torrent; @@ -2393,31 +2364,27 @@ void Session::saveResumeData() void Session::saveTorrentsQueue() const { - // store hash in textual representation - QMap queue; // Use QMap since it should be ordered by key + QVector queue; for (const TorrentImpl *torrent : asConst(m_torrents)) { // We require actual (non-cached) queue position here! const int queuePos = static_cast>(torrent->nativeHandle().queue_position()); if (queuePos >= 0) - queue[queuePos] = torrent->id().toString(); + { + if (queuePos >= queue.size()) + queue.resize(queuePos + 1); + queue[queuePos] = torrent->id(); + } } - QByteArray data; - data.reserve(((TorrentID::length() * 2) + 1) * queue.size()); - for (const QString &torrentID : asConst(queue)) - data += (torrentID.toLatin1() + '\n'); - - const QString filename = QLatin1String {"queue"}; - QMetaObject::invokeMethod(m_resumeDataSavingManager - , [this, data, filename]() { m_resumeDataSavingManager->save(filename, data); }); + QMetaObject::invokeMethod(m_resumeDataStorage + , [this, queue]() { m_resumeDataStorage->storeQueue(queue); }); } void Session::removeTorrentsQueue() const { - const QString filename = QLatin1String {"queue"}; - QMetaObject::invokeMethod(m_resumeDataSavingManager - , [this, filename]() { m_resumeDataSavingManager->remove(filename); }); + QMetaObject::invokeMethod(m_resumeDataStorage + , [this]() { m_resumeDataStorage->storeQueue({}); }); } void Session::setDefaultSavePath(QString path) @@ -3945,48 +3912,8 @@ void Session::handleTorrentResumeDataReady(TorrentImpl *const torrent, const Loa { --m_numResumeData; - // We need to adjust native libtorrent resume data - lt::add_torrent_params p = data.ltAddTorrentParams; - p.save_path = Profile::instance()->toPortablePath(QString::fromStdString(p.save_path)).toStdString(); - if (data.paused) - { - p.flags |= lt::torrent_flags::paused; - p.flags &= ~lt::torrent_flags::auto_managed; - } - else - { - // Torrent can be actually "running" but temporarily "paused" to perform some - // service jobs behind the scenes so we need to restore it as "running" - if (!data.forced) - { - p.flags |= lt::torrent_flags::auto_managed; - } - else - { - p.flags &= ~lt::torrent_flags::paused; - p.flags &= ~lt::torrent_flags::auto_managed; - } - } - - // Separated thread is used for the blocking IO which results in slow processing of many torrents. - // Copying lt::entry objects around isn't cheap. - - auto resumeDataPtr = std::make_shared(lt::write_resume_data(p)); - lt::entry &resumeData = *resumeDataPtr; - - resumeData["qBt-savePath"] = Profile::instance()->toPortablePath(data.savePath).toStdString(); - resumeData["qBt-ratioLimit"] = static_cast(data.ratioLimit * 1000); - resumeData["qBt-seedingTimeLimit"] = data.seedingTimeLimit; - resumeData["qBt-category"] = data.category.toStdString(); - resumeData["qBt-tags"] = setToEntryList(data.tags); - resumeData["qBt-name"] = data.name.toStdString(); - resumeData["qBt-seedStatus"] = data.hasSeedStatus; - resumeData["qBt-contentLayout"] = Utils::String::fromEnum(data.contentLayout).toStdString(); - resumeData["qBt-firstLastPiecePriority"] = data.firstLastPiecePriority; - - const QString filename = QString::fromLatin1("%1.fastresume").arg(torrent->id().toString()); - QMetaObject::invokeMethod(m_resumeDataSavingManager - , [this, filename, resumeDataPtr]() { m_resumeDataSavingManager->save(filename, resumeDataPtr); }); + QMetaObject::invokeMethod(m_resumeDataStorage + , [this, torrentID = torrent->id(), data]() { m_resumeDataStorage->store(torrentID, data); }); } void Session::handleTorrentTrackerReply(TorrentImpl *const torrent, const QString &trackerUrl) @@ -4119,7 +4046,7 @@ bool Session::hasPerTorrentSeedingTimeLimit() const }); } -void Session::initResumeFolder() +void Session::initResumeDataStorage() { m_resumeFolderPath = Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Data) + RESUME_FOLDER); const QDir resumeFolderDir(m_resumeFolderPath); @@ -4128,17 +4055,19 @@ void Session::initResumeFolder() m_resumeFolderLock->setFileName(resumeFolderDir.absoluteFilePath("session.lock")); if (!m_resumeFolderLock->open(QFile::WriteOnly)) { - throw RuntimeError - {tr("Cannot write to torrent resume folder: \"%1\"") - .arg(Utils::Fs::toNativePath(m_resumeFolderPath))}; + throw RuntimeError {tr("Cannot write to torrent resume folder: \"%1\"") + .arg(Utils::Fs::toNativePath(m_resumeFolderPath))}; } } else { - throw RuntimeError - {tr("Cannot create torrent resume folder: \"%1\"") - .arg(Utils::Fs::toNativePath(m_resumeFolderPath))}; + throw RuntimeError {tr("Cannot create torrent resume folder: \"%1\"") + .arg(Utils::Fs::toNativePath(m_resumeFolderPath))}; } + + m_resumeDataStorage = new BencodeResumeDataStorage {m_resumeFolderPath}; + m_resumeDataStorage->moveToThread(m_ioThread); + connect(m_ioThread, &QThread::finished, m_resumeDataStorage, &QObject::deleteLater); } void Session::configureDeferred() @@ -4219,159 +4148,22 @@ const CacheStatus &Session::cacheStatus() const return m_cacheStatus; } -bool Session::loadTorrentResumeData(const QByteArray &data, const TorrentInfo &metadata, LoadTorrentParams &torrentParams) -{ - torrentParams = {}; - - lt::error_code ec; - const lt::bdecode_node root = lt::bdecode(data, ec); - if (ec || (root.type() != lt::bdecode_node::dict_t)) return false; - - torrentParams.restored = true; - torrentParams.category = fromLTString(root.dict_find_string_value("qBt-category")); - torrentParams.name = fromLTString(root.dict_find_string_value("qBt-name")); - torrentParams.savePath = Profile::instance()->fromPortablePath( - Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-savePath")))); - torrentParams.hasSeedStatus = root.dict_find_int_value("qBt-seedStatus"); - torrentParams.firstLastPiecePriority = root.dict_find_int_value("qBt-firstLastPiecePriority"); - torrentParams.seedingTimeLimit = root.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME); - - // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x. - // === BEGIN DEPRECATED CODE === // - const lt::bdecode_node contentLayoutNode = root.dict_find("qBt-contentLayout"); - if (contentLayoutNode.type() == lt::bdecode_node::string_t) - { - const QString contentLayoutStr = fromLTString(contentLayoutNode.string_value()); - torrentParams.contentLayout = Utils::String::toEnum(contentLayoutStr, TorrentContentLayout::Original); - } - else - { - const bool hasRootFolder = root.dict_find_int_value("qBt-hasRootFolder"); - torrentParams.contentLayout = (hasRootFolder ? TorrentContentLayout::Original : TorrentContentLayout::NoSubfolder); - } - // === END DEPRECATED CODE === // - // === BEGIN REPLACEMENT CODE === // -// torrentParams.contentLayout = Utils::String::parse( -// fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default); - // === END REPLACEMENT CODE === // - - const lt::string_view ratioLimitString = root.dict_find_string_value("qBt-ratioLimit"); - if (ratioLimitString.empty()) - torrentParams.ratioLimit = root.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO * 1000) / 1000.0; - else - torrentParams.ratioLimit = fromLTString(ratioLimitString).toDouble(); - - const lt::bdecode_node tagsNode = root.dict_find("qBt-tags"); - if (tagsNode.type() == lt::bdecode_node::list_t) - { - for (int i = 0; i < tagsNode.list_size(); ++i) - { - const QString tag = fromLTString(tagsNode.list_string_value_at(i)); - if (Session::isValidTag(tag)) - torrentParams.tags << tag; - } - } - - // NOTE: Do we really need the following block in case of existing (restored) torrent? - torrentParams.savePath = normalizePath(torrentParams.savePath); - if (!torrentParams.category.isEmpty()) - { - if (!m_categories.contains(torrentParams.category) && !addCategory(torrentParams.category)) - torrentParams.category = ""; - } - - lt::add_torrent_params &p = torrentParams.ltAddTorrentParams; - - p = lt::read_resume_data(root, ec); - p.save_path = Profile::instance()->fromPortablePath(fromLTString(p.save_path)).toStdString(); - if (metadata.isValid()) - p.ti = metadata.nativeInfo(); - - if (p.flags & lt::torrent_flags::stop_when_ready) - { - // If torrent has "stop_when_ready" flag set then it is actually "stopped" - torrentParams.paused = true; - torrentParams.forced = false; - // ...but temporarily "resumed" to perform some service jobs (e.g. checking) - p.flags &= ~lt::torrent_flags::paused; - p.flags |= lt::torrent_flags::auto_managed; - } - else - { - torrentParams.paused = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed); - torrentParams.forced = !(p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed); - } - - const bool hasMetadata = (p.ti && p.ti->is_valid()); - if (!hasMetadata && !root.dict_find("info-hash")) - return false; - - return true; -} - // Will resume torrents in backup directory void Session::startUpTorrents() { - const QDir resumeDataDir {m_resumeFolderPath}; - QStringList fastresumes = resumeDataDir.entryList( - QStringList(QLatin1String("*.fastresume")), QDir::Files, QDir::Unsorted); - - const auto readFile = [](const QString &path, QByteArray &buf) -> bool - { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) - { - LogMsg(tr("Cannot read file %1: %2").arg(path, file.errorString()), Log::WARNING); - return false; - } - - buf = file.readAll(); - return true; - }; - qDebug("Starting up torrents..."); - qDebug("Queue size: %d", fastresumes.size()); - - const QRegularExpression rx(QLatin1String("^([A-Fa-f0-9]{40})\\.fastresume$")); - - if (isQueueingSystemEnabled()) - { - QFile queueFile {resumeDataDir.absoluteFilePath(QLatin1String {"queue"})}; - QStringList queue; - if (queueFile.open(QFile::ReadOnly)) - { - QByteArray line; - while (!(line = queueFile.readLine()).isEmpty()) - queue.append(QString::fromLatin1(line.trimmed()) + QLatin1String {".fastresume"}); - } - else - { - LogMsg(tr("Couldn't load torrents queue from '%1'. Error: %2") - .arg(queueFile.fileName(), queueFile.errorString()), Log::WARNING); - } - - if (!queue.empty()) - fastresumes = queue + List::toSet(fastresumes).subtract(List::toSet(queue)).values(); - } + const QVector torrents = m_resumeDataStorage->registeredTorrents(); int resumedTorrentsCount = 0; - for (const QString &fastresumeName : asConst(fastresumes)) - { - const QRegularExpressionMatch rxMatch = rx.match(fastresumeName); - if (!rxMatch.hasMatch()) continue; - - const QString hash = rxMatch.captured(1); - const QString fastresumePath = resumeDataDir.absoluteFilePath(fastresumeName); - QByteArray data; - LoadTorrentParams torrentParams; - const QString torrentFilePath = resumeDataDir.filePath(QString::fromLatin1("%1.torrent").arg(hash)); - TorrentInfo metadata = TorrentInfo::loadFromFile(torrentFilePath); - if (readFile(fastresumePath, data) && loadTorrentResumeData(data, metadata, torrentParams)) + for (const TorrentID &torrentID : torrents) + { + const std::optional resumeData = m_resumeDataStorage->load(torrentID); + if (resumeData) { - qDebug() << "Starting up torrent" << hash << "..."; - if (!loadTorrent(torrentParams)) + qDebug() << "Starting up torrent" << torrentID.toString() << "..."; + if (!loadTorrent(*resumeData)) LogMsg(tr("Unable to resume torrent '%1'.", "e.g: Unable to resume torrent 'hash'.") - .arg(hash), Log::CRITICAL); + .arg(torrentID.toString()), Log::CRITICAL); // process add torrent messages before message queue overflow if ((resumedTorrentsCount % 100) == 0) readAlerts(); @@ -4381,7 +4173,7 @@ void Session::startUpTorrents() else { LogMsg(tr("Unable to resume torrent '%1'.", "e.g: Unable to resume torrent 'hash'.") - .arg(hash), Log::CRITICAL); + .arg(torrentID.toString()), Log::CRITICAL); } } } diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index c6bd8c813..e6a2bb0b2 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -62,7 +62,6 @@ class QUrl; class BandwidthScheduler; class FileSearcher; class FilterParserThread; -class ResumeDataSavingManager; class Statistics; // These values should remain unchanged when adding new items @@ -96,6 +95,7 @@ namespace BitTorrent { class InfoHash; class MagnetUri; + class ResumeDataStorage; class Torrent; class TorrentImpl; class Tracker; @@ -567,7 +567,7 @@ namespace BitTorrent bool hasPerTorrentRatioLimit() const; bool hasPerTorrentSeedingTimeLimit() const; - void initResumeFolder(); + void initResumeDataStorage(); // Session configuration Q_INVOKABLE void configure(); @@ -593,7 +593,6 @@ namespace BitTorrent void applyOSMemoryPriority() const; #endif - bool loadTorrentResumeData(const QByteArray &data, const TorrentInfo &metadata, LoadTorrentParams &torrentParams); bool loadTorrent(LoadTorrentParams params); LoadTorrentParams initLoadTorrentParams(const AddTorrentParams &addTorrentParams); bool addTorrent_impl(const std::variant &source, const AddTorrentParams &addTorrentParams); @@ -762,7 +761,7 @@ namespace BitTorrent QPointer m_tracker; // fastresume data writing thread QThread *m_ioThread = nullptr; - ResumeDataSavingManager *m_resumeDataSavingManager = nullptr; + ResumeDataStorage *m_resumeDataStorage = nullptr; FileSearcher *m_fileSearcher = nullptr; QSet m_downloadedMetadata; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index bbc1bde31..ce2c16f12 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -63,6 +63,7 @@ #include "base/utils/string.h" #include "common.h" #include "downloadpriority.h" +#include "loadtorrentparams.h" #include "ltqhash.h" #include "ltunderlyingtype.h" #include "peeraddress.h" diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index 9aef754dd..5b49b7d7b 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -52,27 +52,7 @@ namespace BitTorrent { class Session; - struct AddTorrentParams; - - struct LoadTorrentParams - { - lt::add_torrent_params ltAddTorrentParams {}; - - QString name; - QString category; - QSet tags; - QString savePath; - TorrentContentLayout contentLayout = TorrentContentLayout::Original; - bool firstLastPiecePriority = false; - bool hasSeedStatus = false; - bool forced = false; - bool paused = false; - - qreal ratioLimit = Torrent::USE_GLOBAL_RATIO; - int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME; - - bool restored = false; // is existing torrent job? - }; + struct LoadTorrentParams; enum class MoveStorageMode {