From 62b50d1475bbb3b7d7a0c747b90d41c402754fe9 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Thu, 9 Dec 2021 13:05:49 +0300 Subject: [PATCH 1/2] Make TorrentInfo immutable --- src/base/CMakeLists.txt | 1 + src/base/base.pri | 1 + src/base/bittorrent/addtorrentparams.h | 1 + src/base/bittorrent/session.cpp | 118 +++++++++----- src/base/bittorrent/session.h | 2 +- src/base/bittorrent/torrentcontentlayout.cpp | 70 ++++++++ src/base/bittorrent/torrentcontentlayout.h | 5 +- src/base/bittorrent/torrentimpl.cpp | 64 ++++---- src/base/bittorrent/torrentimpl.h | 7 +- src/base/bittorrent/torrentinfo.cpp | 161 +++++-------------- src/base/bittorrent/torrentinfo.h | 23 ++- src/base/utils/fs.cpp | 37 +++++ src/base/utils/fs.h | 4 + src/gui/addnewtorrentdialog.cpp | 93 ++++++++--- src/gui/addnewtorrentdialog.h | 3 +- src/gui/properties/peerlistwidget.cpp | 4 +- src/gui/properties/piecesbar.cpp | 18 ++- src/gui/properties/propertieswidget.cpp | 2 +- src/gui/torrentcontentmodel.cpp | 4 +- src/gui/torrentcontentmodel.h | 4 +- src/webui/api/synccontroller.cpp | 6 +- src/webui/api/torrentscontroller.cpp | 9 +- 22 files changed, 382 insertions(+), 255 deletions(-) create mode 100644 src/base/bittorrent/torrentcontentlayout.cpp diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 1b96f61f2..064e233dc 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -116,6 +116,7 @@ add_library(qbt_base STATIC bittorrent/speedmonitor.cpp bittorrent/statistics.cpp bittorrent/torrent.cpp + bittorrent/torrentcontentlayout.cpp bittorrent/torrentcreatorthread.cpp bittorrent/torrentimpl.cpp bittorrent/torrentinfo.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 57af3dbd6..8607ca085 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -116,6 +116,7 @@ SOURCES += \ $$PWD/bittorrent/speedmonitor.cpp \ $$PWD/bittorrent/statistics.cpp \ $$PWD/bittorrent/torrent.cpp \ + $$PWD/bittorrent/torrentcontentlayout.cpp \ $$PWD/bittorrent/torrentcreatorthread.cpp \ $$PWD/bittorrent/torrentimpl.cpp \ $$PWD/bittorrent/torrentinfo.cpp \ diff --git a/src/base/bittorrent/addtorrentparams.h b/src/base/bittorrent/addtorrentparams.h index 23967b5be..3306667ab 100644 --- a/src/base/bittorrent/addtorrentparams.h +++ b/src/base/bittorrent/addtorrentparams.h @@ -53,6 +53,7 @@ namespace BitTorrent bool firstLastPiecePriority = false; bool addForced = false; std::optional addPaused; + QStringList filePaths; // used if TorrentInfo is set QVector filePriorities; // used if TorrentInfo is set bool skipChecking = false; std::optional contentLayout; diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 559d380c1..4d5d0638f 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -1698,11 +1698,14 @@ void Session::handleDownloadFinished(const Net::DownloadResult &result) { case Net::DownloadStatus::Success: emit downloadFromUrlFinished(result.url); - addTorrent(TorrentInfo::load(result.data).value_or(TorrentInfo()), m_downloadedTorrents.take(result.url)); + if (const nonstd::expected loadResult = TorrentInfo::load(result.data); loadResult) + addTorrent(loadResult.value(), m_downloadedTorrents.take(result.url)); + else + LogMsg(tr("Couldn't load torrent: %1").arg(loadResult.error()), Log::WARNING); break; case Net::DownloadStatus::RedirectedToMagnet: emit downloadFromUrlFinished(result.url); - addTorrent(MagnetUri {result.magnet}, m_downloadedTorrents.take(result.url)); + addTorrent(MagnetUri(result.magnet), m_downloadedTorrents.take(result.url)); break; default: emit downloadFromUrlFailed(result.url, result.errorString); @@ -1727,7 +1730,7 @@ void Session::fileSearchFinished(const TorrentID &id, const QString &savePath, c lt::add_torrent_params &p = params.ltAddTorrentParams; p.save_path = Utils::Fs::toNativePath(savePath).toStdString(); - const TorrentInfo torrentInfo {p.ti}; + const TorrentInfo torrentInfo {*p.ti}; const auto nativeIndexes = torrentInfo.nativeIndexes(); for (int i = 0; i < fileNames.size(); ++i) p.renamed_files[nativeIndexes[i]] = fileNames[i].toStdString(); @@ -2028,13 +2031,15 @@ bool Session::addTorrent(const QString &source, const AddTorrentParams ¶ms) return addTorrent(magnetUri, params); TorrentFileGuard guard {source}; - if (addTorrent(TorrentInfo::loadFromFile(source).value_or(TorrentInfo()), params)) + const nonstd::expected loadResult = TorrentInfo::loadFromFile(source); + if (!loadResult) { - guard.markAsAddedToSession(); - return true; + LogMsg(tr("Couldn't load torrent: %1").arg(loadResult.error()), Log::WARNING); + return false; } - return false; + guard.markAsAddedToSession(); + return addTorrent(loadResult.value(), params); } bool Session::addTorrent(const MagnetUri &magnetUri, const AddTorrentParams ¶ms) @@ -2046,8 +2051,6 @@ bool Session::addTorrent(const MagnetUri &magnetUri, const AddTorrentParams &par bool Session::addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams ¶ms) { - if (!torrentInfo.isValid()) return false; - return addTorrent_impl(torrentInfo, params); } @@ -2093,9 +2096,7 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr bool Session::addTorrent_impl(const std::variant &source, const AddTorrentParams &addTorrentParams) { const bool hasMetadata = std::holds_alternative(source); - TorrentInfo metadata = (hasMetadata ? std::get(source) : TorrentInfo {}); - const MagnetUri &magnetUri = (hasMetadata ? MagnetUri {} : std::get(source)); - const auto id = TorrentID::fromInfoHash(hasMetadata ? metadata.infoHash() : magnetUri.infoHash()); + const auto id = TorrentID::fromInfoHash(hasMetadata ? std::get(source).infoHash() : std::get(source).infoHash()); // It looks illogical that we don't just use an existing handle, // but as previous experience has shown, it actually creates unnecessary @@ -2111,12 +2112,29 @@ bool Session::addTorrent_impl(const std::variant &source TorrentImpl *const torrent = m_torrents.value(id); if (torrent) { // a duplicate torrent is added - if (torrent->isPrivate() || (hasMetadata && metadata.isPrivate())) + if (torrent->isPrivate()) return false; - // merge trackers and web seeds - torrent->addTrackers(hasMetadata ? metadata.trackers() : magnetUri.trackers()); - torrent->addUrlSeeds(hasMetadata ? metadata.urlSeeds() : magnetUri.urlSeeds()); + if (hasMetadata) + { + const TorrentInfo &torrentInfo = std::get(source); + + if (torrentInfo.isPrivate()) + return false; + + // merge trackers and web seeds + torrent->addTrackers(torrentInfo.trackers()); + torrent->addUrlSeeds(torrentInfo.urlSeeds()); + } + else + { + const MagnetUri &magnetUri = std::get(source); + + // merge trackers and web seeds + torrent->addTrackers(magnetUri.trackers()); + torrent->addUrlSeeds(magnetUri.urlSeeds()); + } + return true; } @@ -2129,39 +2147,53 @@ bool Session::addTorrent_impl(const std::variant &source const QString actualSavePath = loadTorrentParams.savePath.isEmpty() ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath; if (hasMetadata) { - metadata.setContentLayout(loadTorrentParams.contentLayout); - - if (!loadTorrentParams.hasSeedStatus) - { - findIncompleteFiles(metadata, actualSavePath); - isFindingIncompleteFiles = true; - } + const TorrentInfo &torrentInfo = std::get(source); // if torrent name wasn't explicitly set we handle the case of // initial renaming of torrent content and rename torrent accordingly if (loadTorrentParams.name.isEmpty()) { - QString contentName = metadata.rootFolder(); - if (contentName.isEmpty() && (metadata.filesCount() == 1)) - contentName = Utils::Fs::fileName(metadata.filePath(0)); + QString contentName = torrentInfo.rootFolder(); + if (contentName.isEmpty() && (torrentInfo.filesCount() == 1)) + contentName = Utils::Fs::fileName(torrentInfo.filePath(0)); - if (!contentName.isEmpty() && (contentName != metadata.name())) + if (!contentName.isEmpty() && (contentName != torrentInfo.name())) loadTorrentParams.name = contentName; } + Q_ASSERT(addTorrentParams.filePaths.isEmpty() || (addTorrentParams.filePaths.size() == torrentInfo.filesCount())); + + const TorrentContentLayout contentLayout = ((loadTorrentParams.contentLayout == TorrentContentLayout::Original) + ? detectContentLayout(torrentInfo.filePaths()) : loadTorrentParams.contentLayout); + QStringList filePaths = (!addTorrentParams.filePaths.isEmpty() ? addTorrentParams.filePaths : torrentInfo.filePaths()); + applyContentLayout(filePaths, contentLayout, Utils::Fs::findRootFolder(torrentInfo.filePaths())); + + if (!loadTorrentParams.hasSeedStatus) + { + findIncompleteFiles(torrentInfo, actualSavePath, filePaths); + isFindingIncompleteFiles = true; + } + + const auto nativeIndexes = torrentInfo.nativeIndexes(); + if (!filePaths.isEmpty()) + { + for (int index = 0; index < addTorrentParams.filePaths.size(); ++index) + p.renamed_files[nativeIndexes[index]] = Utils::Fs::toNativePath(addTorrentParams.filePaths.at(index)).toStdString(); + } + Q_ASSERT(p.file_priorities.empty()); - const int internalFilesCount = metadata.nativeInfo()->files().num_files(); // including .pad files + const int internalFilesCount = torrentInfo.nativeInfo()->files().num_files(); // including .pad files // Use qBittorrent default priority rather than libtorrent's (4) p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal)); - const auto nativeIndexes = metadata.nativeIndexes(); Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size())); for (int i = 0; i < addTorrentParams.filePriorities.size(); ++i) p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(addTorrentParams.filePriorities[i]); - p.ti = metadata.nativeInfo(); + p.ti = torrentInfo.nativeInfo(); } else { + const MagnetUri &magnetUri = std::get(source); p = magnetUri.addTorrentParams(); if (loadTorrentParams.name.isEmpty() && !p.name.empty()) @@ -2232,10 +2264,12 @@ bool Session::loadTorrent(LoadTorrentParams params) return true; } -void Session::findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath) const +void Session::findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath, const QStringList &filePaths) const { + Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); + const auto searchId = TorrentID::fromInfoHash(torrentInfo.infoHash()); - const QStringList originalFileNames = torrentInfo.filePaths(); + const QStringList originalFileNames = (filePaths.isEmpty() ? torrentInfo.filePaths() : filePaths); const QString completeSavePath = savePath; const QString incompleteSavePath = (isTempPathEnabled() ? torrentTempPath(torrentInfo) : QString {}); QMetaObject::invokeMethod(m_fileSearcher, [=]() @@ -3899,9 +3933,9 @@ void Session::handleTorrentMetadataReceived(TorrentImpl *const torrent) if (!torrentExportDirectory().isEmpty()) { #ifdef QBT_USES_LIBTORRENT2 - const TorrentInfo torrentInfo {torrent->nativeHandle().torrent_file_with_hashes()}; + const TorrentInfo torrentInfo {*torrent->nativeHandle().torrent_file_with_hashes()}; #else - const TorrentInfo torrentInfo {torrent->nativeHandle().torrent_file()}; + const TorrentInfo torrentInfo {*torrent->nativeHandle().torrent_file()}; #endif exportTorrentFile(torrentInfo, torrentExportDirectory(), torrent->name()); } @@ -3955,9 +3989,9 @@ void Session::handleTorrentFinished(TorrentImpl *const torrent) if (!finishedTorrentExportDirectory().isEmpty()) { #ifdef QBT_USES_LIBTORRENT2 - const TorrentInfo torrentInfo {torrent->nativeHandle().torrent_file_with_hashes()}; + const TorrentInfo torrentInfo {*torrent->nativeHandle().torrent_file_with_hashes()}; #else - const TorrentInfo torrentInfo {torrent->nativeHandle().torrent_file()}; + const TorrentInfo torrentInfo {*torrent->nativeHandle().torrent_file()}; #endif exportTorrentFile(torrentInfo, finishedTorrentExportDirectory(), torrent->name()); } @@ -4155,7 +4189,7 @@ void Session::recursiveTorrentDownload(const TorrentID &id) for (const QString &torrentRelpath : asConst(torrent->filePaths())) { - if (torrentRelpath.endsWith(".torrent")) + if (torrentRelpath.endsWith(QLatin1String(".torrent"))) { LogMsg(tr("Recursive download of file '%1' embedded in torrent '%2'" , "Recursive download of 'test.torrent' embedded in torrent 'test2'") @@ -4165,7 +4199,11 @@ void Session::recursiveTorrentDownload(const TorrentID &id) AddTorrentParams params; // Passing the save path along to the sub torrent file params.savePath = torrent->savePath(); - addTorrent(TorrentInfo::loadFromFile(torrentFullpath).value_or(TorrentInfo()), params); + const nonstd::expected loadResult = TorrentInfo::loadFromFile(torrentFullpath); + if (loadResult) + addTorrent(loadResult.value(), params); + else + LogMsg(tr("Couldn't load torrent: %1").arg(loadResult.error()), Log::WARNING); } } } @@ -4468,7 +4506,7 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle) // Copy the torrent file to the export folder if (!torrentExportDirectory().isEmpty()) { - const TorrentInfo torrentInfo {params.ltAddTorrentParams.ti}; + const TorrentInfo torrentInfo {*params.ltAddTorrentParams.ti}; exportTorrentFile(torrentInfo, torrentExportDirectory(), torrent->name()); } } @@ -4597,7 +4635,7 @@ void Session::handleMetadataReceivedAlert(const lt::metadata_received_alert *p) if (downloadedMetadataIter != m_downloadedMetadata.end()) { - const TorrentInfo metadata {p->handle.torrent_file()}; + const TorrentInfo metadata {*p->handle.torrent_file()}; m_downloadedMetadata.erase(downloadedMetadataIter); --m_extraLimit; diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index 6bb927f8a..583cd36ba 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -499,7 +499,7 @@ namespace BitTorrent bool addMoveTorrentStorageJob(TorrentImpl *torrent, const QString &newPath, MoveStorageMode mode); - void findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath) const; + void findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath, const QStringList &filePaths = {}) const; signals: void allTorrentsFinished(); diff --git a/src/base/bittorrent/torrentcontentlayout.cpp b/src/base/bittorrent/torrentcontentlayout.cpp new file mode 100644 index 000000000..59718fddd --- /dev/null +++ b/src/base/bittorrent/torrentcontentlayout.cpp @@ -0,0 +1,70 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2020-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 + * 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 "torrentcontentlayout.h" + +#include "base/utils/fs.h" + +namespace +{ + QString removeExtension(const QString &fileName) + { + const QString extension = Utils::Fs::fileExtension(fileName); + return extension.isEmpty() + ? fileName + : fileName.chopped(extension.size() + 1); + } +} + +BitTorrent::TorrentContentLayout BitTorrent::detectContentLayout(const QStringList &filePaths) +{ + const QString rootFolder = Utils::Fs::findRootFolder(filePaths); + return (rootFolder.isEmpty() + ? TorrentContentLayout::NoSubfolder + : TorrentContentLayout::Subfolder); +} + +void BitTorrent::applyContentLayout(QStringList &filePaths, const TorrentContentLayout contentLayout, const QString &rootFolder) +{ + Q_ASSERT(!filePaths.isEmpty()); + + switch (contentLayout) + { + case TorrentContentLayout::Subfolder: + if (Utils::Fs::findRootFolder(filePaths).isEmpty()) + Utils::Fs::addRootFolder(filePaths, !rootFolder.isEmpty() ? rootFolder : removeExtension(filePaths.at(0))); + break; + + case TorrentContentLayout::NoSubfolder: + Utils::Fs::stripRootFolder(filePaths); + break; + + default: + break; + } +} diff --git a/src/base/bittorrent/torrentcontentlayout.h b/src/base/bittorrent/torrentcontentlayout.h index 2483997f4..a93a6b2e1 100644 --- a/src/base/bittorrent/torrentcontentlayout.h +++ b/src/base/bittorrent/torrentcontentlayout.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2020 Vladimir Golovnev + * Copyright (C) 2020-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 @@ -48,4 +48,7 @@ namespace BitTorrent Q_ENUM_NS(TorrentContentLayout) } + + TorrentContentLayout detectContentLayout(const QStringList &filePaths); + void applyContentLayout(QStringList &filePaths, TorrentContentLayout contentLayout, const QString &rootFolder = {}); } diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 9f4599e9e..6819a2e99 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -272,7 +272,19 @@ TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession { // Initialize it only if torrent is added with metadata. // Otherwise it should be initialized in "Metadata received" handler. - m_torrentInfo = TorrentInfo {m_nativeHandle.torrent_file()}; + m_torrentInfo = TorrentInfo(*m_ltAddTorrentParams.ti); + + Q_ASSERT(m_filePaths.isEmpty()); + const int filesCount = m_torrentInfo.filesCount(); + m_filePaths.reserve(filesCount); + const std::shared_ptr currentInfo = m_nativeHandle.torrent_file(); + const lt::file_storage &fileStorage = currentInfo->files(); + for (int i = 0; i < filesCount; ++i) + { + const lt::file_index_t nativeIndex = m_torrentInfo.nativeIndexes().at(i); + const QString filePath = Utils::Fs::toUniformPath(QString::fromStdString(fileStorage.file_path(nativeIndex))); + m_filePaths.append(filePath); + } } initializeStatus(m_nativeStatus, m_ltAddTorrentParams); @@ -743,7 +755,7 @@ int TorrentImpl::seedingTimeLimit() const QString TorrentImpl::filePath(const int index) const { - return m_torrentInfo.filePath(index); + return m_filePaths.at(index); } qlonglong TorrentImpl::fileSize(const int index) const @@ -753,7 +765,7 @@ qlonglong TorrentImpl::fileSize(const int index) const QStringList TorrentImpl::filePaths() const { - return m_torrentInfo.filePaths(); + return m_filePaths; } // Return a list of absolute paths corresponding @@ -1461,7 +1473,7 @@ void TorrentImpl::applyFirstLastPiecePriority(const bool enabled, const QVector< // Determine the priority to set const DownloadPriority newPrio = enabled ? DownloadPriority::Maximum : filePrio; const auto piecePrio = static_cast(static_cast(newPrio)); - const TorrentInfo::PieceRange extremities = info().filePieces(index); + const TorrentInfo::PieceRange extremities = m_torrentInfo.filePieces(index); // worst case: AVI index = 1% of total file size (at the end of the file) const int nNumPieces = std::ceil(fileSize(index) * 0.01 / pieceLength()); @@ -1484,12 +1496,14 @@ void TorrentImpl::endReceivedMetadataHandling(const QString &savePath, const QSt { lt::add_torrent_params &p = m_ltAddTorrentParams; - const TorrentInfo torrentInfo {m_nativeHandle.torrent_file()}; - const auto nativeIndexes = torrentInfo.nativeIndexes(); + const std::shared_ptr metadata = std::const_pointer_cast(m_nativeHandle.torrent_file()); + m_torrentInfo = TorrentInfo(*metadata); + m_filePaths = fileNames; + const auto nativeIndexes = m_torrentInfo.nativeIndexes(); for (int i = 0; i < fileNames.size(); ++i) p.renamed_files[nativeIndexes[i]] = fileNames[i].toStdString(); p.save_path = Utils::Fs::toNativePath(savePath).toStdString(); - p.ti = torrentInfo.nativeInfo(); + p.ti = metadata; const int internalFilesCount = p.ti->files().num_files(); // including .pad files // Use qBittorrent default priority rather than libtorrent's (4) @@ -1536,8 +1550,6 @@ void TorrentImpl::reload() m_nativeHandle = m_nativeSession->add_torrent(p); m_nativeHandle.queue_position_set(queuePos); - - m_torrentInfo = TorrentInfo {m_nativeHandle.torrent_file()}; } void TorrentImpl::pause() @@ -1608,10 +1620,6 @@ void TorrentImpl::moveStorage(const QString &newPath, const MoveStorageMode mode void TorrentImpl::renameFile(const int index, const QString &path) { -#ifndef QBT_USES_LIBTORRENT2 - const QString oldPath = filePath(index); - m_oldPath[index].push_back(oldPath); -#endif ++m_renameCount; m_nativeHandle.rename_file(m_torrentInfo.nativeIndexes().at(index), Utils::Fs::toNativePath(path).toStdString()); } @@ -1766,10 +1774,11 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p) m_ltAddTorrentParams.have_pieces.clear(); m_ltAddTorrentParams.verified_pieces.clear(); - TorrentInfo metadata = TorrentInfo {m_nativeHandle.torrent_file()}; - metadata.setContentLayout(m_contentLayout); + TorrentInfo metadata = TorrentInfo(*m_nativeHandle.torrent_file()); - m_session->findIncompleteFiles(metadata, m_savePath); + QStringList filePaths = metadata.filePaths(); + applyContentLayout(filePaths, m_contentLayout); + m_session->findIncompleteFiles(metadata, m_savePath, filePaths); } else { @@ -1848,15 +1857,11 @@ void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p) // Remove empty leftover folders // For example renaming "a/b/c" to "d/b/c", then folders "a/b" and "a" will // be removed if they are empty -#ifndef QBT_USES_LIBTORRENT2 - const QString oldFilePath = m_oldPath[fileIndex].takeFirst(); - if (m_oldPath[fileIndex].isEmpty()) - m_oldPath.remove(fileIndex); -#else - const QString oldFilePath = Utils::Fs::toUniformPath(p->old_name()); -#endif + const QString oldFilePath = m_filePaths.at(fileIndex); const QString newFilePath = Utils::Fs::toUniformPath(p->new_name()); + m_filePaths[fileIndex] = newFilePath; + QList oldPathParts = QStringView(oldFilePath).split('/', Qt::SkipEmptyParts); oldPathParts.removeLast(); // drop file name part QList newPathParts = QStringView(newFilePath).split('/', Qt::SkipEmptyParts); @@ -1897,12 +1902,6 @@ void TorrentImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert LogMsg(tr("File rename failed. Torrent: \"%1\", file: \"%2\", reason: \"%3\"") .arg(name(), filePath(fileIndex), QString::fromLocal8Bit(p->error.message().c_str())), Log::WARNING); -#ifndef QBT_USES_LIBTORRENT2 - m_oldPath[fileIndex].removeFirst(); - if (m_oldPath[fileIndex].isEmpty()) - m_oldPath.remove(fileIndex); -#endif - --m_renameCount; while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) m_moveFinishedTriggers.takeFirst()(); @@ -2081,7 +2080,7 @@ void TorrentImpl::adjustActualSavePath() void TorrentImpl::adjustActualSavePath_impl() { const bool needUseTempDir = useTempPath(); - const QDir tempDir {m_session->torrentTempPath(info())}; + const QDir tempDir {m_session->torrentTempPath(m_torrentInfo)}; const QDir currentDir {actualStorageLocation()}; const QDir targetDir {needUseTempDir ? tempDir : QDir {savePath()}}; @@ -2291,6 +2290,8 @@ void TorrentImpl::prioritizeFiles(const QVector &priorities) QVector TorrentImpl::availableFileFractions() const { + Q_ASSERT(hasMetadata()); + const int filesCount = this->filesCount(); if (filesCount <= 0) return {}; @@ -2300,10 +2301,9 @@ QVector TorrentImpl::availableFileFractions() const QVector res; res.reserve(filesCount); - const TorrentInfo info = this->info(); for (int i = 0; i < filesCount; ++i) { - const TorrentInfo::PieceRange filePieces = info.filePieces(i); + const TorrentInfo::PieceRange filePieces = m_torrentInfo.filePieces(i); int availablePieces = 0; for (const int piece : filePieces) diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index 14721e935..4bcaf9818 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -289,6 +289,7 @@ namespace BitTorrent lt::torrent_status m_nativeStatus; TorrentState m_state = TorrentState::Unknown; TorrentInfo m_torrentInfo; + QStringList m_filePaths; SpeedMonitor m_speedMonitor; InfoHash m_infoHash; @@ -301,12 +302,6 @@ namespace BitTorrent MaintenanceJob m_maintenanceJob = MaintenanceJob::None; -#ifndef QBT_USES_LIBTORRENT2 - // Until libtorrent provided an "old_name" field in `file_renamed_alert` - // we relied on this workaround to remove empty leftover folders - QHash> m_oldPath; -#endif - QHash> m_trackerPeerCounts; FileErrorInfo m_lastFileError; diff --git a/src/base/bittorrent/torrentinfo.cpp b/src/base/bittorrent/torrentinfo.cpp index 317fb9726..3f6050e7a 100644 --- a/src/base/bittorrent/torrentinfo.cpp +++ b/src/base/bittorrent/torrentinfo.cpp @@ -49,38 +49,14 @@ using namespace BitTorrent; -namespace -{ - QString getRootFolder(const QStringList &filePaths) - { - QString rootFolder; - for (const QString &filePath : filePaths) - { - if (QDir::isAbsolutePath(filePath)) continue; - - const auto filePathElements = QStringView(filePath).split(u'/'); - // if at least one file has no root folder, no common root folder exists - if (filePathElements.count() <= 1) return {}; - - if (rootFolder.isEmpty()) - rootFolder = filePathElements.at(0).toString(); - else if (rootFolder != filePathElements.at(0)) - return {}; - } - - return rootFolder; - } -} - const int torrentInfoId = qRegisterMetaType(); -TorrentInfo::TorrentInfo(std::shared_ptr nativeInfo) - : m_nativeInfo {std::const_pointer_cast(nativeInfo)} +TorrentInfo::TorrentInfo(const lt::torrent_info &nativeInfo) + : m_nativeInfo {std::make_shared(nativeInfo)} { - if (!m_nativeInfo) - return; + Q_ASSERT(m_nativeInfo->is_valid() && (m_nativeInfo->num_files() > 0)); - const lt::file_storage &fileStorage = m_nativeInfo->files(); + const lt::file_storage &fileStorage = m_nativeInfo->orig_files(); m_nativeIndexes.reserve(fileStorage.num_files()); for (const lt::file_index_t nativeIndex : fileStorage.file_range()) { @@ -105,6 +81,11 @@ TorrentInfo &TorrentInfo::operator=(const TorrentInfo &other) return *this; } +bool TorrentInfo::isValid() const +{ + return (m_nativeInfo != nullptr); +} + nonstd::expected TorrentInfo::load(const QByteArray &data) noexcept { // 2-step construction to overcome default limits of `depth_limit` & `token_limit` which are @@ -118,11 +99,11 @@ nonstd::expected TorrentInfo::load(const QByteArray &data) if (ec) return nonstd::make_unexpected(QString::fromStdString(ec.message())); - TorrentInfo info {std::shared_ptr(new lt::torrent_info(node, ec))}; + lt::torrent_info nativeInfo {node, ec}; if (ec) return nonstd::make_unexpected(QString::fromStdString(ec.message())); - return info; + return TorrentInfo(nativeInfo); } nonstd::expected TorrentInfo::loadFromFile(const QString &path) noexcept @@ -173,11 +154,6 @@ nonstd::expected TorrentInfo::saveToFile(const QString &path) con return {}; } -bool TorrentInfo::isValid() const -{ - return (m_nativeInfo && m_nativeInfo->is_valid() && (m_nativeInfo->num_files() > 0)); -} - InfoHash TorrentInfo::infoHash() const { if (!isValid()) return {}; @@ -192,6 +168,7 @@ InfoHash TorrentInfo::infoHash() const QString TorrentInfo::name() const { if (!isValid()) return {}; + return QString::fromStdString(m_nativeInfo->orig_files().name()); } @@ -206,56 +183,65 @@ QDateTime TorrentInfo::creationDate() const QString TorrentInfo::creator() const { if (!isValid()) return {}; + return QString::fromStdString(m_nativeInfo->creator()); } QString TorrentInfo::comment() const { if (!isValid()) return {}; + return QString::fromStdString(m_nativeInfo->comment()); } bool TorrentInfo::isPrivate() const { if (!isValid()) return false; + return m_nativeInfo->priv(); } qlonglong TorrentInfo::totalSize() const { if (!isValid()) return -1; + return m_nativeInfo->total_size(); } int TorrentInfo::filesCount() const { if (!isValid()) return -1; + return m_nativeIndexes.size(); } int TorrentInfo::pieceLength() const { if (!isValid()) return -1; + return m_nativeInfo->piece_length(); } int TorrentInfo::pieceLength(const int index) const { if (!isValid()) return -1; + return m_nativeInfo->piece_size(lt::piece_index_t {index}); } int TorrentInfo::piecesCount() const { if (!isValid()) return -1; + return m_nativeInfo->num_pieces(); } QString TorrentInfo::filePath(const int index) const { if (!isValid()) return {}; + return Utils::Fs::toUniformPath( - QString::fromStdString(m_nativeInfo->files().file_path(m_nativeIndexes[index]))); + QString::fromStdString(m_nativeInfo->orig_files().file_path(m_nativeIndexes[index]))); } QStringList TorrentInfo::filePaths() const @@ -268,23 +254,18 @@ QStringList TorrentInfo::filePaths() const return list; } -QString TorrentInfo::origFilePath(const int index) const -{ - if (!isValid()) return {}; - return Utils::Fs::toUniformPath( - QString::fromStdString(m_nativeInfo->orig_files().file_path(m_nativeIndexes[index]))); -} - qlonglong TorrentInfo::fileSize(const int index) const { if (!isValid()) return -1; - return m_nativeInfo->files().file_size(m_nativeIndexes[index]); + + return m_nativeInfo->orig_files().file_size(m_nativeIndexes[index]); } qlonglong TorrentInfo::fileOffset(const int index) const { if (!isValid()) return -1; - return m_nativeInfo->files().file_offset(m_nativeIndexes[index]); + + return m_nativeInfo->orig_files().file_offset(m_nativeIndexes[index]); } QVector TorrentInfo::trackers() const @@ -349,8 +330,8 @@ QVector TorrentInfo::fileIndicesForPiece(const int pieceIndex) const if (!isValid() || (pieceIndex < 0) || (pieceIndex >= piecesCount())) return {}; - const std::vector files = nativeInfo()->map_block( - lt::piece_index_t {pieceIndex}, 0, nativeInfo()->piece_size(lt::piece_index_t {pieceIndex})); + const std::vector files = m_nativeInfo->map_block( + lt::piece_index_t {pieceIndex}, 0, m_nativeInfo->piece_size(lt::piece_index_t {pieceIndex})); QVector res; res.reserve(static_cast(files.size())); for (const lt::file_slice &fileSlice : files) @@ -403,7 +384,7 @@ TorrentInfo::PieceRange TorrentInfo::filePieces(const int fileIndex) const return {}; } - const lt::file_storage &files = nativeInfo()->files(); + const lt::file_storage &files = m_nativeInfo->orig_files(); const auto fileSize = files.file_size(m_nativeIndexes[fileIndex]); const auto fileOffset = files.file_offset(m_nativeIndexes[fileIndex]); @@ -415,26 +396,25 @@ TorrentInfo::PieceRange TorrentInfo::filePieces(const int fileIndex) const return makeInterval(beginIdx, endIdx); } -void TorrentInfo::renameFile(const int index, const QString &newPath) -{ - if (!isValid()) return; - nativeInfo()->rename_file(m_nativeIndexes[index], Utils::Fs::toNativePath(newPath).toStdString()); -} - int TorrentInfo::fileIndex(const QString &fileName) const { // the check whether the object is valid is not needed here // because if filesCount() returns -1 the loop exits immediately for (int i = 0; i < filesCount(); ++i) + { if (fileName == filePath(i)) return i; + } return -1; } QString TorrentInfo::rootFolder() const { - return getRootFolder(filePaths()); + if (!isValid()) + return {}; + + return Utils::Fs::findRootFolder(filePaths()); } bool TorrentInfo::hasRootFolder() const @@ -442,75 +422,20 @@ bool TorrentInfo::hasRootFolder() const return !rootFolder().isEmpty(); } -void TorrentInfo::setContentLayout(const TorrentContentLayout layout) -{ - switch (layout) - { - case TorrentContentLayout::Original: - setContentLayout(defaultContentLayout()); - break; - case TorrentContentLayout::Subfolder: - if (rootFolder().isEmpty()) - addRootFolder(); - break; - case TorrentContentLayout::NoSubfolder: - if (!rootFolder().isEmpty()) - stripRootFolder(); - break; - } -} - -void TorrentInfo::stripRootFolder() +TorrentContentLayout TorrentInfo::contentLayout() const { - lt::file_storage files = m_nativeInfo->files(); - - // Solution for case of renamed root folder - const QString path = filePath(0); - const std::string newName = path.left(path.indexOf('/')).toStdString(); - if (files.name() != newName) - { - files.set_name(newName); - for (const lt::file_index_t nativeIndex : files.file_range()) - files.rename_file(nativeIndex, files.file_path(nativeIndex)); - } - - files.set_name(""); - m_nativeInfo->remap_files(files); -} - -void TorrentInfo::addRootFolder() -{ - const QString originalName = name(); - Q_ASSERT(!originalName.isEmpty()); - - const QString extension = Utils::Fs::fileExtension(originalName); - const QString rootFolder = extension.isEmpty() - ? originalName - : originalName.chopped(extension.size() + 1); - const std::string rootPrefix = Utils::Fs::toNativePath(rootFolder + QLatin1Char {'/'}).toStdString(); - lt::file_storage files = m_nativeInfo->files(); - files.set_name(rootFolder.toStdString()); - for (const lt::file_index_t nativeIndex : files.file_range()) - files.rename_file(nativeIndex, rootPrefix + files.file_path(nativeIndex)); - m_nativeInfo->remap_files(files); -} - -TorrentContentLayout TorrentInfo::defaultContentLayout() const -{ - QStringList origFilePaths; - origFilePaths.reserve(filesCount()); - for (int i = 0; i < filesCount(); ++i) - origFilePaths << origFilePath(i); + if (!isValid()) + return TorrentContentLayout::Original; - const QString origRootFolder = getRootFolder(origFilePaths); - return (origRootFolder.isEmpty() - ? TorrentContentLayout::NoSubfolder - : TorrentContentLayout::Subfolder); + return detectContentLayout(filePaths()); } std::shared_ptr TorrentInfo::nativeInfo() const { - return m_nativeInfo; + if (!isValid()) + return nullptr; + + return std::make_shared(*m_nativeInfo); } QVector TorrentInfo::nativeIndexes() const diff --git a/src/base/bittorrent/torrentinfo.h b/src/base/bittorrent/torrentinfo.h index a015fa758..4f09a1ed3 100644 --- a/src/base/bittorrent/torrentinfo.h +++ b/src/base/bittorrent/torrentinfo.h @@ -35,7 +35,6 @@ #include "base/3rdparty/expected.hpp" #include "base/indexrange.h" -#include "abstractfilestorage.h" #include "torrentcontentlayout.h" class QByteArray; @@ -48,14 +47,16 @@ namespace BitTorrent class InfoHash; struct TrackerEntry; - class TorrentInfo final : public AbstractFileStorage + class TorrentInfo { Q_DECLARE_TR_FUNCTIONS(TorrentInfo) public: - explicit TorrentInfo(std::shared_ptr nativeInfo = {}); + TorrentInfo() = default; TorrentInfo(const TorrentInfo &other); + explicit TorrentInfo(const lt::torrent_info &nativeInfo); + static nonstd::expected load(const QByteArray &data) noexcept; static nonstd::expected loadFromFile(const QString &path) noexcept; nonstd::expected saveToFile(const QString &path) const; @@ -70,14 +71,13 @@ namespace BitTorrent QString comment() const; bool isPrivate() const; qlonglong totalSize() const; - int filesCount() const override; + int filesCount() const; int pieceLength() const; int pieceLength(int index) const; int piecesCount() const; - QString filePath(int index) const override; + QString filePath(int index) const; QStringList filePaths() const; - QString origFilePath(int index) const; - qlonglong fileSize(int index) const override; + qlonglong fileSize(int index) const; qlonglong fileOffset(int index) const; QVector trackers() const; QVector urlSeeds() const; @@ -92,11 +92,8 @@ namespace BitTorrent PieceRange filePieces(const QString &file) const; PieceRange filePieces(int fileIndex) const; - void renameFile(int index, const QString &newPath) override; - QString rootFolder() const; bool hasRootFolder() const; - void setContentLayout(TorrentContentLayout layout); std::shared_ptr nativeInfo() const; QVector nativeIndexes() const; @@ -104,11 +101,9 @@ namespace BitTorrent private: // returns file index or -1 if fileName is not found int fileIndex(const QString &fileName) const; - void stripRootFolder(); - void addRootFolder(); - TorrentContentLayout defaultContentLayout() const; + TorrentContentLayout contentLayout() const; - std::shared_ptr m_nativeInfo; + std::shared_ptr m_nativeInfo; // internal indexes of files (payload only, excluding any .pad files) // by which they are addressed in libtorrent diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index a273a48c6..5ff572e09 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -398,3 +398,40 @@ bool Utils::Fs::isNetworkFileSystem(const QString &path) #endif } #endif // Q_OS_HAIKU + +QString Utils::Fs::findRootFolder(const QStringList &filePaths) +{ + QString rootFolder; + for (const QString &filePath : filePaths) + { + const auto filePathElements = QStringView(filePath).split(u'/'); + // if at least one file has no root folder, no common root folder exists + if (filePathElements.count() <= 1) + return {}; + + if (rootFolder.isEmpty()) + rootFolder = filePathElements.at(0).toString(); + else if (rootFolder != filePathElements.at(0)) + return {}; + } + + return rootFolder; +} + +void Utils::Fs::stripRootFolder(QStringList &filePaths) +{ + const QString commonRootFolder = findRootFolder(filePaths); + if (commonRootFolder.isEmpty()) + return; + + for (QString &filePath : filePaths) + filePath = filePath.mid(commonRootFolder.size() + 1); +} + +void Utils::Fs::addRootFolder(QStringList &filePaths, const QString &rootFolder) +{ + Q_ASSERT(!rootFolder.isEmpty()); + + for (QString &filePath : filePaths) + filePath = rootFolder + QLatin1Char('/') + filePath; +} diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index 253bca628..9a6d28dab 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -71,6 +71,10 @@ namespace Utils::Fs QString tempPath(); + QString findRootFolder(const QStringList &filePaths); + void stripRootFolder(QStringList &filePaths); + void addRootFolder(QStringList &filePaths, const QString &name); + #if !defined Q_OS_HAIKU bool isNetworkFileSystem(const QString &path); #endif diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index e565e787b..778a32258 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -72,6 +72,51 @@ namespace { return SettingsStorage::instance(); } + + class FileStorageAdaptor final : public BitTorrent::AbstractFileStorage + { + public: + FileStorageAdaptor(const BitTorrent::TorrentInfo &torrentInfo, QStringList &filePaths) + : m_torrentInfo {torrentInfo} + , m_filePaths {filePaths} + { + Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); + } + + int filesCount() const override + { + return m_torrentInfo.filesCount(); + } + + qlonglong fileSize(const int index) const override + { + Q_ASSERT((index >= 0) && (index < filesCount())); + return m_torrentInfo.fileSize(index); + } + + QString filePath(const int index) const override + { + Q_ASSERT((index >= 0) && (index < filesCount())); + return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index)); + } + + void renameFile(const int index, const QString &newFilePath) override + { + Q_ASSERT((index >= 0) && (index < filesCount())); + const QString currentFilePath = filePath(index); + if (currentFilePath == newFilePath) + return; + + if (m_filePaths.isEmpty()) + m_filePaths = m_torrentInfo.filePaths(); + + m_filePaths[index] = newFilePath; + } + + private: + const BitTorrent::TorrentInfo &m_torrentInfo; + QStringList &m_filePaths; + }; } const int AddNewTorrentDialog::minPathHistoryLength; @@ -146,10 +191,8 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP // Signal / slots connect(m_ui->doNotDeleteTorrentCheckBox, &QCheckBox::clicked, this, &AddNewTorrentDialog::doNotDeleteTorrentClicked); QShortcut *editHotkey = new QShortcut(Qt::Key_F2, m_ui->contentTreeView, nullptr, nullptr, Qt::WidgetShortcut); - connect(editHotkey, &QShortcut::activated - , this, [this]() { m_ui->contentTreeView->renameSelectedFile(m_torrentInfo); }); - connect(m_ui->contentTreeView, &QAbstractItemView::doubleClicked - , this, [this]() { m_ui->contentTreeView->renameSelectedFile(m_torrentInfo); }); + connect(editHotkey, &QShortcut::activated, this, &AddNewTorrentDialog::renameSelectedFile); + connect(m_ui->contentTreeView, &QAbstractItemView::doubleClicked, this, &AddNewTorrentDialog::renameSelectedFile); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus(); } @@ -251,7 +294,6 @@ bool AddNewTorrentDialog::loadTorrentFile(const QString &torrentPath) : torrentPath; const nonstd::expected result = BitTorrent::TorrentInfo::loadFromFile(decodedPath); - m_torrentInfo = result.value_or(BitTorrent::TorrentInfo()); if (!result) { RaisedMessageBox::critical(this, tr("Invalid torrent") @@ -260,6 +302,7 @@ bool AddNewTorrentDialog::loadTorrentFile(const QString &torrentPath) return false; } + m_torrentInfo = result.value(); m_torrentGuard = std::make_unique(decodedPath); return loadTorrentImpl(); @@ -267,7 +310,6 @@ bool AddNewTorrentDialog::loadTorrentFile(const QString &torrentPath) bool AddNewTorrentDialog::loadTorrentImpl() { - m_hasMetadata = true; const auto torrentID = BitTorrent::TorrentID::fromInfoHash(m_torrentInfo.infoHash()); // Prevent showing the dialog if download is already present @@ -398,7 +440,7 @@ void AddNewTorrentDialog::updateDiskSpaceLabel() // Determine torrent size qlonglong torrentSize = 0; - if (m_hasMetadata) + if (hasMetadata()) { if (m_contentModel) { @@ -457,7 +499,7 @@ void AddNewTorrentDialog::setSavePath(const QString &newPath) void AddNewTorrentDialog::saveTorrentFile() { - Q_ASSERT(m_hasMetadata); + Q_ASSERT(hasMetadata()); const QString torrentFileExtension {C_TORRENT_FILE_EXTENSION}; const QString filter {tr("Torrent file (*%1)").arg(torrentFileExtension)}; @@ -479,6 +521,11 @@ void AddNewTorrentDialog::saveTorrentFile() } } +bool AddNewTorrentDialog::hasMetadata() const +{ + return m_torrentInfo.isValid(); +} + void AddNewTorrentDialog::populateSavePathComboBox() { m_ui->savePath->clear(); @@ -548,8 +595,7 @@ void AddNewTorrentDialog::displayContentTreeMenu(const QPoint &) menu->setAttribute(Qt::WA_DeleteOnClose); if (selectedRows.size() == 1) { - menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename...") - , this, [this]() { m_ui->contentTreeView->renameSelectedFile(m_torrentInfo); }); + menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename..."), this, &AddNewTorrentDialog::renameSelectedFile); menu->addSeparator(); QMenu *priorityMenu = menu->addMenu(tr("Priority")); @@ -634,7 +680,7 @@ void AddNewTorrentDialog::accept() setEnabled(!m_ui->checkBoxNeverShow->isChecked()); // Add torrent - if (!m_hasMetadata) + if (!hasMetadata()) BitTorrent::Session::instance()->addTorrent(m_magnetURI, m_torrentParams); else BitTorrent::Session::instance()->addTorrent(m_torrentInfo, m_torrentParams); @@ -645,7 +691,7 @@ void AddNewTorrentDialog::accept() void AddNewTorrentDialog::reject() { - if (!m_hasMetadata) + if (!hasMetadata()) { setMetadataProgressIndicator(false); BitTorrent::Session::instance()->cancelDownloadMetadata(m_magnetURI.infoHash().toTorrentID()); @@ -660,16 +706,8 @@ void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata disconnect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &AddNewTorrentDialog::updateMetadata); - if (!metadata.isValid()) - { - RaisedMessageBox::critical(this, tr("I/O Error"), ("Invalid metadata.")); - setMetadataProgressIndicator(false, tr("Invalid metadata")); - return; - } - // Good to go m_torrentInfo = metadata; - m_hasMetadata = true; setMetadataProgressIndicator(true, tr("Parsing metadata...")); // Update UI @@ -694,7 +732,7 @@ void AddNewTorrentDialog::setMetadataProgressIndicator(bool visibleIndicator, co void AddNewTorrentDialog::setupTreeview() { - if (!m_hasMetadata) + if (!hasMetadata()) { m_ui->labelCommentData->setText(tr("Not Available", "This comment is unavailable")); m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable")); @@ -719,7 +757,7 @@ void AddNewTorrentDialog::setupTreeview() connect(m_ui->contentTreeView, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::displayContentTreeMenu); // List files in torrent - m_contentModel->model()->setupModelData(m_torrentInfo); + m_contentModel->model()->setupModelData(FileStorageAdaptor(m_torrentInfo, m_torrentParams.filePaths)); if (const QByteArray state = m_storeTreeHeaderState; !state.isEmpty()) m_ui->contentTreeView->header()->restoreState(state); @@ -747,7 +785,6 @@ void AddNewTorrentDialog::handleDownloadFinished(const Net::DownloadResult &down case Net::DownloadStatus::Success: { const nonstd::expected result = BitTorrent::TorrentInfo::load(downloadResult.data); - m_torrentInfo = result.value_or(BitTorrent::TorrentInfo()); if (!result) { RaisedMessageBox::critical(this, tr("Invalid torrent"), tr("Failed to load from URL: %1.\nError: %2") @@ -755,6 +792,7 @@ void AddNewTorrentDialog::handleDownloadFinished(const Net::DownloadResult &down return; } + m_torrentInfo = result.value(); m_torrentGuard = std::make_unique(); if (loadTorrentImpl()) @@ -800,3 +838,12 @@ void AddNewTorrentDialog::doNotDeleteTorrentClicked(bool checked) { m_torrentGuard->setAutoRemove(!checked); } + +void AddNewTorrentDialog::renameSelectedFile() +{ + if (hasMetadata()) + { + FileStorageAdaptor fileStorageAdaptor {m_torrentInfo, m_torrentParams.filePaths}; + m_ui->contentTreeView->renameSelectedFile(fileStorageAdaptor); + } +} diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h index 0fb98dacd..1965a643e 100644 --- a/src/gui/addnewtorrentdialog.h +++ b/src/gui/addnewtorrentdialog.h @@ -86,6 +86,7 @@ private slots: void TMMChanged(int index); void categoryChanged(int index); void doNotDeleteTorrentClicked(bool checked); + void renameSelectedFile(); void accept() override; void reject() override; @@ -104,13 +105,13 @@ private: void setupTreeview(); void setSavePath(const QString &newPath); void saveTorrentFile(); + bool hasMetadata() const; void showEvent(QShowEvent *event) override; Ui::AddNewTorrentDialog *m_ui; TorrentContentFilterModel *m_contentModel = nullptr; PropListDelegate *m_contentDelegate = nullptr; - bool m_hasMetadata = false; BitTorrent::MagnetUri m_magnetURI; BitTorrent::TorrentInfo m_torrentInfo; int m_oldIndex = 0; diff --git a/src/gui/properties/peerlistwidget.cpp b/src/gui/properties/peerlistwidget.cpp index d6b81bb50..d8572bee4 100644 --- a/src/gui/properties/peerlistwidget.cpp +++ b/src/gui/properties/peerlistwidget.cpp @@ -471,7 +471,9 @@ void PeerListWidget::updatePeer(const BitTorrent::Torrent *torrent, const BitTor setModelData(row, PeerListColumns::TOT_UP, totalUp, peer.totalUpload(), intDataTextAlignment); setModelData(row, PeerListColumns::RELEVANCE, (Utils::String::fromDouble(peer.relevance() * 100, 1) + '%'), peer.relevance(), intDataTextAlignment); - const QStringList downloadingFiles {torrent->info().filesForPiece(peer.downloadingPieceIndex())}; + const QStringList downloadingFiles {torrent->hasMetadata() + ? torrent->info().filesForPiece(peer.downloadingPieceIndex()) + : QStringList()}; const QString downloadingFilesDisplayValue = downloadingFiles.join(';'); setModelData(row, PeerListColumns::DOWNLOADING_PIECE, downloadingFilesDisplayValue, downloadingFilesDisplayValue, {}, downloadingFiles.join(QLatin1Char('\n'))); diff --git a/src/gui/properties/piecesbar.cpp b/src/gui/properties/piecesbar.cpp index fda9c8113..8a624b00e 100644 --- a/src/gui/properties/piecesbar.cpp +++ b/src/gui/properties/piecesbar.cpp @@ -256,13 +256,14 @@ void PiecesBar::showToolTip(const QHelpEvent *e) const bool showDetailedInformation = QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier); if (showDetailedInformation && m_torrent->hasMetadata()) { + const BitTorrent::TorrentInfo torrentInfo = m_torrent->info(); const int imagePos = e->pos().x() - borderWidth; if ((imagePos >=0) && (imagePos < m_image.width())) { stream << ""; - PieceIndexToImagePos transform {m_torrent->info(), m_image}; + PieceIndexToImagePos transform {torrentInfo, m_image}; int pieceIndex = transform.pieceIndex(imagePos); - const QVector files {m_torrent->info().fileIndicesForPiece(pieceIndex)}; + const QVector files {torrentInfo.fileIndicesForPiece(pieceIndex)}; QString tooltipTitle; if (files.count() > 1) @@ -271,7 +272,7 @@ void PiecesBar::showToolTip(const QHelpEvent *e) } else { - if (m_torrent->info().fileSize(files.front()) == m_torrent->info().pieceLength(pieceIndex)) + if (torrentInfo.fileSize(files.front()) == torrentInfo.pieceLength(pieceIndex)) tooltipTitle = tr("File in this piece"); else tooltipTitle = tr("File in these pieces"); @@ -281,8 +282,8 @@ void PiecesBar::showToolTip(const QHelpEvent *e) for (int f : files) { - const QString filePath {m_torrent->info().filePath(f)}; - renderer(Utils::Misc::friendlyUnit(m_torrent->info().fileSize(f)), filePath); + const QString filePath {torrentInfo.filePath(f)}; + renderer(Utils::Misc::friendlyUnit(torrentInfo.fileSize(f)), filePath); } stream << ""; } @@ -306,13 +307,14 @@ void PiecesBar::highlightFile(int imagePos) if (!m_torrent || !m_torrent->hasMetadata() || (imagePos < 0) || (imagePos >= m_image.width())) return; - PieceIndexToImagePos transform {m_torrent->info(), m_image}; + const BitTorrent::TorrentInfo torrentInfo = m_torrent->info(); + PieceIndexToImagePos transform {torrentInfo, m_image}; int pieceIndex = transform.pieceIndex(imagePos); - QVector fileIndices {m_torrent->info().fileIndicesForPiece(pieceIndex)}; + QVector fileIndices {torrentInfo.fileIndicesForPiece(pieceIndex)}; if (fileIndices.count() == 1) { - BitTorrent::TorrentInfo::PieceRange filePieces = m_torrent->info().filePieces(fileIndices.first()); + BitTorrent::TorrentInfo::PieceRange filePieces = torrentInfo.filePieces(fileIndices.first()); ImageRange imageRange = transform.imagePos(filePieces); QRect newHighlightedRegion {imageRange.first(), 0, imageRange.size(), m_image.height()}; diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index bbd26894a..573ad90b3 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -528,7 +528,7 @@ void PropertiesWidget::loadDynamicData() if (!isContentInitialized) { // List files in torrent - m_propListModel->model()->setupModelData(m_torrent->info()); + m_propListModel->model()->setupModelData(*m_torrent); // Load file priorities m_propListModel->model()->updateFilesPriorities(m_torrent->filePriorities()); // Update file progress/availability diff --git a/src/gui/torrentcontentmodel.cpp b/src/gui/torrentcontentmodel.cpp index ce3132be3..1eb0176af 100644 --- a/src/gui/torrentcontentmodel.cpp +++ b/src/gui/torrentcontentmodel.cpp @@ -50,8 +50,8 @@ #include #endif +#include "base/bittorrent/abstractfilestorage.h" #include "base/bittorrent/downloadpriority.h" -#include "base/bittorrent/torrentinfo.h" #include "base/global.h" #include "base/utils/fs.h" #include "torrentcontentmodelfile.h" @@ -485,7 +485,7 @@ void TorrentContentModel::clear() endResetModel(); } -void TorrentContentModel::setupModelData(const BitTorrent::TorrentInfo &info) +void TorrentContentModel::setupModelData(const BitTorrent::AbstractFileStorage &info) { qDebug("setup model data called"); const int filesCount = info.filesCount(); diff --git a/src/gui/torrentcontentmodel.h b/src/gui/torrentcontentmodel.h index 66c816520..10764957a 100644 --- a/src/gui/torrentcontentmodel.h +++ b/src/gui/torrentcontentmodel.h @@ -41,7 +41,7 @@ class TorrentContentModelFile; namespace BitTorrent { - class TorrentInfo; + class AbstractFileStorage; } class TorrentContentModel final : public QAbstractItemModel @@ -74,7 +74,7 @@ public: QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = {}) const override; void clear(); - void setupModelData(const BitTorrent::TorrentInfo &info); + void setupModelData(const BitTorrent::AbstractFileStorage &info); signals: void filteredFilesChanged(); diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index 2aac15741..106db64dd 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -573,10 +573,12 @@ void SyncController::torrentPeersAction() {KEY_PEER_CONNECTION_TYPE, pi.connectionType()}, {KEY_PEER_FLAGS, pi.flags()}, {KEY_PEER_FLAGS_DESCRIPTION, pi.flagsDescription()}, - {KEY_PEER_RELEVANCE, pi.relevance()}, - {KEY_PEER_FILES, torrent->info().filesForPiece(pi.downloadingPieceIndex()).join('\n')} + {KEY_PEER_RELEVANCE, pi.relevance()} }; + if (torrent->hasMetadata()) + peer.insert(KEY_PEER_FILES, torrent->info().filesForPiece(pi.downloadingPieceIndex()).join('\n')); + if (resolvePeerCountries) { peer[KEY_PEER_COUNTRY_CODE] = pi.country().toLower(); diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 65b579c67..27d376777 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -594,9 +594,12 @@ void TorrentsController::pieceHashesAction() throw APIError(APIErrorType::NotFound); QJsonArray pieceHashes; - const QVector hashes = torrent->info().pieceHashes(); - for (const QByteArray &hash : hashes) - pieceHashes.append(QString(hash.toHex())); + if (torrent->hasMetadata()) + { + const QVector hashes = torrent->info().pieceHashes(); + for (const QByteArray &hash : hashes) + pieceHashes.append(QString(hash.toHex())); + } setResult(pieceHashes); } From 6f8fae9a7b4f1aaa405182bd651a38b6bf6eec31 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Thu, 16 Dec 2021 12:50:16 +0300 Subject: [PATCH 2/2] Apply selected layout to displayed torrent content --- src/gui/addnewtorrentdialog.cpp | 35 +++++++++++++++++++++++++++++++++ src/gui/addnewtorrentdialog.h | 1 + 2 files changed, 36 insertions(+) diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 778a32258..e97fa0180 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -43,6 +43,7 @@ #include "base/bittorrent/magneturi.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrent.h" +#include "base/bittorrent/torrentcontentlayout.h" #include "base/global.h" #include "base/net/downloadmanager.h" #include "base/settingsstorage.h" @@ -164,6 +165,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP m_ui->contentLayoutComboBox->setCurrentIndex( static_cast(m_torrentParams.contentLayout.value_or(session->torrentContentLayout()))); + connect(m_ui->contentLayoutComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::contentLayoutChanged); m_ui->sequentialCheckBox->setChecked(m_torrentParams.sequential); m_ui->firstLastCheckBox->setChecked(m_torrentParams.firstLastPiecePriority); @@ -484,6 +486,31 @@ void AddNewTorrentDialog::categoryChanged(int index) } } +void AddNewTorrentDialog::contentLayoutChanged(const int index) +{ + if (!hasMetadata()) + return; + + const auto filePriorities = m_contentModel->model()->getFilePriorities(); + m_contentModel->model()->clear(); + + Q_ASSERT(!m_torrentParams.filePaths.isEmpty()); + const auto contentLayout = ((index == 0) + ? BitTorrent::detectContentLayout(m_torrentInfo.filePaths()) + : static_cast(index)); + BitTorrent::applyContentLayout(m_torrentParams.filePaths, contentLayout, Utils::Fs::findRootFolder(m_torrentInfo.filePaths())); + m_contentModel->model()->setupModelData(FileStorageAdaptor(m_torrentInfo, m_torrentParams.filePaths)); + m_contentModel->model()->updateFilesPriorities(filePriorities); + + // Expand single-item folders recursively + QModelIndex currentIndex; + while (m_contentModel->rowCount(currentIndex) == 1) + { + currentIndex = m_contentModel->index(0, 0, currentIndex); + m_ui->contentTreeView->setExpanded(currentIndex, true); + } +} + void AddNewTorrentDialog::setSavePath(const QString &newPath) { int existingIndex = indexOfSavePath(newPath); @@ -702,6 +729,8 @@ void AddNewTorrentDialog::reject() void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata) { + Q_ASSERT(metadata.isValid()); + if (metadata.infoHash() != m_magnetURI.infoHash()) return; disconnect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &AddNewTorrentDialog::updateMetadata); @@ -756,6 +785,12 @@ void AddNewTorrentDialog::setupTreeview() , qOverload(&QAbstractItemView::edit)); connect(m_ui->contentTreeView, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::displayContentTreeMenu); + const auto contentLayout = ((m_ui->contentLayoutComboBox->currentIndex() == 0) + ? BitTorrent::detectContentLayout(m_torrentInfo.filePaths()) + : static_cast(m_ui->contentLayoutComboBox->currentIndex())); + if (m_torrentParams.filePaths.isEmpty()) + m_torrentParams.filePaths = m_torrentInfo.filePaths(); + BitTorrent::applyContentLayout(m_torrentParams.filePaths, contentLayout, Utils::Fs::findRootFolder(m_torrentInfo.filePaths())); // List files in torrent m_contentModel->model()->setupModelData(FileStorageAdaptor(m_torrentInfo, m_torrentParams.filePaths)); if (const QByteArray state = m_storeTreeHeaderState; !state.isEmpty()) diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h index 1965a643e..4df53b491 100644 --- a/src/gui/addnewtorrentdialog.h +++ b/src/gui/addnewtorrentdialog.h @@ -85,6 +85,7 @@ private slots: void handleDownloadFinished(const Net::DownloadResult &downloadResult); void TMMChanged(int index); void categoryChanged(int index); + void contentLayoutChanged(int index); void doNotDeleteTorrentClicked(bool checked); void renameSelectedFile();