diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 8e465b89c..418b617cd 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -1815,8 +1815,19 @@ bool Session::deleteTorrent(const InfoHash &hash, const DeleteOption deleteOptio for (const QString &file : files) Utils::Fs::forceRemove(resumeDataDir.absoluteFilePath(file)); + 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) + { + return job.torrent == torrent; + }); + if (iter != m_moveStorageQueue.end()) + m_moveStorageQueue.erase(iter); + } + delete torrent; - qDebug("Torrent deleted."); return true; } @@ -3938,6 +3949,78 @@ void Session::handleTorrentTrackerError(TorrentHandle *const torrent, const QStr emit trackerError(torrent, trackerUrl); } +bool Session::addMoveTorrentStorageJob(TorrentHandle *torrent, const QString &newPath, const MoveStorageMode mode) +{ + Q_ASSERT(torrent); + + if (m_moveStorageQueue.size() > 1) { + const auto iter = std::find_if(m_moveStorageQueue.begin() + 1, m_moveStorageQueue.end() + , [torrent](const MoveStorageJob &job) + { + return job.torrent == torrent; + }); + + if (iter != m_moveStorageQueue.end()) { + // remove existing inactive job + m_moveStorageQueue.erase(iter); + } + } + + QString currentLocation = QString::fromStdString( + torrent->nativeHandle().status(lt::torrent_handle::query_save_path).save_path); + if (!m_moveStorageQueue.isEmpty() && (m_moveStorageQueue.first().torrent == torrent)) { + // if there is active job for this torrent consider its target path as current location + // of this torrent to prevent creating meaningless job that will do nothing + currentLocation = m_moveStorageQueue.first().path; + } + + if (QDir {currentLocation} == QDir {newPath}) + return false; + + const MoveStorageJob moveStorageJob {torrent, newPath, mode}; + qDebug("Move storage from \"%s\" to \"%s\" is enqueued.", qUtf8Printable(currentLocation), qUtf8Printable(newPath)); + + if (m_moveStorageQueue.size() == 1) + moveTorrentStorage(moveStorageJob); + + return true; +} + +void Session::moveTorrentStorage(const MoveStorageJob &job) const +{ + lt::torrent_handle handle = job.torrent->nativeHandle(); + + qDebug("Moving torrent storage to \"%s\"...", qUtf8Printable(job.path)); +#if (LIBTORRENT_VERSION_NUM < 10200) + handle.move_storage(job.path.toUtf8().constData() + , ((job.mode == MoveStorageMode::Overwrite) + ? lt::always_replace_files : lt::dont_replace)); +#else + handle.move_storage(job.path.toUtf8().constData() + , ((job.mode == MoveStorageMode::Overwrite) + ? lt::move_flags_t::always_replace_files : lt::move_flags_t::dont_replace)); +#endif +} + +void Session::handleMoveTorrentStorageJobFinished(const QString &errorMessage) +{ + Q_ASSERT(!m_moveStorageQueue.isEmpty()); + + const MoveStorageJob finishedJob = m_moveStorageQueue.takeFirst(); + if (!m_moveStorageQueue.isEmpty()) + moveTorrentStorage(m_moveStorageQueue.first()); + + const auto iter = std::find_if(m_moveStorageQueue.cbegin(), m_moveStorageQueue.cend() + , [&finishedJob](const MoveStorageJob &job) + { + return job.torrent == finishedJob.torrent; + }); + if (iter == m_moveStorageQueue.cend()) { + // There is no more job for this torrent + finishedJob.torrent->handleStorageMoved(finishedJob.path, errorMessage); + } +} + void Session::handleTorrentTrackerWarning(TorrentHandle *const torrent, const QString &trackerUrl) { emit trackerWarning(torrent, trackerUrl); @@ -4254,8 +4337,6 @@ void Session::handleAlert(const lt::alert *a) case lt::torrent_finished_alert::alert_type: case lt::save_resume_data_alert::alert_type: case lt::save_resume_data_failed_alert::alert_type: - case lt::storage_moved_alert::alert_type: - case lt::storage_moved_failed_alert::alert_type: case lt::torrent_paused_alert::alert_type: case lt::torrent_resumed_alert::alert_type: case lt::tracker_error_alert::alert_type: @@ -4319,6 +4400,12 @@ void Session::handleAlert(const lt::alert *a) handleAlertsDroppedAlert(static_cast(a)); break; #endif + case lt::storage_moved_alert::alert_type: + handleStorageMovedAlert(static_cast(a)); + break; + case lt::storage_moved_failed_alert::alert_type: + handleStorageMovedFailedAlert(static_cast(a)); + break; } } catch (const std::exception &exc) { @@ -4813,6 +4900,29 @@ void Session::handleAlertsDroppedAlert(const lt::alerts_dropped_alert *p) const } #endif +void Session::handleStorageMovedAlert(const lt::storage_moved_alert *p) +{ + if (m_moveStorageQueue.isEmpty()) return; + + const TorrentHandle *torrent = m_torrents.value(p->handle.info_hash()); + const MoveStorageJob ¤tJob = m_moveStorageQueue.first(); + if (currentJob.torrent != torrent) return; + + const QString newPath {p->storage_path()}; + handleMoveTorrentStorageJobFinished(newPath != currentJob.path ? tr("New path doesn't match a target path.") : QString {}); +} + +void Session::handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *p) +{ + if (m_moveStorageQueue.isEmpty()) return; + + const TorrentHandle *torrent = m_torrents.value(p->handle.info_hash()); + const MoveStorageJob ¤tJob = m_moveStorageQueue.first(); + if (currentJob.torrent != torrent) return; + + handleMoveTorrentStorageJobFinished(QString::fromStdString(p->message())); +} + void Session::handleStateUpdateAlert(const lt::state_update_alert *p) { QVector updatedTorrents; diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index 259b6a688..892a8db28 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -96,6 +96,8 @@ namespace BitTorrent class TrackerEntry; struct CreateTorrentParams; + enum class MoveStorageMode; + // 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 @@ -461,6 +463,8 @@ namespace BitTorrent void handleTorrentTrackerWarning(TorrentHandle *const torrent, const QString &trackerUrl); void handleTorrentTrackerError(TorrentHandle *const torrent, const QString &trackerUrl); + bool addMoveTorrentStorageJob(TorrentHandle *torrent, const QString &newPath, MoveStorageMode mode); + signals: void addTorrentFailed(const QString &error); void allTorrentsFinished(); @@ -514,6 +518,13 @@ namespace BitTorrent void networkConfigurationChange(const QNetworkConfiguration &); private: + struct MoveStorageJob + { + TorrentHandle *torrent; + QString path; + MoveStorageMode mode; + }; + struct RemovingTorrentData { QString name; @@ -583,6 +594,8 @@ namespace BitTorrent #if (LIBTORRENT_VERSION_NUM >= 10200) void handleAlertsDroppedAlert(const lt::alerts_dropped_alert *p) const; #endif + void handleStorageMovedAlert(const lt::storage_moved_alert *p); + void handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *p); void createTorrentHandle(const lt::torrent_handle &nativeHandle); @@ -592,6 +605,9 @@ namespace BitTorrent std::vector getPendingAlerts(lt::time_duration time = lt::time_duration::zero()) const; + void moveTorrentStorage(const MoveStorageJob &job) const; + void handleMoveTorrentStorageJobFinished(const QString &errorMessage = {}); + // BitTorrent lt::session *m_nativeSession = nullptr; @@ -732,6 +748,8 @@ namespace BitTorrent QNetworkConfigurationManager *m_networkManager = nullptr; + QList m_moveStorageQueue; + static Session *m_instance; }; } diff --git a/src/base/bittorrent/torrenthandle.cpp b/src/base/bittorrent/torrenthandle.cpp index 44d48ee36..47ccdbceb 100644 --- a/src/base/bittorrent/torrenthandle.cpp +++ b/src/base/bittorrent/torrenthandle.cpp @@ -327,7 +327,7 @@ QString TorrentHandle::currentTracker() const QString TorrentHandle::savePath(bool actual) const { if (actual) - return Utils::Fs::toUniformPath(nativeActualSavePath()); + return Utils::Fs::toUniformPath(actualStorageLocation()); else return Utils::Fs::toUniformPath(m_savePath); } @@ -369,7 +369,7 @@ void TorrentHandle::setAutoTMMEnabled(bool enabled) m_session->handleTorrentSavingModeChanged(this); if (m_useAutoTMM) - move_impl(m_session->categorySavePath(m_category), true); + move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); } bool TorrentHandle::hasRootFolder() const @@ -377,7 +377,7 @@ bool TorrentHandle::hasRootFolder() const return m_hasRootFolder; } -QString TorrentHandle::nativeActualSavePath() const +QString TorrentHandle::actualStorageLocation() const { return QString::fromStdString(m_nativeStatus.save_path); } @@ -898,44 +898,42 @@ void TorrentHandle::updateState() else if (isMoveInProgress()) { m_state = TorrentState::Moving; } + else if (hasMissingFiles()) { + m_state = TorrentState::MissingFiles; + } else if (isPaused()) { - if (hasMissingFiles()) - m_state = TorrentState::MissingFiles; - else - m_state = isSeed() ? TorrentState::PausedUploading : TorrentState::PausedDownloading; + m_state = isSeed() ? TorrentState::PausedUploading : TorrentState::PausedDownloading; + } + else if (m_session->isQueueingSystemEnabled() && isQueued() && !isChecking()) { + m_state = isSeed() ? TorrentState::QueuedUploading : TorrentState::QueuedDownloading; } else { - if (m_session->isQueueingSystemEnabled() && isQueued() && !isChecking()) { - m_state = isSeed() ? TorrentState::QueuedUploading : TorrentState::QueuedDownloading; - } - else { - switch (m_nativeStatus.state) { - case lt::torrent_status::finished: - case lt::torrent_status::seeding: - if (isForced()) - m_state = TorrentState::ForcedUploading; - else - m_state = m_nativeStatus.upload_payload_rate > 0 ? TorrentState::Uploading : TorrentState::StalledUploading; - break; - case lt::torrent_status::allocating: - m_state = TorrentState::Allocating; - break; - case lt::torrent_status::checking_files: - m_state = m_hasSeedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading; - break; - case lt::torrent_status::downloading_metadata: - m_state = TorrentState::DownloadingMetadata; - break; - case lt::torrent_status::downloading: - if (isForced()) - m_state = TorrentState::ForcedDownloading; - else - m_state = m_nativeStatus.download_payload_rate > 0 ? TorrentState::Downloading : TorrentState::StalledDownloading; - break; - default: - qWarning("Unrecognized torrent status, should not happen!!! status was %d", m_nativeStatus.state); - m_state = TorrentState::Unknown; - } + switch (m_nativeStatus.state) { + case lt::torrent_status::finished: + case lt::torrent_status::seeding: + if (isForced()) + m_state = TorrentState::ForcedUploading; + else + m_state = m_nativeStatus.upload_payload_rate > 0 ? TorrentState::Uploading : TorrentState::StalledUploading; + break; + case lt::torrent_status::allocating: + m_state = TorrentState::Allocating; + break; + case lt::torrent_status::checking_files: + m_state = m_hasSeedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading; + break; + case lt::torrent_status::downloading_metadata: + m_state = TorrentState::DownloadingMetadata; + break; + case lt::torrent_status::downloading: + if (isForced()) + m_state = TorrentState::ForcedDownloading; + else + m_state = m_nativeStatus.download_payload_rate > 0 ? TorrentState::Downloading : TorrentState::StalledDownloading; + break; + default: + qWarning("Unrecognized torrent status, should not happen!!! status was %d", m_nativeStatus.state); + m_state = TorrentState::Unknown; } } } @@ -1334,7 +1332,7 @@ bool TorrentHandle::setCategory(const QString &category) if (m_useAutoTMM) { if (!m_session->isDisableAutoTMMWhenCategoryChanged()) - move_impl(m_session->categorySavePath(m_category), true); + move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); else setAutoTMMEnabled(false); } @@ -1354,21 +1352,19 @@ void TorrentHandle::move(QString path) if (!path.endsWith('/')) path += '/'; - move_impl(path, false); + move_impl(path, MoveStorageMode::KeepExistingFiles); } -void TorrentHandle::move_impl(QString path, bool overwrite) +void TorrentHandle::move_impl(QString path, const MoveStorageMode mode) { if (path == savePath()) return; path = Utils::Fs::toNativePath(path); - if (!useTempPath()) { - moveStorage(path, overwrite); - } - else { - m_savePath = path; - m_session->handleTorrentSavePathChanged(this); - } + if (!useTempPath()) + moveStorage(path, mode); + + m_savePath = path; + m_session->handleTorrentSavePathChanged(this); } void TorrentHandle::forceReannounce(int index) @@ -1502,30 +1498,10 @@ void TorrentHandle::resume_impl(bool forced) m_nativeHandle.resume(); } -void TorrentHandle::moveStorage(const QString &newPath, bool overwrite) +void TorrentHandle::moveStorage(const QString &newPath, const MoveStorageMode mode) { - if (isMoveInProgress()) { - qDebug("enqueue move storage to %s", qUtf8Printable(newPath)); - m_moveStorageInfo.queuedPath = newPath; - m_moveStorageInfo.queuedOverwrite = overwrite; - } - else { - const QString oldPath = nativeActualSavePath(); - if (QDir(oldPath) == QDir(newPath)) return; - - qDebug("move storage: %s to %s", qUtf8Printable(oldPath), qUtf8Printable(newPath)); - // Actually move the storage -#if (LIBTORRENT_VERSION_NUM < 10200) - m_nativeHandle.move_storage(newPath.toUtf8().constData() - , (overwrite ? lt::always_replace_files : lt::dont_replace)); -#else - m_nativeHandle.move_storage(newPath.toUtf8().constData() - , (overwrite ? lt::move_flags_t::always_replace_files : lt::move_flags_t::dont_replace)); -#endif - m_moveStorageInfo.oldPath = oldPath; - m_moveStorageInfo.newPath = newPath; - updateState(); - } + if (m_session->addMoveTorrentStorageJob(this, newPath, mode)) + m_storageIsMoving = true; } void TorrentHandle::renameFile(const int index, const QString &name) @@ -1540,66 +1516,18 @@ void TorrentHandle::handleStateUpdate(const lt::torrent_status &nativeStatus) updateStatus(nativeStatus); } -void TorrentHandle::handleStorageMovedAlert(const lt::storage_moved_alert *p) +void TorrentHandle::handleStorageMoved(const QString &newPath, const QString &errorMessage) { - if (!isMoveInProgress()) { - qWarning() << "Unexpected " << Q_FUNC_INFO << " call."; - return; - } + m_storageIsMoving = false; - const QString newPath(p->storage_path()); - if (newPath != m_moveStorageInfo.newPath) { - qWarning() << Q_FUNC_INFO << ": New path doesn't match a path in a queue."; - return; - } + if (!errorMessage.isEmpty()) + LogMsg(tr("Could not move torrent: %1. Reason: %2").arg(name(), errorMessage), Log::CRITICAL); + else + LogMsg(tr("Successfully moved torrent: %1. New path: %2").arg(name(), newPath)); - LogMsg(tr("Successfully moved torrent: %1. New path: %2").arg(name(), m_moveStorageInfo.newPath)); - - const QDir oldDir {m_moveStorageInfo.oldPath}; - if ((oldDir == QDir(m_session->torrentTempPath(info()))) - && (oldDir != QDir(m_session->tempPath()))) { - // torrent without root folder still has it in its temporary save path - // so its temp path isn't equal to temp path root - qDebug() << "Removing torrent temp folder:" << m_moveStorageInfo.oldPath; - Utils::Fs::smartRemoveEmptyFolderTree(m_moveStorageInfo.oldPath); - } - - m_moveStorageInfo.newPath.clear(); updateStatus(); - if (!m_moveStorageInfo.queuedPath.isEmpty()) { - moveStorage(m_moveStorageInfo.queuedPath, m_moveStorageInfo.queuedOverwrite); - m_moveStorageInfo.queuedPath.clear(); - } - - if (!useTempPath()) { - m_savePath = newPath; - m_session->handleTorrentSavePathChanged(this); - } - - while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) - m_moveFinishedTriggers.takeFirst()(); -} - -void TorrentHandle::handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *p) -{ - if (!isMoveInProgress()) { - qWarning() << "Unexpected " << Q_FUNC_INFO << " call."; - return; - } - - LogMsg(tr("Could not move torrent: '%1'. Reason: %2") - .arg(name(), QString::fromStdString(p->message())), Log::CRITICAL); - - m_moveStorageInfo.newPath.clear(); - updateStatus(); - - if (!m_moveStorageInfo.queuedPath.isEmpty()) { - moveStorage(m_moveStorageInfo.queuedPath, m_moveStorageInfo.queuedOverwrite); - m_moveStorageInfo.queuedPath.clear(); - } - - while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) + while ((m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) m_moveFinishedTriggers.takeFirst()(); } @@ -1922,7 +1850,7 @@ void TorrentHandle::handleTempPathChanged() void TorrentHandle::handleCategorySavePathChanged() { if (m_useAutoTMM) - move_impl(m_session->categorySavePath(m_category), true); + move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); } void TorrentHandle::handleAppendExtensionToggled() @@ -1953,12 +1881,6 @@ void TorrentHandle::handleAlert(const lt::alert *a) case lt::save_resume_data_failed_alert::alert_type: handleSaveResumeDataFailedAlert(static_cast(a)); break; - case lt::storage_moved_alert::alert_type: - handleStorageMovedAlert(static_cast(a)); - break; - case lt::storage_moved_failed_alert::alert_type: - handleStorageMovedFailedAlert(static_cast(a)); - break; case lt::torrent_paused_alert::alert_type: handleTorrentPausedAlert(static_cast(a)); break; @@ -2028,19 +1950,27 @@ void TorrentHandle::adjustActualSavePath() void TorrentHandle::adjustActualSavePath_impl() { - QString path; - if (!useTempPath()) { - // Disabling temp dir - // Moving all torrents to their destination folder - path = savePath(); - } - else { - // Moving all downloading torrents to temporary folder - path = m_session->torrentTempPath(info()); - qDebug() << "Moving torrent to its temporary folder:" << path; + const bool needUseTempDir = useTempPath(); + const QDir tempDir {m_session->torrentTempPath(info())}; + const QDir currentDir {actualStorageLocation()}; + const QDir targetDir {needUseTempDir ? tempDir : QDir {savePath()}}; + + if (targetDir == currentDir) return; + + if (!needUseTempDir) { + if ((currentDir == tempDir) && (currentDir != QDir {m_session->tempPath()})) { + // torrent without root folder still has it in its temporary save path + // so its temp path isn't equal to temp path root + const QString currentDirPath = currentDir.absolutePath(); + m_moveFinishedTriggers.append([currentDirPath] + { + qDebug() << "Removing torrent temp folder:" << currentDirPath; + Utils::Fs::smartRemoveEmptyFolderTree(currentDirPath); + }); + } } - moveStorage(Utils::Fs::toNativePath(path), true); + moveStorage(Utils::Fs::toNativePath(targetDir.absolutePath()), MoveStorageMode::Overwrite); } lt::torrent_handle TorrentHandle::nativeHandle() const @@ -2057,7 +1987,7 @@ void TorrentHandle::updateTorrentInfo() bool TorrentHandle::isMoveInProgress() const { - return !m_moveStorageInfo.newPath.isEmpty(); + return m_storageIsMoving; } bool TorrentHandle::useTempPath() const diff --git a/src/base/bittorrent/torrenthandle.h b/src/base/bittorrent/torrenthandle.h index 8c8d813cc..559bdb3d5 100644 --- a/src/base/bittorrent/torrenthandle.h +++ b/src/base/bittorrent/torrenthandle.h @@ -99,6 +99,12 @@ namespace BitTorrent int numPeers = 0; }; + enum class MoveStorageMode + { + KeepExistingFiles, + Overwrite + }; + enum class TorrentState { Unknown = -1, @@ -345,6 +351,7 @@ namespace BitTorrent void handleCategorySavePathChanged(); void handleAppendExtensionToggled(); void saveResumeData(); + void handleStorageMoved(const QString &newPath, const QString &errorMessage); /** * @brief fraction of file pieces that are available at least from one peer @@ -376,8 +383,6 @@ namespace BitTorrent void handlePerformanceAlert(const lt::performance_alert *p) const; void handleSaveResumeDataAlert(const lt::save_resume_data_alert *p); void handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p); - void handleStorageMovedAlert(const lt::storage_moved_alert *p); - void handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *p); void handleTorrentCheckedAlert(const lt::torrent_checked_alert *p); void handleTorrentFinishedAlert(const lt::torrent_finished_alert *p); void handleTorrentPausedAlert(const lt::torrent_paused_alert *p); @@ -388,14 +393,14 @@ namespace BitTorrent void resume_impl(bool forced); bool isMoveInProgress() const; - QString nativeActualSavePath() const; + QString actualStorageLocation() const; bool isAutoManaged() const; void setAutoManaged(bool enable); void adjustActualSavePath(); void adjustActualSavePath_impl(); - void move_impl(QString path, bool overwrite); - void moveStorage(const QString &newPath, bool overwrite); + void move_impl(QString path, MoveStorageMode mode); + void moveStorage(const QString &newPath, MoveStorageMode mode); void manageIncompleteFiles(); void setFirstLastPiecePriorityImpl(bool enabled, const QVector &updatedFilePrio = {}); @@ -408,16 +413,7 @@ namespace BitTorrent InfoHash m_hash; - struct - { - QString oldPath; - QString newPath; - // queuedPath is where files should be moved to, - // when current moving is completed - QString queuedPath; - bool queuedOverwrite = true; - } m_moveStorageInfo; - + bool m_storageIsMoving = false; // m_moveFinishedTriggers is activated only when the following conditions are met: // all file rename jobs complete, all file move jobs complete QQueue m_moveFinishedTriggers; diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index 30e196c03..17c2e92ff 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.cpp @@ -507,12 +507,9 @@ void TransferListWidget::setSelectedTorrentsLocation() if (torrents.isEmpty()) return; const QString oldLocation = torrents[0]->savePath(); - qDebug("Old location is %s", qUtf8Printable(oldLocation)); - const QString newLocation = QFileDialog::getExistingDirectory(this, tr("Choose save path"), oldLocation, QFileDialog::DontConfirmOverwrite | QFileDialog::ShowDirsOnly | QFileDialog::HideNameFilterDetails); if (newLocation.isEmpty() || !QDir(newLocation).exists()) return; - qDebug("New location is %s", qUtf8Printable(newLocation)); // Actually move storage for (BitTorrent::TorrentHandle *const torrent : torrents) {