diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index d764871d7..38154e98a 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -38,6 +38,8 @@ add_library(qbt_base STATIC bittorrent/torrent.h bittorrent/torrentcontenthandler.h bittorrent/torrentcontentlayout.h + bittorrent/torrentcontentremoveoption.h + bittorrent/torrentcontentremover.h bittorrent/torrentcreationmanager.h bittorrent/torrentcreationtask.h bittorrent/torrentcreator.h @@ -145,6 +147,7 @@ add_library(qbt_base STATIC bittorrent/sslparameters.cpp bittorrent/torrent.cpp bittorrent/torrentcontenthandler.cpp + bittorrent/torrentcontentremover.cpp bittorrent/torrentcreationmanager.cpp bittorrent/torrentcreationtask.cpp bittorrent/torrentcreator.cpp diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index a5b1bc7b4..96f4816fd 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -37,17 +37,12 @@ #include "addtorrentparams.h" #include "categoryoptions.h" #include "sharelimitaction.h" +#include "torrentcontentremoveoption.h" #include "trackerentry.h" #include "trackerentrystatus.h" class QString; -enum DeleteOption -{ - DeleteTorrent, - DeleteTorrentAndFiles -}; - namespace BitTorrent { class InfoHash; @@ -58,6 +53,12 @@ namespace BitTorrent struct CacheStatus; struct SessionStatus; + enum class TorrentRemoveOption + { + KeepContent, + RemoveContent + }; + // Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised // since `Q_NAMESPACE` cannot be used when the same namespace resides at different files. // https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779 @@ -434,6 +435,8 @@ namespace BitTorrent virtual void setMergeTrackersEnabled(bool enabled) = 0; virtual bool isStartPaused() const = 0; virtual void setStartPaused(bool value) = 0; + virtual TorrentContentRemoveOption torrentContentRemoveOption() const = 0; + virtual void setTorrentContentRemoveOption(TorrentContentRemoveOption option) = 0; virtual bool isRestored() const = 0; @@ -453,7 +456,7 @@ namespace BitTorrent virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0; virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) = 0; - virtual bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteOption::DeleteTorrent) = 0; + virtual bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) = 0; virtual bool downloadMetadata(const TorrentDescriptor &torrentDescr) = 0; virtual bool cancelDownloadMetadata(const TorrentID &id) = 0; diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index f621c9d6a..8c21682e8 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -101,6 +101,7 @@ #include "nativesessionextension.h" #include "portforwarderimpl.h" #include "resumedatastorage.h" +#include "torrentcontentremover.h" #include "torrentdescriptor.h" #include "torrentimpl.h" #include "tracker.h" @@ -525,6 +526,7 @@ SessionImpl::SessionImpl(QObject *parent) , m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3} , m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3} , m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3} + , m_torrentContentRemoveOption {BITTORRENT_SESSION_KEY(u"TorrentContentRemoveOption"_s), TorrentContentRemoveOption::MoveToTrash} , m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)} , m_seedingLimitTimer {new QTimer(this)} , m_resumeDataTimer {new QTimer(this)} @@ -593,6 +595,11 @@ SessionImpl::SessionImpl(QObject *parent) connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater); connect(m_fileSearcher, &FileSearcher::searchFinished, this, &SessionImpl::fileSearchFinished); + m_torrentContentRemover = new TorrentContentRemover; + m_torrentContentRemover->moveToThread(m_ioThread.get()); + connect(m_ioThread.get(), &QThread::finished, m_torrentContentRemover, &QObject::deleteLater); + connect(m_torrentContentRemover, &TorrentContentRemover::jobFinished, this, &SessionImpl::torrentContentRemovingFinished); + m_ioThread->start(); initMetrics(); @@ -2287,12 +2294,12 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent) if (shareLimitAction == ShareLimitAction::Remove) { LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName)); - deleteTorrent(torrent->id()); + removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent); } else if (shareLimitAction == ShareLimitAction::RemoveWithContent) { LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName)); - deleteTorrent(torrent->id(), DeleteTorrentAndFiles); + removeTorrent(torrent->id(), TorrentRemoveOption::RemoveContent); } else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped()) { @@ -2332,6 +2339,19 @@ void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath, } } +void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage) +{ + if (errorMessage.isEmpty()) + { + LogMsg(tr("Torrent content removed. Torrent: \"%1\"").arg(torrentName)); + } + else + { + LogMsg(tr("Failed to remove torrent content. Torrent: \"%1\". Error: \"%2\"") + .arg(torrentName, errorMessage), Log::WARNING); + } +} + Torrent *SessionImpl::getTorrent(const TorrentID &id) const { return m_torrents.value(id); @@ -2378,26 +2398,29 @@ void SessionImpl::banIP(const QString &ip) // Delete a torrent from the session, given its hash // and from the disk, if the corresponding deleteOption is chosen -bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption) +bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption deleteOption) { TorrentImpl *const torrent = m_torrents.take(id); if (!torrent) return false; - qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrent->id().toString())); + const TorrentID torrentID = torrent->id(); + const QString torrentName = torrent->name(); + + qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrentID.toString())); emit torrentAboutToBeRemoved(torrent); if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid()) m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1())); // Remove it from session - if (deleteOption == DeleteTorrent) + if (deleteOption == TorrentRemoveOption::KeepContent) { - m_removingTorrents[torrent->id()] = {torrent->name(), {}, deleteOption}; + m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), {}, deleteOption}; const lt::torrent_handle nativeHandle {torrent->nativeHandle()}; const auto iter = std::find_if(m_moveStorageQueue.begin(), m_moveStorageQueue.end() - , [&nativeHandle](const MoveStorageJob &job) + , [&nativeHandle](const MoveStorageJob &job) { return job.torrentHandle == nativeHandle; }); @@ -2415,14 +2438,14 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp } else { - m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption}; + m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), torrent->actualFilePaths(), deleteOption}; if (m_moveStorageQueue.size() > 1) { // Delete "move storage job" for the deleted torrent // (note: we shouldn't delete active job) const auto iter = std::find_if((m_moveStorageQueue.begin() + 1), m_moveStorageQueue.end() - , [torrent](const MoveStorageJob &job) + , [torrent](const MoveStorageJob &job) { return job.torrentHandle == torrent->nativeHandle(); }); @@ -2430,12 +2453,13 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp m_moveStorageQueue.erase(iter); } - m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_files); + m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_partfile); } // Remove it from torrent resume directory - m_resumeDataStorage->remove(torrent->id()); + m_resumeDataStorage->remove(torrentID); + LogMsg(tr("Torrent removed. Torrent: \"%1\"").arg(torrentName)); delete torrent; return true; } @@ -2463,7 +2487,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id) } #endif - m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files); + m_nativeSession->remove_torrent(nativeHandle); return true; } @@ -3974,6 +3998,16 @@ void SessionImpl::setStartPaused(const bool value) m_startPaused = value; } +TorrentContentRemoveOption SessionImpl::torrentContentRemoveOption() const +{ + return m_torrentContentRemoveOption; +} + +void SessionImpl::setTorrentContentRemoveOption(const TorrentContentRemoveOption option) +{ + m_torrentContentRemoveOption = option; +} + QStringList SessionImpl::bannedIPs() const { return m_bannedIPs; @@ -5147,7 +5181,7 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath) // Last job is completed for torrent that being removing, so actually remove it const lt::torrent_handle nativeHandle {finishedJob.torrentHandle}; const RemovingTorrentData &removingTorrentData = m_removingTorrents[nativeHandle.info_hash()]; - if (removingTorrentData.deleteOption == DeleteTorrent) + if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent) m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile); } } @@ -5666,74 +5700,32 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle, return torrent; } -void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert *alert) +void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/) { -#ifdef QBT_USES_LIBTORRENT2 - const auto id = TorrentID::fromInfoHash(alert->info_hashes); -#else - const auto id = TorrentID::fromInfoHash(alert->info_hash); -#endif - - const auto removingTorrentDataIter = m_removingTorrents.find(id); - if (removingTorrentDataIter != m_removingTorrents.end()) - { - if (removingTorrentDataIter->deleteOption == DeleteTorrent) - { - LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name)); - m_removingTorrents.erase(removingTorrentDataIter); - } - } + // We cannot consider `torrent_removed_alert` as a starting point for removing content, + // because it has an inconsistent posting time between different versions of libtorrent, + // so files may still be in use in some cases. } void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert) { #ifdef QBT_USES_LIBTORRENT2 - const auto id = TorrentID::fromInfoHash(alert->info_hashes); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes); #else - const auto id = TorrentID::fromInfoHash(alert->info_hash); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hash); #endif - - const auto removingTorrentDataIter = m_removingTorrents.find(id); - if (removingTorrentDataIter == m_removingTorrents.end()) - return; - - // torrent_deleted_alert can also be posted due to deletion of partfile. Ignore it in such a case. - if (removingTorrentDataIter->deleteOption == DeleteTorrent) - return; - - Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove); - LogMsg(tr("Removed torrent and deleted its content. Torrent: \"%1\"").arg(removingTorrentDataIter->name)); - m_removingTorrents.erase(removingTorrentDataIter); + handleRemovedTorrent(torrentID); } void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert) { #ifdef QBT_USES_LIBTORRENT2 - const auto id = TorrentID::fromInfoHash(alert->info_hashes); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes); #else - const auto id = TorrentID::fromInfoHash(alert->info_hash); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hash); #endif - - const auto removingTorrentDataIter = m_removingTorrents.find(id); - if (removingTorrentDataIter == m_removingTorrents.end()) - return; - - if (alert->error) - { - // libtorrent won't delete the directory if it contains files not listed in the torrent, - // so we remove the directory ourselves - Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove); - - LogMsg(tr("Removed torrent but failed to delete its content and/or partfile. Torrent: \"%1\". Error: \"%2\"") - .arg(removingTorrentDataIter->name, QString::fromLocal8Bit(alert->error.message().c_str())) - , Log::WARNING); - } - else // torrent without metadata, hence no files on disk - { - LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name)); - } - - m_removingTorrents.erase(removingTorrentDataIter); + const auto errorMessage = alert->error ? QString::fromLocal8Bit(alert->error.message().c_str()) : QString(); + handleRemovedTorrent(torrentID, errorMessage); } void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert) @@ -6169,7 +6161,7 @@ void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *a if (torrent2) { if (torrent1) - deleteTorrent(torrentIDv1); + removeTorrent(torrentIDv1); else cancelDownloadMetadata(torrentIDv1); @@ -6278,3 +6270,29 @@ void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, Q } }); } + +void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError) +{ + const auto removingTorrentDataIter = m_removingTorrents.find(torrentID); + if (removingTorrentDataIter == m_removingTorrents.end()) + return; + + if (!partfileRemoveError.isEmpty()) + { + LogMsg(tr("Failed to remove partfile. Torrent: \"%1\". Reason: \"%2\".") + .arg(removingTorrentDataIter->name, partfileRemoveError) + , Log::WARNING); + } + + if ((removingTorrentDataIter->removeOption == TorrentRemoveOption::RemoveContent) + && !removingTorrentDataIter->contentStoragePath.isEmpty()) + { + QMetaObject::invokeMethod(m_torrentContentRemover, [this, jobData = *removingTorrentDataIter] + { + m_torrentContentRemover->performJob(jobData.name, jobData.contentStoragePath + , jobData.fileNames, m_torrentContentRemoveOption); + }); + } + + m_removingTorrents.erase(removingTorrentDataIter); +} diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index 4acd624fe..a5bac0847 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -75,6 +75,7 @@ namespace BitTorrent class InfoHash; class ResumeDataStorage; class Torrent; + class TorrentContentRemover; class TorrentDescriptor; class TorrentImpl; class Tracker; @@ -411,6 +412,8 @@ namespace BitTorrent void setMergeTrackersEnabled(bool enabled) override; bool isStartPaused() const override; void setStartPaused(bool value) override; + TorrentContentRemoveOption torrentContentRemoveOption() const override; + void setTorrentContentRemoveOption(TorrentContentRemoveOption option) override; bool isRestored() const override; @@ -430,7 +433,7 @@ namespace BitTorrent bool isKnownTorrent(const InfoHash &infoHash) const override; bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) override; - bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteTorrent) override; + bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) override; bool downloadMetadata(const TorrentDescriptor &torrentDescr) override; bool cancelDownloadMetadata(const TorrentID &id) override; @@ -491,6 +494,7 @@ namespace BitTorrent void handleIPFilterParsed(int ruleCount); void handleIPFilterError(); void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames); + void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage); private: struct ResumeSessionContext; @@ -506,8 +510,9 @@ namespace BitTorrent struct RemovingTorrentData { QString name; - Path pathToRemove; - DeleteOption deleteOption {}; + Path contentStoragePath; + PathList fileNames; + TorrentRemoveOption removeOption {}; }; explicit SessionImpl(QObject *parent = nullptr); @@ -599,6 +604,8 @@ namespace BitTorrent void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash>> updatedTrackers); + void handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError = {}); + CachedSettingValue m_DHTBootstrapNodes; CachedSettingValue m_isDHTEnabled; CachedSettingValue m_isLSDEnabled; @@ -723,6 +730,7 @@ namespace BitTorrent CachedSettingValue m_I2POutboundQuantity; CachedSettingValue m_I2PInboundLength; CachedSettingValue m_I2POutboundLength; + CachedSettingValue m_torrentContentRemoveOption; SettingValue m_startPaused; lt::session *m_nativeSession = nullptr; @@ -765,6 +773,7 @@ namespace BitTorrent QThreadPool *m_asyncWorker = nullptr; ResumeDataStorage *m_resumeDataStorage = nullptr; FileSearcher *m_fileSearcher = nullptr; + TorrentContentRemover *m_torrentContentRemover = nullptr; QHash m_downloadedMetadata; diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index 34cccd4ff..2dcffdf85 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -228,6 +228,7 @@ namespace BitTorrent virtual void setShareLimitAction(ShareLimitAction action) = 0; virtual PathList filePaths() const = 0; + virtual PathList actualFilePaths() const = 0; virtual TorrentInfo info() const = 0; virtual bool isFinished() const = 0; diff --git a/src/base/bittorrent/torrentcontentremoveoption.h b/src/base/bittorrent/torrentcontentremoveoption.h new file mode 100644 index 000000000..1083e5ec0 --- /dev/null +++ b/src/base/bittorrent/torrentcontentremoveoption.h @@ -0,0 +1,50 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 + +namespace BitTorrent +{ + // Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised + // since `Q_NAMESPACE` cannot be used when the same namespace resides at different files. + // https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779 + inline namespace TorrentContentRemoveOptionNS + { + Q_NAMESPACE + + enum class TorrentContentRemoveOption + { + Delete, + MoveToTrash + }; + + Q_ENUM_NS(TorrentContentRemoveOption) + } +} diff --git a/src/base/bittorrent/torrentcontentremover.cpp b/src/base/bittorrent/torrentcontentremover.cpp new file mode 100644 index 000000000..861f4be96 --- /dev/null +++ b/src/base/bittorrent/torrentcontentremover.cpp @@ -0,0 +1,61 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 "torrentcontentremover.h" + +#include "base/utils/fs.h" + +void BitTorrent::TorrentContentRemover::performJob(const QString &torrentName, const Path &basePath + , const PathList &fileNames, const TorrentContentRemoveOption option) +{ + QString errorMessage; + + if (!fileNames.isEmpty()) + { + const auto removeFileFn = [&option](const Path &filePath) + { + return ((option == TorrentContentRemoveOption::MoveToTrash) + ? Utils::Fs::moveFileToTrash : Utils::Fs::removeFile)(filePath); + }; + + for (const Path &fileName : fileNames) + { + if (const auto result = removeFileFn(basePath / fileName) + ; !result && errorMessage.isEmpty()) + { + errorMessage = result.error(); + } + } + + const Path rootPath = Path::findRootFolder(fileNames); + if (!rootPath.isEmpty()) + Utils::Fs::smartRemoveEmptyFolderTree(basePath / rootPath); + } + + emit jobFinished(torrentName, errorMessage); +} diff --git a/src/base/bittorrent/torrentcontentremover.h b/src/base/bittorrent/torrentcontentremover.h new file mode 100644 index 000000000..a18161493 --- /dev/null +++ b/src/base/bittorrent/torrentcontentremover.h @@ -0,0 +1,53 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 "base/path.h" +#include "torrentcontentremoveoption.h" + +namespace BitTorrent +{ + class TorrentContentRemover final : public QObject + { + Q_OBJECT + Q_DISABLE_COPY_MOVE(TorrentContentRemover) + + public: + using QObject::QObject; + + public slots: + void performJob(const QString &torrentName, const Path &basePath + , const PathList &fileNames, TorrentContentRemoveOption option); + + signals: + void jobFinished(const QString &torrentName, const QString &errorMessage); + }; +} diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index d4638e3c8..95fcccf94 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -986,6 +986,21 @@ PathList TorrentImpl::filePaths() const return m_filePaths; } +PathList TorrentImpl::actualFilePaths() const +{ + if (!hasMetadata()) + return {}; + + PathList paths; + paths.reserve(filesCount()); + + const lt::file_storage files = nativeTorrentInfo()->files(); + for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes())) + paths.emplaceBack(files.file_path(nativeIndex)); + + return paths; +} + QVector TorrentImpl::filePriorities() const { return m_filePriorities; diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index 54b84c1f4..ffc83b202 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -153,6 +153,7 @@ namespace BitTorrent Path actualFilePath(int index) const override; qlonglong fileSize(int index) const override; PathList filePaths() const override; + PathList actualFilePaths() const override; QVector filePriorities() const override; TorrentInfo info() const override; diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 13c1c8b00..d97390cbb 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -134,17 +134,17 @@ void Preferences::setCustomUIThemePath(const Path &path) setValue(u"Preferences/General/CustomUIThemePath"_s, path); } -bool Preferences::deleteTorrentFilesAsDefault() const +bool Preferences::removeTorrentContent() const { return value(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, false); } -void Preferences::setDeleteTorrentFilesAsDefault(const bool del) +void Preferences::setRemoveTorrentContent(const bool remove) { - if (del == deleteTorrentFilesAsDefault()) + if (remove == removeTorrentContent()) return; - setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, del); + setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, remove); } bool Preferences::confirmOnExit() const diff --git a/src/base/preferences.h b/src/base/preferences.h index 90f26b055..fcc70bb97 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -105,8 +105,8 @@ public: void setUseCustomUITheme(bool use); Path customUIThemePath() const; void setCustomUIThemePath(const Path &path); - bool deleteTorrentFilesAsDefault() const; - void setDeleteTorrentFilesAsDefault(bool del); + bool removeTorrentContent() const; + void setRemoveTorrentContent(bool remove); bool confirmOnExit() const; void setConfirmOnExit(bool confirm); bool speedInTitleBar() const; diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index 4d89f49a0..dbcfafb74 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -29,8 +29,6 @@ #include "fs.h" -#include -#include #include #if defined(Q_OS_WIN) @@ -52,6 +50,7 @@ #include #endif +#include #include #include #include @@ -311,20 +310,42 @@ bool Utils::Fs::renameFile(const Path &from, const Path &to) * * This function will try to fix the file permissions before removing it. */ -bool Utils::Fs::removeFile(const Path &path) +nonstd::expected Utils::Fs::removeFile(const Path &path) { - if (QFile::remove(path.data())) - return true; - QFile file {path.data()}; + if (file.remove()) + return {}; + if (!file.exists()) - return true; + return {}; // Make sure we have read/write permissions file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); - return file.remove(); + if (file.remove()) + return {}; + + return nonstd::make_unexpected(file.errorString()); } +nonstd::expected Utils::Fs::moveFileToTrash(const Path &path) +{ + QFile file {path.data()}; + if (file.moveToTrash()) + return {}; + + if (!file.exists()) + return {}; + + // Make sure we have read/write permissions + file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); + if (file.moveToTrash()) + return {}; + + const QString errorMessage = file.errorString(); + return nonstd::make_unexpected(!errorMessage.isEmpty() ? errorMessage : QCoreApplication::translate("fs", "Unknown error")); +} + + bool Utils::Fs::isReadable(const Path &path) { return QFileInfo(path.data()).isReadable(); diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index 3dccb1d2d..eced8b072 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -35,6 +35,7 @@ #include +#include "base/3rdparty/expected.hpp" #include "base/global.h" #include "base/pathfwd.h" @@ -60,7 +61,8 @@ namespace Utils::Fs bool copyFile(const Path &from, const Path &to); bool renameFile(const Path &from, const Path &to); - bool removeFile(const Path &path); + nonstd::expected removeFile(const Path &path); + nonstd::expected moveFileToTrash(const Path &path); bool mkdir(const Path &dirPath); bool mkpath(const Path &dirPath); bool rmdir(const Path &dirPath); diff --git a/src/gui/advancedsettings.cpp b/src/gui/advancedsettings.cpp index d81cfecc6..15133c222 100644 --- a/src/gui/advancedsettings.cpp +++ b/src/gui/advancedsettings.cpp @@ -63,6 +63,7 @@ namespace // qBittorrent section QBITTORRENT_HEADER, RESUME_DATA_STORAGE, + TORRENT_CONTENT_REMOVE_OPTION, #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) MEMORY_WORKING_SET_LIMIT, #endif @@ -364,6 +365,8 @@ void AdvancedSettings::saveAdvancedSettings() const session->setI2PInboundLength(m_spinBoxI2PInboundLength.value()); session->setI2POutboundLength(m_spinBoxI2POutboundLength.value()); #endif + + session->setTorrentContentRemoveOption(m_comboBoxTorrentContentRemoveOption.currentData().value()); } #ifndef QBT_USES_LIBTORRENT2 @@ -472,6 +475,11 @@ void AdvancedSettings::loadAdvancedSettings() m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType()))); addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage); + m_comboBoxTorrentContentRemoveOption.addItem(tr("Delete files permanently"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::Delete)); + m_comboBoxTorrentContentRemoveOption.addItem(tr("Move files to trash (if possible)"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::MoveToTrash)); + m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption()))); + addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption); + #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) // Physical memory (RAM) usage limit m_spinBoxMemoryWorkingSetLimit.setMinimum(1); diff --git a/src/gui/advancedsettings.h b/src/gui/advancedsettings.h index 386a44d10..c1f7b9a66 100644 --- a/src/gui/advancedsettings.h +++ b/src/gui/advancedsettings.h @@ -81,7 +81,7 @@ private: m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity, m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused; QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxDiskIOReadMode, m_comboBoxDiskIOWriteMode, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm, - m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage; + m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage, m_comboBoxTorrentContentRemoveOption; QLineEdit m_lineEditAppInstanceName, m_pythonExecutablePath, m_lineEditAnnounceIP, m_lineEditDHTBootstrapNodes; #ifndef QBT_USES_LIBTORRENT2 diff --git a/src/gui/deletionconfirmationdialog.cpp b/src/gui/deletionconfirmationdialog.cpp index e667a4ad3..9f5fe5452 100644 --- a/src/gui/deletionconfirmationdialog.cpp +++ b/src/gui/deletionconfirmationdialog.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -30,6 +31,7 @@ #include +#include "base/bittorrent/session.h" #include "base/global.h" #include "base/preferences.h" #include "uithememanager.h" @@ -53,8 +55,8 @@ DeletionConfirmationDialog::DeletionConfirmationDialog(QWidget *parent, const in m_ui->rememberBtn->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s)); m_ui->rememberBtn->setIconSize(Utils::Gui::mediumIconSize()); - m_ui->checkPermDelete->setChecked(defaultDeleteFiles || Preferences::instance()->deleteTorrentFilesAsDefault()); - connect(m_ui->checkPermDelete, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState); + m_ui->checkRemoveContent->setChecked(defaultDeleteFiles || Preferences::instance()->removeTorrentContent()); + connect(m_ui->checkRemoveContent, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Remove")); m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); @@ -67,18 +69,18 @@ DeletionConfirmationDialog::~DeletionConfirmationDialog() delete m_ui; } -bool DeletionConfirmationDialog::isDeleteFileSelected() const +bool DeletionConfirmationDialog::isRemoveContentSelected() const { - return m_ui->checkPermDelete->isChecked(); + return m_ui->checkRemoveContent->isChecked(); } void DeletionConfirmationDialog::updateRememberButtonState() { - m_ui->rememberBtn->setEnabled(m_ui->checkPermDelete->isChecked() != Preferences::instance()->deleteTorrentFilesAsDefault()); + m_ui->rememberBtn->setEnabled(m_ui->checkRemoveContent->isChecked() != Preferences::instance()->removeTorrentContent()); } void DeletionConfirmationDialog::on_rememberBtn_clicked() { - Preferences::instance()->setDeleteTorrentFilesAsDefault(m_ui->checkPermDelete->isChecked()); + Preferences::instance()->setRemoveTorrentContent(m_ui->checkRemoveContent->isChecked()); m_ui->rememberBtn->setEnabled(false); } diff --git a/src/gui/deletionconfirmationdialog.h b/src/gui/deletionconfirmationdialog.h index f93f1746b..9ff657849 100644 --- a/src/gui/deletionconfirmationdialog.h +++ b/src/gui/deletionconfirmationdialog.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -37,16 +38,16 @@ namespace Ui class DeletionConfirmationDialog; } -class DeletionConfirmationDialog : public QDialog +class DeletionConfirmationDialog final : public QDialog { Q_OBJECT Q_DISABLE_COPY_MOVE(DeletionConfirmationDialog) public: DeletionConfirmationDialog(QWidget *parent, int size, const QString &name, bool defaultDeleteFiles); - ~DeletionConfirmationDialog(); + ~DeletionConfirmationDialog() override; - bool isDeleteFileSelected() const; + bool isRemoveContentSelected() const; private slots: void updateRememberButtonState(); diff --git a/src/gui/deletionconfirmationdialog.ui b/src/gui/deletionconfirmationdialog.ui index 9912e2b44..56834c918 100644 --- a/src/gui/deletionconfirmationdialog.ui +++ b/src/gui/deletionconfirmationdialog.ui @@ -75,7 +75,7 @@ - + 0 @@ -88,7 +88,7 @@ - Also permanently delete the files + Also remove the content files diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index a1b79b29e..67b6b6cd7 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.cpp @@ -116,9 +116,10 @@ namespace void removeTorrents(const QVector &torrents, const bool isDeleteFileSelected) { auto *session = BitTorrent::Session::instance(); - const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent; + const BitTorrent::TorrentRemoveOption removeOption = isDeleteFileSelected + ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent; for (const BitTorrent::Torrent *torrent : torrents) - session->deleteTorrent(torrent->id(), deleteOption); + session->removeTorrent(torrent->id(), removeOption); } } @@ -442,7 +443,7 @@ void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles) { // Some torrents might be removed when waiting for user input, so refetch the torrent list // NOTE: this will only work when dialog is modal - removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected()); + removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected()); }); dialog->open(); } @@ -465,7 +466,7 @@ void TransferListWidget::deleteVisibleTorrents() { // Some torrents might be removed when waiting for user input, so refetch the torrent list // NOTE: this will only work when dialog is modal - removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected()); + removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected()); }); dialog->open(); } diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index db51b4cd3..2d216ff92 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -136,7 +136,7 @@ void AppController::preferencesAction() data[u"file_log_age"_s] = app()->fileLoggerAge(); data[u"file_log_age_type"_s] = app()->fileLoggerAgeType(); // Delete torrent contents files on torrent removal - data[u"delete_torrent_content_files"_s] = pref->deleteTorrentFilesAsDefault(); + data[u"delete_torrent_content_files"_s] = pref->removeTorrentContent(); // Downloads // When adding a torrent @@ -350,6 +350,8 @@ void AppController::preferencesAction() // qBitorrent preferences // Resume data storage type data[u"resume_data_storage_type"_s] = Utils::String::fromEnum(session->resumeDataStorageType()); + // Torrent content removing mode + data[u"torrent_content_remove_option"_s] = Utils::String::fromEnum(session->torrentContentRemoveOption()); // Physical memory (RAM) usage limit data[u"memory_working_set_limit"_s] = app()->memoryWorkingSetLimit(); // Current network interface @@ -519,7 +521,7 @@ void AppController::setPreferencesAction() app()->setFileLoggerAgeType(it.value().toInt()); // Delete torrent content files on torrent removal if (hasKey(u"delete_torrent_content_files"_s)) - pref->setDeleteTorrentFilesAsDefault(it.value().toBool()); + pref->setRemoveTorrentContent(it.value().toBool()); // Downloads // When adding a torrent @@ -931,6 +933,9 @@ void AppController::setPreferencesAction() // Resume data storage type if (hasKey(u"resume_data_storage_type"_s)) session->setResumeDataStorageType(Utils::String::toEnum(it.value().toString(), BitTorrent::ResumeDataStorageType::Legacy)); + // Torrent content removing mode + if (hasKey(u"torrent_content_remove_option"_s)) + session->setTorrentContentRemoveOption(Utils::String::toEnum(it.value().toString(), BitTorrent::TorrentContentRemoveOption::MoveToTrash)); // Physical memory (RAM) usage limit if (hasKey(u"memory_working_set_limit"_s)) app()->setMemoryWorkingSetLimit(it.value().toInt()); diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 9ec640681..8a36fb81d 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -1096,11 +1096,11 @@ void TorrentsController::deleteAction() requireParams({u"hashes"_s, u"deleteFiles"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; - const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false) - ? DeleteTorrentAndFiles : DeleteTorrent; + const BitTorrent::TorrentRemoveOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false) + ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent; applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent) { - BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption); + BitTorrent::Session::instance()->removeTorrent(torrent->id(), deleteOption); }); } diff --git a/src/webui/www/private/confirmdeletion.html b/src/webui/www/private/confirmdeletion.html index 77144e8d9..99cc47860 100644 --- a/src/webui/www/private/confirmdeletion.html +++ b/src/webui/www/private/confirmdeletion.html @@ -91,7 +91,7 @@

  QBT_TR(Are you sure you want to remove the selected torrents from the transfer list?)QBT_TR[CONTEXT=HttpServer]

     -

+

    
diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index c01be0ee6..966ab579b 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -997,6 +997,17 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD + + + + + + + + @@ -2318,6 +2329,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD // Advanced settings // qBittorrent section $("resumeDataStorageType").setProperty("value", pref.resume_data_storage_type); + $("torrentContentRemoveOption").setProperty("value", pref.torrent_content_remove_option); $("memoryWorkingSetLimit").setProperty("value", pref.memory_working_set_limit); updateNetworkInterfaces(pref.current_network_interface, pref.current_interface_name); updateInterfaceAddresses(pref.current_network_interface, pref.current_interface_address); @@ -2764,6 +2776,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD // Update advanced settings // qBittorrent section settings["resume_data_storage_type"] = $("resumeDataStorageType").getProperty("value"); + settings["torrent_content_remove_option"] = $("torrentContentRemoveOption").getProperty("value"); settings["memory_working_set_limit"] = Number($("memoryWorkingSetLimit").getProperty("value")); settings["current_network_interface"] = $("networkInterface").getProperty("value"); settings["current_interface_address"] = $("optionalIPAddressToBind").getProperty("value");