From 89cedd411eba228e8f3cf4f4d9232231e127b38d Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Wed, 5 May 2021 16:56:31 +0300 Subject: [PATCH 1/3] Allow add torrents with relative save path The relative save path will be resoloved against the default one. --- src/base/bittorrent/session.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index ba9fa5019..6ffe2442b 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -2062,8 +2062,10 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr const bool useAutoTMM = addTorrentParams.useAutoTMM.value_or(!isAutoTMMDisabledByDefault()); if (useAutoTMM) loadTorrentParams.savePath = ""; - else if (addTorrentParams.savePath.trimmed().isEmpty()) + else if (addTorrentParams.savePath.isEmpty()) loadTorrentParams.savePath = defaultSavePath(); + else if (QDir(addTorrentParams.savePath).isRelative()) + loadTorrentParams.savePath = QDir(defaultSavePath()).absoluteFilePath(addTorrentParams.savePath); else loadTorrentParams.savePath = normalizePath(addTorrentParams.savePath); From 9565b695ef003e46c98f9f293d3ef3faf00917e1 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Wed, 5 May 2021 16:41:30 +0300 Subject: [PATCH 2/3] Unify custom exceptions --- src/app/application.cpp | 2 +- src/app/cmdoptions.cpp | 11 ----------- src/app/cmdoptions.h | 11 ++++------- src/app/main.cpp | 2 +- src/base/bittorrent/tracker.cpp | 2 +- src/base/exceptions.cpp | 8 ++++---- src/base/exceptions.h | 18 +++++++++++++----- src/base/rss/rss_autodownloader.cpp | 12 +----------- src/base/rss/rss_autodownloader.h | 9 ++++----- src/base/rss/rss_feed.cpp | 4 ++-- src/base/utils/foreignapps.cpp | 2 +- src/base/utils/version.h | 17 ++++++++++------- src/gui/programupdater.cpp | 2 +- 13 files changed, 43 insertions(+), 57 deletions(-) diff --git a/src/app/application.cpp b/src/app/application.cpp index e03b0cf77..7bf6ee0c4 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -638,7 +638,7 @@ int Application::exec(const QStringList ¶ms) catch (const RuntimeError &err) { #ifdef DISABLE_GUI - fprintf(stderr, "%s", err.what()); + fprintf(stderr, "%s", qPrintable(err.message())); #else QMessageBox msgBox; msgBox.setIcon(QMessageBox::Critical); diff --git a/src/app/cmdoptions.cpp b/src/app/cmdoptions.cpp index 6535e1702..15cb4a85a 100644 --- a/src/app/cmdoptions.cpp +++ b/src/app/cmdoptions.cpp @@ -498,17 +498,6 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args) return result; } -CommandLineParameterError::CommandLineParameterError(const QString &messageForUser) - : std::runtime_error(messageForUser.toLocal8Bit().data()) - , m_messageForUser(messageForUser) -{ -} - -const QString &CommandLineParameterError::messageForUser() const -{ - return m_messageForUser; -} - QString wrapText(const QString &text, int initialIndentation = USAGE_TEXT_COLUMN, int wrapAtColumn = WRAP_AT_COLUMN) { QStringList words = text.split(' '); diff --git a/src/app/cmdoptions.h b/src/app/cmdoptions.h index 2f167b99d..46d84b66e 100644 --- a/src/app/cmdoptions.h +++ b/src/app/cmdoptions.h @@ -31,11 +31,12 @@ #pragma once #include -#include #include #include +#include "base/exceptions.h" + class QProcessEnvironment; struct QBtCommandLineParameters @@ -67,14 +68,10 @@ struct QBtCommandLineParameters QStringList paramList() const; }; -class CommandLineParameterError : public std::runtime_error +class CommandLineParameterError : public RuntimeError { public: - explicit CommandLineParameterError(const QString &messageForUser); - const QString &messageForUser() const; - -private: - const QString m_messageForUser; + using RuntimeError::RuntimeError; }; QBtCommandLineParameters parseCommandLine(const QStringList &args); diff --git a/src/app/main.cpp b/src/app/main.cpp index f3c20339e..11b37e2e1 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -311,7 +311,7 @@ int main(int argc, char *argv[]) } catch (const CommandLineParameterError &er) { - displayBadArgMessage(er.messageForUser()); + displayBadArgMessage(er.message()); return EXIT_FAILURE; } } diff --git a/src/base/bittorrent/tracker.cpp b/src/base/bittorrent/tracker.cpp index 372242d49..ca454b267 100644 --- a/src/base/bittorrent/tracker.cpp +++ b/src/base/bittorrent/tracker.cpp @@ -265,7 +265,7 @@ Http::Response Tracker::processRequest(const Http::Request &request, const Http: const lt::entry::dictionary_type bencodedEntry = { - {ANNOUNCE_RESPONSE_FAILURE_REASON, {error.what()}} + {ANNOUNCE_RESPONSE_FAILURE_REASON, {error.message().toStdString()}} }; QByteArray reply; lt::bencode(std::back_inserter(reply), bencodedEntry); diff --git a/src/base/exceptions.cpp b/src/base/exceptions.cpp index 27527ddbb..66e291fa7 100644 --- a/src/base/exceptions.cpp +++ b/src/base/exceptions.cpp @@ -28,12 +28,12 @@ #include "exceptions.h" -RuntimeError::RuntimeError(const QString &message) - : std::runtime_error {message.toUtf8().data()} +Exception::Exception(const QString &message) noexcept + : m_message {message} { } -QString RuntimeError::message() const +QString Exception::message() const noexcept { - return what(); + return m_message; } diff --git a/src/base/exceptions.h b/src/base/exceptions.h index 4b3ef77a4..7d32739ca 100644 --- a/src/base/exceptions.h +++ b/src/base/exceptions.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2018, 2021 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,12 +28,20 @@ #pragma once -#include #include -class RuntimeError : public std::runtime_error +class Exception { public: - explicit RuntimeError(const QString &message = {}); - QString message() const; + explicit Exception(const QString &message = {}) noexcept; + [[nodiscard]] QString message() const noexcept; + +private: + QString m_message; +}; + +class RuntimeError : public Exception +{ +public: + using Exception::Exception; }; diff --git a/src/base/rss/rss_autodownloader.cpp b/src/base/rss/rss_autodownloader.cpp index 2d8c9b07d..4fa441520 100644 --- a/src/base/rss/rss_autodownloader.cpp +++ b/src/base/rss/rss_autodownloader.cpp @@ -114,7 +114,7 @@ AutoDownloader::AutoDownloader() m_fileStorage = new AsyncFileStorage( Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Config) + ConfFolderName)); if (!m_fileStorage) - throw std::runtime_error("Directory for RSS AutoDownloader data is unavailable."); + throw RuntimeError(tr("Directory for RSS AutoDownloader data is unavailable.")); m_fileStorage->moveToThread(m_ioThread); connect(m_ioThread, &QThread::finished, m_fileStorage, &AsyncFileStorage::deleteLater); @@ -527,13 +527,3 @@ void AutoDownloader::timerEvent(QTimerEvent *event) Q_UNUSED(event); store(); } - -ParsingError::ParsingError(const QString &message) - : std::runtime_error(message.toUtf8().data()) -{ -} - -QString ParsingError::message() const -{ - return what(); -} diff --git a/src/base/rss/rss_autodownloader.h b/src/base/rss/rss_autodownloader.h index 16f6ef48e..a55b3e380 100644 --- a/src/base/rss/rss_autodownloader.h +++ b/src/base/rss/rss_autodownloader.h @@ -28,8 +28,6 @@ #pragma once -#include - #include #include #include @@ -38,6 +36,8 @@ #include #include +#include "base/exceptions.h" + class QThread; class QTimer; @@ -53,11 +53,10 @@ namespace RSS class AutoDownloadRule; - class ParsingError : public std::runtime_error + class ParsingError : public RuntimeError { public: - explicit ParsingError(const QString &message); - QString message() const; + using RuntimeError::RuntimeError; }; class AutoDownloader final : public QObject diff --git a/src/base/rss/rss_feed.cpp b/src/base/rss/rss_feed.cpp index a22186037..b38fa5b92 100644 --- a/src/base/rss/rss_feed.cpp +++ b/src/base/rss/rss_feed.cpp @@ -313,7 +313,7 @@ void Feed::loadArticles(const QByteArray &data) if (!addArticle(article)) delete article; } - catch (const std::runtime_error&) {} + catch (const RuntimeError &) {} } } @@ -335,7 +335,7 @@ void Feed::loadArticlesLegacy() if (!addArticle(article)) delete article; } - catch (const std::runtime_error&) {} + catch (const RuntimeError &) {} } } diff --git a/src/base/utils/foreignapps.cpp b/src/base/utils/foreignapps.cpp index 2b6987ae0..c517d0942 100644 --- a/src/base/utils/foreignapps.cpp +++ b/src/base/utils/foreignapps.cpp @@ -76,7 +76,7 @@ namespace { info = {exeName, versionStr.left(idx)}; } - catch (const std::runtime_error &) + catch (const RuntimeError &) { return false; } diff --git a/src/base/utils/version.h b/src/base/utils/version.h index 51bbb92b0..a03ea9155 100644 --- a/src/base/utils/version.h +++ b/src/base/utils/version.h @@ -29,11 +29,12 @@ #pragma once #include -#include #include #include +#include "base/exceptions.h" + namespace Utils { template @@ -64,7 +65,7 @@ namespace Utils * @brief Creates version from string in format "x.y.z" * * @param version Version string in format "x.y.z" - * @throws std::runtime_error if parsing fails + * @throws RuntimeError if parsing fails */ Version(const QString &version) : Version {version.split(QLatin1Char('.'))} @@ -75,7 +76,7 @@ namespace Utils * @brief Creates version from byte array in format "x.y.z" * * @param version Version string in format "x.y.z" - * @throws std::runtime_error if parsing fails + * @throws RuntimeError if parsing fails */ Version(const QByteArray &version) : Version {version.split('.')} @@ -150,9 +151,9 @@ namespace Utils { return Version(s); } - catch (const std::runtime_error &er) + catch (const RuntimeError &er) { - qDebug() << "Error parsing version:" << er.what(); + qDebug() << "Error parsing version:" << er.message(); return defaultVersion; } } @@ -165,7 +166,9 @@ namespace Utils { if ((static_cast(versionParts.size()) > N) || (static_cast(versionParts.size()) < Mandatory)) - throw std::runtime_error("Incorrect number of version components"); + { + throw RuntimeError(QLatin1String("Incorrect number of version components")); + } bool ok = false; ComponentsArray res {{}}; @@ -173,7 +176,7 @@ namespace Utils { res[i] = static_cast(versionParts[static_cast(i)].toInt(&ok)); if (!ok) - throw std::runtime_error("Can not parse version component"); + throw RuntimeError(QLatin1String("Can not parse version component")); } return res; } diff --git a/src/gui/programupdater.cpp b/src/gui/programupdater.cpp index c72327822..317509ed9 100644 --- a/src/gui/programupdater.cpp +++ b/src/gui/programupdater.cpp @@ -66,7 +66,7 @@ namespace } return (newVersion > currentVersion); } - catch (const std::runtime_error &) + catch (const RuntimeError &) { return false; } From 2993fdb16954a9e3301efb7f9171ae1f03ab7743 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Fri, 23 Apr 2021 12:02:25 +0300 Subject: [PATCH 3/3] Improve "Watched folders" feature Make "file system watcher" an application core component and separate it from its presentation model. --- src/app/application.cpp | 6 +- src/base/CMakeLists.txt | 6 +- src/base/base.pri | 6 +- src/base/bittorrent/addtorrentparams.h | 3 + src/base/bittorrent/magneturi.cpp | 3 +- src/base/bittorrent/magneturi.h | 5 +- src/base/bittorrent/session.cpp | 2 + src/base/bittorrent/torrentinfo.cpp | 2 + src/base/bittorrent/torrentinfo.h | 2 + src/base/exceptions.h | 6 + src/base/filesystemwatcher.cpp | 187 ----- src/base/preferences.cpp | 11 - src/base/preferences.h | 2 - src/base/scanfoldersmodel.cpp | 424 ----------- src/base/scanfoldersmodel.h | 111 --- src/base/torrentfileswatcher.cpp | 658 ++++++++++++++++++ src/base/torrentfileswatcher.h | 96 +++ src/gui/CMakeLists.txt | 7 +- src/gui/gui.pri | 13 +- src/gui/optionsdialog.cpp | 121 ++-- src/gui/optionsdialog.h | 11 +- src/gui/optionsdialog.ui | 23 +- src/gui/scanfoldersdelegate.cpp | 118 ---- src/gui/watchedfolderoptionsdialog.cpp | 153 ++++ ...elegate.h => watchedfolderoptionsdialog.h} | 40 +- src/gui/watchedfolderoptionsdialog.ui | 315 +++++++++ src/gui/watchedfoldersmodel.cpp | 178 +++++ .../watchedfoldersmodel.h} | 54 +- src/webui/api/appcontroller.cpp | 87 ++- 29 files changed, 1635 insertions(+), 1015 deletions(-) delete mode 100644 src/base/filesystemwatcher.cpp delete mode 100644 src/base/scanfoldersmodel.cpp delete mode 100644 src/base/scanfoldersmodel.h create mode 100644 src/base/torrentfileswatcher.cpp create mode 100644 src/base/torrentfileswatcher.h delete mode 100644 src/gui/scanfoldersdelegate.cpp create mode 100644 src/gui/watchedfolderoptionsdialog.cpp rename src/gui/{scanfoldersdelegate.h => watchedfolderoptionsdialog.h} (62%) create mode 100644 src/gui/watchedfolderoptionsdialog.ui create mode 100644 src/gui/watchedfoldersmodel.cpp rename src/{base/filesystemwatcher.h => gui/watchedfoldersmodel.h} (50%) diff --git a/src/app/application.cpp b/src/app/application.cpp index 7bf6ee0c4..b0ceef11b 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -75,9 +75,9 @@ #include "base/profile.h" #include "base/rss/rss_autodownloader.h" #include "base/rss/rss_session.h" -#include "base/scanfoldersmodel.h" #include "base/search/searchpluginmanager.h" #include "base/settingsstorage.h" +#include "base/torrentfileswatcher.h" #include "base/utils/compare.h" #include "base/utils/fs.h" #include "base/utils/misc.h" @@ -621,7 +621,7 @@ int Application::exec(const QStringList ¶ms) connect(BitTorrent::Session::instance(), &BitTorrent::Session::allTorrentsFinished, this, &Application::allTorrentsFinished, Qt::QueuedConnection); Net::GeoIPManager::initInstance(); - ScanFoldersModel::initInstance(); + TorrentFilesWatcher::initInstance(); #ifndef DISABLE_WEBUI m_webui = new WebUI; @@ -817,7 +817,7 @@ void Application::cleanup() delete RSS::AutoDownloader::instance(); delete RSS::Session::instance(); - ScanFoldersModel::freeInstance(); + TorrentFilesWatcher::freeInstance(); BitTorrent::Session::freeInstance(); Net::GeoIPManager::freeInstance(); Net::DownloadManager::freeInstance(); diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index d151809e4..6f2e7c657 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -37,7 +37,6 @@ add_library(qbt_base STATIC bittorrent/trackerentry.h digest32.h exceptions.h - filesystemwatcher.h global.h http/connection.h http/httperror.h @@ -71,13 +70,13 @@ add_library(qbt_base STATIC rss/rss_item.h rss/rss_parser.h rss/rss_session.h - scanfoldersmodel.h search/searchdownloadhandler.h search/searchhandler.h search/searchpluginmanager.h settingsstorage.h tagset.h torrentfileguard.h + torrentfileswatcher.h torrentfilter.h types.h unicodestrings.h @@ -122,7 +121,6 @@ add_library(qbt_base STATIC bittorrent/tracker.cpp bittorrent/trackerentry.cpp exceptions.cpp - filesystemwatcher.cpp http/connection.cpp http/httperror.cpp http/requestparser.cpp @@ -151,13 +149,13 @@ add_library(qbt_base STATIC rss/rss_item.cpp rss/rss_parser.cpp rss/rss_session.cpp - scanfoldersmodel.cpp search/searchdownloadhandler.cpp search/searchhandler.cpp search/searchpluginmanager.cpp settingsstorage.cpp tagset.cpp torrentfileguard.cpp + torrentfileswatcher.cpp torrentfilter.cpp utils/bytearray.cpp utils/compare.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 7a863b538..d1dddd97f 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -36,7 +36,6 @@ HEADERS += \ $$PWD/bittorrent/trackerentry.h \ $$PWD/digest32.h \ $$PWD/exceptions.h \ - $$PWD/filesystemwatcher.h \ $$PWD/global.h \ $$PWD/http/connection.h \ $$PWD/http/httperror.h \ @@ -70,7 +69,6 @@ HEADERS += \ $$PWD/rss/rss_item.h \ $$PWD/rss/rss_parser.h \ $$PWD/rss/rss_session.h \ - $$PWD/scanfoldersmodel.h \ $$PWD/search/searchdownloadhandler.h \ $$PWD/search/searchhandler.h \ $$PWD/search/searchpluginmanager.h \ @@ -78,6 +76,7 @@ HEADERS += \ $$PWD/settingvalue.h \ $$PWD/tagset.h \ $$PWD/torrentfileguard.h \ + $$PWD/torrentfileswatcher.h \ $$PWD/torrentfilter.h \ $$PWD/types.h \ $$PWD/unicodestrings.h \ @@ -122,7 +121,6 @@ SOURCES += \ $$PWD/bittorrent/tracker.cpp \ $$PWD/bittorrent/trackerentry.cpp \ $$PWD/exceptions.cpp \ - $$PWD/filesystemwatcher.cpp \ $$PWD/http/connection.cpp \ $$PWD/http/httperror.cpp \ $$PWD/http/requestparser.cpp \ @@ -151,13 +149,13 @@ SOURCES += \ $$PWD/rss/rss_item.cpp \ $$PWD/rss/rss_parser.cpp \ $$PWD/rss/rss_session.cpp \ - $$PWD/scanfoldersmodel.cpp \ $$PWD/search/searchdownloadhandler.cpp \ $$PWD/search/searchhandler.cpp \ $$PWD/search/searchpluginmanager.cpp \ $$PWD/settingsstorage.cpp \ $$PWD/tagset.cpp \ $$PWD/torrentfileguard.cpp \ + $$PWD/torrentfileswatcher.cpp \ $$PWD/torrentfilter.cpp \ $$PWD/utils/bytearray.cpp \ $$PWD/utils/compare.cpp \ diff --git a/src/base/bittorrent/addtorrentparams.h b/src/base/bittorrent/addtorrentparams.h index edb62cc2a..23967b5be 100644 --- a/src/base/bittorrent/addtorrentparams.h +++ b/src/base/bittorrent/addtorrentparams.h @@ -30,6 +30,7 @@ #include +#include #include #include @@ -62,3 +63,5 @@ namespace BitTorrent qreal ratioLimit = Torrent::USE_GLOBAL_RATIO; }; } + +Q_DECLARE_METATYPE(BitTorrent::AddTorrentParams) diff --git a/src/base/bittorrent/magneturi.cpp b/src/base/bittorrent/magneturi.cpp index b6f9e11d7..cf755e1a7 100644 --- a/src/base/bittorrent/magneturi.cpp +++ b/src/base/bittorrent/magneturi.cpp @@ -34,7 +34,6 @@ #include #include -#include #include "infohash.h" @@ -59,6 +58,8 @@ namespace using namespace BitTorrent; +const int magnetUriId = qRegisterMetaType(); + MagnetUri::MagnetUri(const QString &source) : m_valid(false) , m_url(source) diff --git a/src/base/bittorrent/magneturi.h b/src/base/bittorrent/magneturi.h index fdc205c50..861ce53b2 100644 --- a/src/base/bittorrent/magneturi.h +++ b/src/base/bittorrent/magneturi.h @@ -31,13 +31,12 @@ #include #include +#include #include #include "infohash.h" #include "trackerentry.h" -class QUrl; - namespace BitTorrent { class MagnetUri @@ -64,3 +63,5 @@ namespace BitTorrent lt::add_torrent_params m_addTorrentParams; }; } + +Q_DECLARE_METATYPE(BitTorrent::MagnetUri) diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 6ffe2442b..e508ab77c 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -318,6 +318,8 @@ namespace #endif } +const int addTorrentParamsId = qRegisterMetaType(); + // Session Session *Session::m_instance = nullptr; diff --git a/src/base/bittorrent/torrentinfo.cpp b/src/base/bittorrent/torrentinfo.cpp index 1732320d6..e610b8219 100644 --- a/src/base/bittorrent/torrentinfo.cpp +++ b/src/base/bittorrent/torrentinfo.cpp @@ -75,6 +75,8 @@ namespace } } +const int torrentInfoId = qRegisterMetaType(); + TorrentInfo::TorrentInfo(std::shared_ptr nativeInfo) { m_nativeInfo = std::const_pointer_cast(nativeInfo); diff --git a/src/base/bittorrent/torrentinfo.h b/src/base/bittorrent/torrentinfo.h index 17d4b5ddb..781d81952 100644 --- a/src/base/bittorrent/torrentinfo.h +++ b/src/base/bittorrent/torrentinfo.h @@ -110,3 +110,5 @@ namespace BitTorrent std::shared_ptr m_nativeInfo; }; } + +Q_DECLARE_METATYPE(BitTorrent::TorrentInfo) diff --git a/src/base/exceptions.h b/src/base/exceptions.h index 7d32739ca..32d677acf 100644 --- a/src/base/exceptions.h +++ b/src/base/exceptions.h @@ -45,3 +45,9 @@ class RuntimeError : public Exception public: using Exception::Exception; }; + +class InvalidArgument : public Exception +{ +public: + using Exception::Exception; +}; diff --git a/src/base/filesystemwatcher.cpp b/src/base/filesystemwatcher.cpp deleted file mode 100644 index ccc6bd1d9..000000000 --- a/src/base/filesystemwatcher.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 - * - * 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 "filesystemwatcher.h" - -#include - -#include - -#if defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) -#include -#include -#include -#endif - -#include "base/algorithm.h" -#include "base/bittorrent/torrentinfo.h" -#include "base/global.h" -#include "base/logger.h" -#include "base/utils/fs.h" - -using namespace std::chrono_literals; - -namespace -{ - const std::chrono::duration WATCH_INTERVAL = 10s; - const int MAX_PARTIAL_RETRIES = 5; -} - -FileSystemWatcher::FileSystemWatcher(QObject *parent) - : QFileSystemWatcher(parent) -{ - connect(this, &QFileSystemWatcher::directoryChanged, this, &FileSystemWatcher::scanLocalFolder); - - m_partialTorrentTimer.setSingleShot(true); - connect(&m_partialTorrentTimer, &QTimer::timeout, this, &FileSystemWatcher::processPartialTorrents); - - connect(&m_watchTimer, &QTimer::timeout, this, &FileSystemWatcher::scanNetworkFolders); -} - -QStringList FileSystemWatcher::directories() const -{ - QStringList dirs = QFileSystemWatcher::directories(); - for (const QDir &dir : asConst(m_watchedFolders)) - dirs << dir.canonicalPath(); - return dirs; -} - -void FileSystemWatcher::addPath(const QString &path) -{ - if (path.isEmpty()) return; - -#if !defined Q_OS_HAIKU - const QDir dir(path); - if (!dir.exists()) return; - - // Check if the path points to a network file system or not - if (Utils::Fs::isNetworkFileSystem(path)) - { - // Network mode - LogMsg(tr("Watching remote folder: \"%1\"").arg(Utils::Fs::toNativePath(path))); - m_watchedFolders << dir; - - m_watchTimer.start(WATCH_INTERVAL); - return; - } -#endif - - // Normal mode - LogMsg(tr("Watching local folder: \"%1\"").arg(Utils::Fs::toNativePath(path))); - QFileSystemWatcher::addPath(path); - scanLocalFolder(path); -} - -void FileSystemWatcher::removePath(const QString &path) -{ - if (m_watchedFolders.removeOne(path)) - { - if (m_watchedFolders.isEmpty()) - m_watchTimer.stop(); - return; - } - - // Normal mode - QFileSystemWatcher::removePath(path); -} - -void FileSystemWatcher::scanLocalFolder(const QString &path) -{ - QTimer::singleShot(2000, this, [this, path]() { processTorrentsInDir(path); }); -} - -void FileSystemWatcher::scanNetworkFolders() -{ - for (const QDir &dir : asConst(m_watchedFolders)) - processTorrentsInDir(dir); -} - -void FileSystemWatcher::processPartialTorrents() -{ - QStringList noLongerPartial; - - // Check which torrents are still partial - Algorithm::removeIf(m_partialTorrents, [&noLongerPartial](const QString &torrentPath, int &value) - { - if (!QFile::exists(torrentPath)) - return true; - - if (BitTorrent::TorrentInfo::loadFromFile(torrentPath).isValid()) - { - noLongerPartial << torrentPath; - return true; - } - - if (value >= MAX_PARTIAL_RETRIES) - { - QFile::rename(torrentPath, torrentPath + ".qbt_rejected"); - return true; - } - - ++value; - return false; - }); - - // Stop the partial timer if necessary - if (m_partialTorrents.empty()) - { - m_partialTorrentTimer.stop(); - qDebug("No longer any partial torrent."); - } - else - { - qDebug("Still %d partial torrents after delayed processing.", m_partialTorrents.count()); - m_partialTorrentTimer.start(WATCH_INTERVAL); - } - - // Notify of new torrents - if (!noLongerPartial.isEmpty()) - emit torrentsAdded(noLongerPartial); -} - -void FileSystemWatcher::processTorrentsInDir(const QDir &dir) -{ - QStringList torrents; - const QStringList files = dir.entryList({"*.torrent", "*.magnet"}, QDir::Files); - for (const QString &file : files) - { - const QString fileAbsPath = dir.absoluteFilePath(file); - if (file.endsWith(".magnet", Qt::CaseInsensitive)) - torrents << fileAbsPath; - else if (BitTorrent::TorrentInfo::loadFromFile(fileAbsPath).isValid()) - torrents << fileAbsPath; - else if (!m_partialTorrents.contains(fileAbsPath)) - m_partialTorrents[fileAbsPath] = 0; - } - - if (!torrents.empty()) - emit torrentsAdded(torrents); - - if (!m_partialTorrents.empty() && !m_partialTorrentTimer.isActive()) - m_partialTorrentTimer.start(WATCH_INTERVAL); -} diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 0d9d75e71..868fd74db 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -340,17 +340,6 @@ void Preferences::setLastLocationPath(const QString &path) setValue("Preferences/Downloads/LastLocationPath", Utils::Fs::toUniformPath(path)); } -QVariantHash Preferences::getScanDirs() const -{ - return value("Preferences/Downloads/ScanDirsV2").toHash(); -} - -// This must be called somewhere with data from the model -void Preferences::setScanDirs(const QVariantHash &dirs) -{ - setValue("Preferences/Downloads/ScanDirsV2", dirs); -} - QString Preferences::getScanDirsLastPath() const { return Utils::Fs::toUniformPath(value("Preferences/Downloads/ScanDirsLastPath").toString()); diff --git a/src/base/preferences.h b/src/base/preferences.h index e7df00861..96313588c 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -132,8 +132,6 @@ public: // Downloads QString lastLocationPath() const; void setLastLocationPath(const QString &path); - QVariantHash getScanDirs() const; - void setScanDirs(const QVariantHash &dirs); QString getScanDirsLastPath() const; void setScanDirsLastPath(const QString &path); bool isMailNotificationEnabled() const; diff --git a/src/base/scanfoldersmodel.cpp b/src/base/scanfoldersmodel.cpp deleted file mode 100644 index f48f4cd10..000000000 --- a/src/base/scanfoldersmodel.cpp +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christian Kandeler, Christophe Dumez - * - * 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 "scanfoldersmodel.h" - -#include -#include -#include -#include - -#include "bittorrent/session.h" -#include "filesystemwatcher.h" -#include "global.h" -#include "preferences.h" -#include "utils/fs.h" - -struct ScanFoldersModel::PathData -{ - PathData(const QString &watchPath, const PathType &type, const QString &downloadPath) - : watchPath(watchPath) - , downloadType(type) - , downloadPath(downloadPath) - { - if (this->downloadPath.isEmpty() && downloadType == CUSTOM_LOCATION) - downloadType = DEFAULT_LOCATION; - } - - QString watchPath; - PathType downloadType; - QString downloadPath; // valid for CUSTOM_LOCATION -}; - -ScanFoldersModel *ScanFoldersModel::m_instance = nullptr; - -void ScanFoldersModel::initInstance() -{ - if (!m_instance) - m_instance = new ScanFoldersModel; -} - -void ScanFoldersModel::freeInstance() -{ - delete m_instance; - m_instance = nullptr; -} - -ScanFoldersModel *ScanFoldersModel::instance() -{ - return m_instance; -} - -ScanFoldersModel::ScanFoldersModel(QObject *parent) - : QAbstractListModel(parent) - , m_fsWatcher(nullptr) -{ - configure(); - connect(Preferences::instance(), &Preferences::changed, this, &ScanFoldersModel::configure); -} - -ScanFoldersModel::~ScanFoldersModel() -{ - qDeleteAll(m_pathList); -} - -int ScanFoldersModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : m_pathList.count(); -} - -int ScanFoldersModel::columnCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return NB_COLUMNS; -} - -QVariant ScanFoldersModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || (index.row() >= rowCount())) - return {}; - - const PathData *pathData = m_pathList.at(index.row()); - QVariant value; - - switch (index.column()) - { - case WATCH: - if (role == Qt::DisplayRole) - value = Utils::Fs::toNativePath(pathData->watchPath); - break; - case DOWNLOAD: - if (role == Qt::UserRole) - { - value = pathData->downloadType; - } - else if (role == Qt::DisplayRole) - { - switch (pathData->downloadType) - { - case DOWNLOAD_IN_WATCH_FOLDER: - case DEFAULT_LOCATION: - value = pathTypeDisplayName(pathData->downloadType); - break; - case CUSTOM_LOCATION: - value = pathData->downloadPath; - break; - } - } - break; - } - - return value; -} - -QVariant ScanFoldersModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - if ((orientation != Qt::Horizontal) || (role != Qt::DisplayRole) || (section < 0) || (section >= columnCount())) - return {}; - - QVariant title; - - switch (section) - { - case WATCH: - title = tr("Monitored Folder"); - break; - case DOWNLOAD: - title = tr("Override Save Location"); - break; - } - - return title; -} - -Qt::ItemFlags ScanFoldersModel::flags(const QModelIndex &index) const -{ - if (!index.isValid() || (index.row() >= rowCount())) - return QAbstractListModel::flags(index); - - Qt::ItemFlags flags; - - switch (index.column()) - { - case WATCH: - flags = QAbstractListModel::flags(index); - break; - case DOWNLOAD: - flags = QAbstractListModel::flags(index) | Qt::ItemIsEditable; - break; - } - - return flags; -} - -bool ScanFoldersModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (!index.isValid() || (index.row() >= rowCount()) || (index.column() >= columnCount()) - || (index.column() != DOWNLOAD)) - return false; - - if (role == Qt::UserRole) - { - const auto type = static_cast(value.toInt()); - if (type == CUSTOM_LOCATION) - return false; - - m_pathList[index.row()]->downloadType = type; - m_pathList[index.row()]->downloadPath.clear(); - emit dataChanged(index, index); - } - else if (role == Qt::DisplayRole) - { - const QString path = value.toString(); - if (path.isEmpty()) // means we didn't pass CUSTOM_LOCATION type - return false; - - m_pathList[index.row()]->downloadType = CUSTOM_LOCATION; - m_pathList[index.row()]->downloadPath = Utils::Fs::toNativePath(path); - emit dataChanged(index, index); - } - else - { - return false; - } - - return true; -} - -ScanFoldersModel::PathStatus ScanFoldersModel::addPath(const QString &watchPath, const PathType &downloadType, const QString &downloadPath, bool addToFSWatcher) -{ - const QDir watchDir(watchPath); - if (!watchDir.exists()) return DoesNotExist; - if (!watchDir.isReadable()) return CannotRead; - - const QString canonicalWatchPath = watchDir.canonicalPath(); - if (findPathData(canonicalWatchPath) != -1) return AlreadyInList; - - const QDir downloadDir(downloadPath); - const QString canonicalDownloadPath = downloadDir.canonicalPath(); - - if (!m_fsWatcher) - { - m_fsWatcher = new FileSystemWatcher(this); - connect(m_fsWatcher, &FileSystemWatcher::torrentsAdded, this, &ScanFoldersModel::addTorrentsToSession); - } - - beginInsertRows(QModelIndex(), rowCount(), rowCount()); - m_pathList << new PathData(Utils::Fs::toNativePath(canonicalWatchPath), downloadType, Utils::Fs::toNativePath(canonicalDownloadPath)); - endInsertRows(); - - // Start scanning - if (addToFSWatcher) - m_fsWatcher->addPath(canonicalWatchPath); - return Ok; -} - -ScanFoldersModel::PathStatus ScanFoldersModel::updatePath(const QString &watchPath, const PathType &downloadType, const QString &downloadPath) -{ - const QDir watchDir(watchPath); - const QString canonicalWatchPath = watchDir.canonicalPath(); - const int row = findPathData(canonicalWatchPath); - if (row == -1) return DoesNotExist; - - const QDir downloadDir(downloadPath); - const QString canonicalDownloadPath = downloadDir.canonicalPath(); - - m_pathList.at(row)->downloadType = downloadType; - m_pathList.at(row)->downloadPath = Utils::Fs::toNativePath(canonicalDownloadPath); - - return Ok; -} - -void ScanFoldersModel::addToFSWatcher(const QStringList &watchPaths) -{ - if (!m_fsWatcher) - return; // addPath() wasn't called before this - - for (const QString &path : watchPaths) - { - const QDir watchDir(path); - const QString canonicalWatchPath = watchDir.canonicalPath(); - m_fsWatcher->addPath(canonicalWatchPath); - } -} - -void ScanFoldersModel::removePath(const int row, const bool removeFromFSWatcher) -{ - Q_ASSERT((row >= 0) && (row < rowCount())); - beginRemoveRows(QModelIndex(), row, row); - if (removeFromFSWatcher) - m_fsWatcher->removePath(m_pathList.at(row)->watchPath); - delete m_pathList.takeAt(row); - endRemoveRows(); -} - -bool ScanFoldersModel::removePath(const QString &path, const bool removeFromFSWatcher) -{ - const int row = findPathData(path); - if (row == -1) return false; - - removePath(row, removeFromFSWatcher); - return true; -} - -void ScanFoldersModel::removeFromFSWatcher(const QStringList &watchPaths) -{ - for (const QString &path : watchPaths) - m_fsWatcher->removePath(path); -} - -bool ScanFoldersModel::downloadInWatchFolder(const QString &filePath) const -{ - const int row = findPathData(QFileInfo(filePath).dir().path()); - Q_ASSERT(row != -1); - const PathData *data = m_pathList.at(row); - return (data->downloadType == DOWNLOAD_IN_WATCH_FOLDER); -} - -bool ScanFoldersModel::downloadInDefaultFolder(const QString &filePath) const -{ - const int row = findPathData(QFileInfo(filePath).dir().path()); - Q_ASSERT(row != -1); - const PathData *data = m_pathList.at(row); - return (data->downloadType == DEFAULT_LOCATION); -} - -QString ScanFoldersModel::downloadPathTorrentFolder(const QString &filePath) const -{ - const int row = findPathData(QFileInfo(filePath).dir().path()); - Q_ASSERT(row != -1); - const PathData *data = m_pathList.at(row); - if (data->downloadType == CUSTOM_LOCATION) - return data->downloadPath; - - return {}; -} - -int ScanFoldersModel::findPathData(const QString &path) const -{ - for (int i = 0; i < m_pathList.count(); ++i) - if (m_pathList.at(i)->watchPath == Utils::Fs::toNativePath(path)) - return i; - - return -1; -} - -void ScanFoldersModel::makePersistent() -{ - QVariantHash dirs; - - for (const PathData *pathData : asConst(m_pathList)) - { - if (pathData->downloadType == CUSTOM_LOCATION) - dirs.insert(Utils::Fs::toUniformPath(pathData->watchPath), Utils::Fs::toUniformPath(pathData->downloadPath)); - else - dirs.insert(Utils::Fs::toUniformPath(pathData->watchPath), pathData->downloadType); - } - - Preferences::instance()->setScanDirs(dirs); -} - -void ScanFoldersModel::configure() -{ - const QVariantHash dirs = Preferences::instance()->getScanDirs(); - - for (auto i = dirs.cbegin(); i != dirs.cend(); ++i) - { - if (i.value().type() == QVariant::Int) - addPath(i.key(), static_cast(i.value().toInt()), QString()); - else - addPath(i.key(), CUSTOM_LOCATION, i.value().toString()); - } -} - -void ScanFoldersModel::addTorrentsToSession(const QStringList &pathList) -{ - for (const QString &file : pathList) - { - qDebug("File %s added", qUtf8Printable(file)); - - BitTorrent::AddTorrentParams params; - if (downloadInWatchFolder(file)) - { - params.savePath = QFileInfo(file).dir().path(); - params.useAutoTMM = false; - } - else if (!downloadInDefaultFolder(file)) - { - params.savePath = downloadPathTorrentFolder(file); - params.useAutoTMM = false; - } - - if (file.endsWith(".magnet", Qt::CaseInsensitive)) - { - QFile f(file); - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) - { - QTextStream str(&f); - while (!str.atEnd()) - BitTorrent::Session::instance()->addTorrent(str.readLine(), params); - - f.close(); - Utils::Fs::forceRemove(file); - } - else - { - qDebug("Failed to open magnet file: %s", qUtf8Printable(f.errorString())); - } - } - else - { - const BitTorrent::TorrentInfo torrentInfo = BitTorrent::TorrentInfo::loadFromFile(file); - if (torrentInfo.isValid()) - { - BitTorrent::Session::instance()->addTorrent(torrentInfo, params); - Utils::Fs::forceRemove(file); - } - else - { - qDebug("Ignoring incomplete torrent file: %s", qUtf8Printable(file)); - } - } - } -} - -QString ScanFoldersModel::pathTypeDisplayName(const PathType type) -{ - switch (type) - { - case DOWNLOAD_IN_WATCH_FOLDER: - return tr("Monitored folder"); - case DEFAULT_LOCATION: - return tr("Default save location"); - case CUSTOM_LOCATION: - return tr("Browse..."); - default: - qDebug("Invalid PathType: %d", type); - }; - return {}; -} diff --git a/src/base/scanfoldersmodel.h b/src/base/scanfoldersmodel.h deleted file mode 100644 index fe9d1a50f..000000000 --- a/src/base/scanfoldersmodel.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christian Kandeler, Christophe Dumez - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -#pragma once - -#include -#include -#include - -class FileSystemWatcher; - -class ScanFoldersModel final : public QAbstractListModel -{ - Q_OBJECT - Q_DISABLE_COPY(ScanFoldersModel) - -public: - enum PathStatus - { - Ok, - DoesNotExist, - CannotRead, - CannotWrite, - AlreadyInList - }; - - enum Column - { - WATCH, - DOWNLOAD, - NB_COLUMNS - }; - - enum PathType - { - DOWNLOAD_IN_WATCH_FOLDER, - DEFAULT_LOCATION, - CUSTOM_LOCATION - }; - - static void initInstance(); - static void freeInstance(); - static ScanFoldersModel *instance(); - - static QString pathTypeDisplayName(PathType type); - - int rowCount(const QModelIndex &parent = {}) const override; - int columnCount(const QModelIndex &parent = {}) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - - // TODO: removePaths(); singular version becomes private helper functions; - // also: remove functions should take modelindexes - PathStatus addPath(const QString &watchPath, const PathType &downloadType, const QString &downloadPath, bool addToFSWatcher = true); - PathStatus updatePath(const QString &watchPath, const PathType &downloadType, const QString &downloadPath); - // PRECONDITION: The paths must have been added with addPath() first. - void addToFSWatcher(const QStringList &watchPaths); - void removePath(int row, bool removeFromFSWatcher = true); - bool removePath(const QString &path, bool removeFromFSWatcher = true); - void removeFromFSWatcher(const QStringList &watchPaths); - - void makePersistent(); - -public slots: - void configure(); - -private slots: - void addTorrentsToSession(const QStringList &pathList); - -private: - explicit ScanFoldersModel(QObject *parent = nullptr); - ~ScanFoldersModel(); - - virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - bool downloadInWatchFolder(const QString &filePath) const; - bool downloadInDefaultFolder(const QString &filePath) const; - QString downloadPathTorrentFolder(const QString &filePath) const; - int findPathData(const QString &path) const; - - static ScanFoldersModel *m_instance; - struct PathData; - - QList m_pathList; - FileSystemWatcher *m_fsWatcher; -}; diff --git a/src/base/torrentfileswatcher.cpp b/src/base/torrentfileswatcher.cpp new file mode 100644 index 000000000..82c19053a --- /dev/null +++ b/src/base/torrentfileswatcher.cpp @@ -0,0 +1,658 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * Copyright (C) 2010 Christian Kandeler, Christophe Dumez + * + * 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 "torrentfileswatcher.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/algorithm.h" +#include "base/bittorrent/magneturi.h" +#include "base/bittorrent/torrentcontentlayout.h" +#include "base/bittorrent/session.h" +#include "base/bittorrent/torrent.h" +#include "base/bittorrent/torrentinfo.h" +#include "base/exceptions.h" +#include "base/global.h" +#include "base/logger.h" +#include "base/profile.h" +#include "base/settingsstorage.h" +#include "base/tagset.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" + +using namespace std::chrono_literals; + +const std::chrono::duration WATCH_INTERVAL = 10s; +const int MAX_FAILED_RETRIES = 5; +const QString CONF_FILE_NAME {QStringLiteral("watched_folders.json")}; + +const QString OPTION_ADDTORRENTPARAMS {QStringLiteral("add_torrent_params")}; +const QString OPTION_RECURSIVE {QStringLiteral("recursive")}; + +const QString PARAM_CATEGORY {QStringLiteral("category")}; +const QString PARAM_TAGS {QStringLiteral("tags")}; +const QString PARAM_SAVEPATH {QStringLiteral("save_path")}; +const QString PARAM_OPERATINGMODE {QStringLiteral("operating_mode")}; +const QString PARAM_STOPPED {QStringLiteral("stopped")}; +const QString PARAM_CONTENTLAYOUT {QStringLiteral("content_layout")}; +const QString PARAM_AUTOTMM {QStringLiteral("use_auto_tmm")}; +const QString PARAM_UPLOADLIMIT {QStringLiteral("upload_limit")}; +const QString PARAM_DOWNLOADLIMIT {QStringLiteral("download_limit")}; +const QString PARAM_SEEDINGTIMELIMIT {QStringLiteral("seeding_time_limit")}; +const QString PARAM_RATIOLIMIT {QStringLiteral("ratio_limit")}; + +namespace +{ + TagSet parseTagSet(const QJsonArray &jsonArr) + { + TagSet tags; + for (const QJsonValue &jsonVal : jsonArr) + tags.insert(jsonVal.toString()); + + return tags; + } + + QJsonArray serializeTagSet(const TagSet &tags) + { + QJsonArray arr; + for (const QString &tag : tags) + arr.append(tag); + + return arr; + } + + std::optional getOptionalBool(const QJsonObject &jsonObj, const QString &key) + { + const QJsonValue jsonVal = jsonObj.value(key); + if (jsonVal.isUndefined() || jsonVal.isNull()) + return std::nullopt; + + return jsonVal.toBool(); + } + + template + std::optional getOptionalEnum(const QJsonObject &jsonObj, const QString &key) + { + const QJsonValue jsonVal = jsonObj.value(key); + if (jsonVal.isUndefined() || jsonVal.isNull()) + return std::nullopt; + + return Utils::String::toEnum(jsonVal.toString(), {}); + } + + template + Enum getEnum(const QJsonObject &jsonObj, const QString &key) + { + const QJsonValue jsonVal = jsonObj.value(key); + return Utils::String::toEnum(jsonVal.toString(), {}); + } + + BitTorrent::AddTorrentParams parseAddTorrentParams(const QJsonObject &jsonObj) + { + BitTorrent::AddTorrentParams params; + params.category = jsonObj.value(PARAM_CATEGORY).toString(); + params.tags = parseTagSet(jsonObj.value(PARAM_TAGS).toArray()); + params.savePath = jsonObj.value(PARAM_SAVEPATH).toString(); + params.addForced = (getEnum(jsonObj, PARAM_OPERATINGMODE) == BitTorrent::TorrentOperatingMode::Forced); + params.addPaused = getOptionalBool(jsonObj, PARAM_STOPPED); + params.contentLayout = getOptionalEnum(jsonObj, PARAM_CONTENTLAYOUT); + params.useAutoTMM = getOptionalBool(jsonObj, PARAM_AUTOTMM); + params.uploadLimit = jsonObj.value(PARAM_UPLOADLIMIT).toInt(-1); + params.downloadLimit = jsonObj.value(PARAM_DOWNLOADLIMIT).toInt(-1); + params.seedingTimeLimit = jsonObj.value(PARAM_SEEDINGTIMELIMIT).toInt(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME); + params.ratioLimit = jsonObj.value(PARAM_RATIOLIMIT).toDouble(BitTorrent::Torrent::USE_GLOBAL_RATIO); + + return params; + } + + QJsonObject serializeAddTorrentParams(const BitTorrent::AddTorrentParams ¶ms) + { + QJsonObject jsonObj { + {PARAM_CATEGORY, params.category}, + {PARAM_TAGS, serializeTagSet(params.tags)}, + {PARAM_SAVEPATH, params.savePath}, + {PARAM_OPERATINGMODE, Utils::String::fromEnum(params.addForced + ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged)}, + {PARAM_UPLOADLIMIT, params.uploadLimit}, + {PARAM_DOWNLOADLIMIT, params.downloadLimit}, + {PARAM_SEEDINGTIMELIMIT, params.seedingTimeLimit}, + {PARAM_RATIOLIMIT, params.ratioLimit} + }; + + if (params.addPaused) + jsonObj[PARAM_STOPPED] = *params.addPaused; + if (params.contentLayout) + jsonObj[PARAM_CONTENTLAYOUT] = Utils::String::fromEnum(*params.contentLayout); + if (params.useAutoTMM) + jsonObj[PARAM_AUTOTMM] = *params.useAutoTMM; + + return jsonObj; + } + + TorrentFilesWatcher::WatchedFolderOptions parseWatchedFolderOptions(const QJsonObject &jsonObj) + { + TorrentFilesWatcher::WatchedFolderOptions options; + options.addTorrentParams = parseAddTorrentParams(jsonObj.value(OPTION_ADDTORRENTPARAMS).toObject()); + options.recursive = jsonObj.value(OPTION_RECURSIVE).toBool(); + + return options; + } + + QJsonObject serializeWatchedFolderOptions(const TorrentFilesWatcher::WatchedFolderOptions &options) + { + return { + {OPTION_ADDTORRENTPARAMS, serializeAddTorrentParams(options.addTorrentParams)}, + {OPTION_RECURSIVE, options.recursive} + }; + } +} + +class TorrentFilesWatcher::Worker final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(Worker) + +public: + Worker(); + +public slots: + void setWatchedFolder(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options); + void removeWatchedFolder(const QString &path); + +signals: + void magnetFound(const BitTorrent::MagnetUri &magnetURI, const BitTorrent::AddTorrentParams &addTorrentParams); + void torrentFound(const BitTorrent::TorrentInfo &torrentInfo, const BitTorrent::AddTorrentParams &addTorrentParams); + +private: + void onTimeout(); + void processWatchedFolder(const QString &path); + void processFolder(const QString &path, const QString &watchedFolderPath, const TorrentFilesWatcher::WatchedFolderOptions &options); + void processFailedTorrents(); + void addWatchedFolder(const QString &watchedFolderID, const TorrentFilesWatcher::WatchedFolderOptions &options); + void updateWatchedFolder(const QString &watchedFolderID, const TorrentFilesWatcher::WatchedFolderOptions &options); + + QFileSystemWatcher *m_watcher = nullptr; + QTimer *m_watchTimer = nullptr; + QHash m_watchedFolders; + QSet m_watchedByTimeoutFolders; + + // Failed torrents + QTimer *m_retryTorrentTimer = nullptr; + QHash> m_failedTorrents; +}; + +TorrentFilesWatcher *TorrentFilesWatcher::m_instance = nullptr; + +void TorrentFilesWatcher::initInstance() +{ + if (!m_instance) + m_instance = new TorrentFilesWatcher; +} + +void TorrentFilesWatcher::freeInstance() +{ + delete m_instance; + m_instance = nullptr; +} + +TorrentFilesWatcher *TorrentFilesWatcher::instance() +{ + return m_instance; +} + +TorrentFilesWatcher::TorrentFilesWatcher(QObject *parent) + : QObject {parent} + , m_ioThread {new QThread(this)} + , m_asyncWorker {new TorrentFilesWatcher::Worker} +{ + connect(m_asyncWorker, &TorrentFilesWatcher::Worker::magnetFound, this, &TorrentFilesWatcher::onMagnetFound); + connect(m_asyncWorker, &TorrentFilesWatcher::Worker::torrentFound, this, &TorrentFilesWatcher::onTorrentFound); + + m_asyncWorker->moveToThread(m_ioThread); + m_ioThread->start(); + + load(); +} + +TorrentFilesWatcher::~TorrentFilesWatcher() +{ + m_ioThread->quit(); + m_ioThread->wait(); + delete m_asyncWorker; +} + +QString TorrentFilesWatcher::makeCleanPath(const QString &path) +{ + if (path.isEmpty()) + throw InvalidArgument(tr("Watched folder path cannot be empty.")); + + const QDir dir {path}; + if (dir.isRelative()) + throw InvalidArgument(tr("Watched folder path cannot be relative.")); + + return dir.canonicalPath(); +} + +void TorrentFilesWatcher::load() +{ + QFile confFile {QDir(specialFolderLocation(SpecialFolder::Config)).absoluteFilePath(CONF_FILE_NAME)}; + if (!confFile.exists()) + { + loadLegacy(); + return; + } + + if (!confFile.open(QFile::ReadOnly)) + { + LogMsg(tr("Couldn't load Watched Folders configuration from %1. Error: %2") + .arg(confFile.fileName(), confFile.errorString()), Log::WARNING); + return; + } + + QJsonParseError jsonError; + const QJsonDocument jsonDoc = QJsonDocument::fromJson(confFile.readAll(), &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + LogMsg(tr("Couldn't parse Watched Folders configuration from %1. Error: %2") + .arg(confFile.fileName(), jsonError.errorString()), Log::WARNING); + return; + } + + if (!jsonDoc.isObject()) + { + LogMsg(tr("Couldn't load Watched Folders configuration from %1. Invalid data format.") + .arg(confFile.fileName()), Log::WARNING); + return; + } + + const QJsonObject jsonObj = jsonDoc.object(); + for (auto it = jsonObj.constBegin(); it != jsonObj.constEnd(); ++it) + { + const QString &watchedFolder = it.key(); + const WatchedFolderOptions options = parseWatchedFolderOptions(it.value().toObject()); + try + { + doSetWatchedFolder(watchedFolder, options); + } + catch (const InvalidArgument &err) + { + LogMsg(err.message(), Log::WARNING); + } + } +} + +void TorrentFilesWatcher::loadLegacy() +{ + const auto dirs = SettingsStorage::instance()->loadValue("Preferences/Downloads/ScanDirsV2"); + + for (auto i = dirs.cbegin(); i != dirs.cend(); ++i) + { + const QString watchedFolder = i.key(); + BitTorrent::AddTorrentParams params; + if (i.value().type() == QVariant::Int) + { + if (i.value().toInt() == 0) + { + params.savePath = watchedFolder; + params.useAutoTMM = false; + } + } + else + { + const QString customSavePath = i.value().toString(); + params.savePath = customSavePath; + params.useAutoTMM = false; + } + + try + { + doSetWatchedFolder(watchedFolder, {params, false}); + } + catch (const InvalidArgument &err) + { + LogMsg(err.message(), Log::WARNING); + } + } + + store(); + SettingsStorage::instance()->removeValue("Preferences/Downloads/ScanDirsV2"); +} + +void TorrentFilesWatcher::store() const +{ + QJsonObject jsonObj; + for (auto it = m_watchedFolders.cbegin(); it != m_watchedFolders.cend(); ++it) + { + const QString &watchedFolder = it.key(); + const WatchedFolderOptions &options = it.value(); + jsonObj[watchedFolder] = serializeWatchedFolderOptions(options); + } + + const QByteArray data = QJsonDocument(jsonObj).toJson(); + + QSaveFile confFile {QDir(specialFolderLocation(SpecialFolder::Config)).absoluteFilePath(CONF_FILE_NAME)}; + if (!confFile.open(QIODevice::WriteOnly) || (confFile.write(data) != data.size()) || !confFile.commit()) + { + LogMsg(tr("Couldn't store Watched Folders configuration to %1. Error: %2") + .arg(confFile.fileName(), confFile.errorString()), Log::WARNING); + } +} + +QHash TorrentFilesWatcher::folders() const +{ + return m_watchedFolders; +} + +void TorrentFilesWatcher::setWatchedFolder(const QString &path, const WatchedFolderOptions &options) +{ + doSetWatchedFolder(path, options); + store(); +} + +void TorrentFilesWatcher::doSetWatchedFolder(const QString &path, const WatchedFolderOptions &options) +{ + const QString cleanPath = makeCleanPath(path); + m_watchedFolders[cleanPath] = options; + + QMetaObject::invokeMethod(m_asyncWorker, [this, path, options]() + { + m_asyncWorker->setWatchedFolder(path, options); + }); + + emit watchedFolderSet(cleanPath, options); +} + +void TorrentFilesWatcher::removeWatchedFolder(const QString &path) +{ + const QString cleanPath = makeCleanPath(path); + if (m_watchedFolders.remove(cleanPath)) + { + QMetaObject::invokeMethod(m_asyncWorker, [this, cleanPath]() + { + m_asyncWorker->removeWatchedFolder(cleanPath); + }); + + emit watchedFolderRemoved(cleanPath); + + store(); + } +} + +void TorrentFilesWatcher::onMagnetFound(const BitTorrent::MagnetUri &magnetURI + , const BitTorrent::AddTorrentParams &addTorrentParams) +{ + BitTorrent::Session::instance()->addTorrent(magnetURI, addTorrentParams); +} + +void TorrentFilesWatcher::onTorrentFound(const BitTorrent::TorrentInfo &torrentInfo + , const BitTorrent::AddTorrentParams &addTorrentParams) +{ + BitTorrent::Session::instance()->addTorrent(torrentInfo, addTorrentParams); +} + +TorrentFilesWatcher::Worker::Worker() + : m_watcher {new QFileSystemWatcher(this)} + , m_watchTimer {new QTimer(this)} + , m_retryTorrentTimer {new QTimer(this)} +{ + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &Worker::processWatchedFolder); + connect(m_watchTimer, &QTimer::timeout, this, &Worker::onTimeout); + + connect(m_retryTorrentTimer, &QTimer::timeout, this, &Worker::processFailedTorrents); +} + +void TorrentFilesWatcher::Worker::onTimeout() +{ + for (const QString &path : asConst(m_watchedByTimeoutFolders)) + processWatchedFolder(path); +} + +void TorrentFilesWatcher::Worker::setWatchedFolder(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options) +{ + if (m_watchedFolders.contains(path)) + updateWatchedFolder(path, options); + else + addWatchedFolder(path, options); +} + +void TorrentFilesWatcher::Worker::removeWatchedFolder(const QString &path) +{ + m_watchedFolders.remove(path); + + m_watcher->removePath(path); + m_watchedByTimeoutFolders.remove(path); + if (m_watchedByTimeoutFolders.isEmpty()) + m_watchTimer->stop(); + + m_failedTorrents.remove(path); + if (m_failedTorrents.isEmpty()) + m_retryTorrentTimer->stop(); +} + +void TorrentFilesWatcher::Worker::processWatchedFolder(const QString &path) +{ + const TorrentFilesWatcher::WatchedFolderOptions options = m_watchedFolders.value(path); + processFolder(path, path, options); + + if (!m_failedTorrents.empty() && !m_retryTorrentTimer->isActive()) + m_retryTorrentTimer->start(WATCH_INTERVAL); +} + +void TorrentFilesWatcher::Worker::processFolder(const QString &path, const QString &watchedFolderPath + , const TorrentFilesWatcher::WatchedFolderOptions &options) +{ + const QDir watchedDir {watchedFolderPath}; + + QDirIterator dirIter {path, {"*.torrent", "*.magnet"}, QDir::Files}; + while (dirIter.hasNext()) + { + const QString filePath = dirIter.next(); + BitTorrent::AddTorrentParams addTorrentParams = options.addTorrentParams; + if (path != watchedFolderPath) + { + const QString subdirPath = watchedDir.relativeFilePath(path); + addTorrentParams.savePath = QDir::cleanPath(QDir(addTorrentParams.savePath).filePath(subdirPath)); + } + + if (filePath.endsWith(QLatin1String(".magnet"), Qt::CaseInsensitive)) + { + QFile file {filePath}; + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream str {&file}; + while (!str.atEnd()) + emit magnetFound(BitTorrent::MagnetUri(str.readLine()), addTorrentParams); + + file.close(); + Utils::Fs::forceRemove(filePath); + } + else + { + LogMsg(tr("Failed to open magnet file: %1").arg(file.errorString())); + } + } + else + { + const auto torrentInfo = BitTorrent::TorrentInfo::loadFromFile(filePath); + if (torrentInfo.isValid()) + { + emit torrentFound(torrentInfo, addTorrentParams); + Utils::Fs::forceRemove(filePath); + } + else + { + if (!m_failedTorrents.value(path).contains(filePath)) + { + m_failedTorrents[path][filePath] = 0; + } + } + } + } + + if (options.recursive) + { + QDirIterator dirIter {path, (QDir::Dirs | QDir::NoDot | QDir::NoDotDot)}; + while (dirIter.hasNext()) + { + const QString folderPath = dirIter.next(); + // Skip processing of subdirectory that is explicitly set as watched folder + if (!m_watchedFolders.contains(folderPath)) + processFolder(folderPath, watchedFolderPath, options); + } + } +} + +void TorrentFilesWatcher::Worker::processFailedTorrents() +{ + // Check which torrents are still partial + Algorithm::removeIf(m_failedTorrents, [this](const QString &watchedFolderPath, QHash &partialTorrents) + { + const QDir dir {watchedFolderPath}; + const TorrentFilesWatcher::WatchedFolderOptions options = m_watchedFolders.value(watchedFolderPath); + Algorithm::removeIf(partialTorrents, [this, &dir, &options](const QString &torrentPath, int &value) + { + if (!QFile::exists(torrentPath)) + return true; + + const auto torrentInfo = BitTorrent::TorrentInfo::loadFromFile(torrentPath); + if (torrentInfo.isValid()) + { + BitTorrent::AddTorrentParams addTorrentParams = options.addTorrentParams; + const QString exactDirPath = QFileInfo(torrentPath).canonicalPath(); + if (exactDirPath != dir.path()) + { + const QString subdirPath = dir.relativeFilePath(exactDirPath); + addTorrentParams.savePath = QDir(addTorrentParams.savePath).filePath(subdirPath); + } + + emit torrentFound(torrentInfo, addTorrentParams); + Utils::Fs::forceRemove(torrentPath); + + return true; + } + + if (value >= MAX_FAILED_RETRIES) + { + LogMsg(tr("Rejecting failed torrent file: %1").arg(torrentPath)); + QFile::rename(torrentPath, torrentPath + ".qbt_rejected"); + return true; + } + + ++value; + return false; + }); + + if (partialTorrents.isEmpty()) + return true; + + return false; + }); + + // Stop the partial timer if necessary + if (m_failedTorrents.empty()) + m_retryTorrentTimer->stop(); + else + m_retryTorrentTimer->start(WATCH_INTERVAL); +} + +void TorrentFilesWatcher::Worker::addWatchedFolder(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options) +{ +#if !defined Q_OS_HAIKU + // Check if the path points to a network file system or not + if (Utils::Fs::isNetworkFileSystem(path)) + { + m_watchedByTimeoutFolders.insert(path); + } + else +#endif + if (options.recursive) + { + m_watchedByTimeoutFolders.insert(path); + if (!m_watchTimer->isActive()) + m_watchTimer->start(WATCH_INTERVAL); + } + else + { + m_watcher->addPath(path); + QTimer::singleShot(2000, this, [this, path]() { processWatchedFolder(path); }); + } + + m_watchedFolders[path] = options; + + LogMsg(tr("Watching folder: \"%1\"").arg(Utils::Fs::toNativePath(path))); +} + +void TorrentFilesWatcher::Worker::updateWatchedFolder(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options) +{ + const bool recursiveModeChanged = (m_watchedFolders[path].recursive != options.recursive); +#if !defined Q_OS_HAIKU + if (recursiveModeChanged && !Utils::Fs::isNetworkFileSystem(path)) +#else + if (recursiveModeChanged) +#endif + { + if (options.recursive) + { + m_watcher->removePath(path); + + m_watchedByTimeoutFolders.insert(path); + if (!m_watchTimer->isActive()) + m_watchTimer->start(WATCH_INTERVAL); + } + else + { + m_watchedByTimeoutFolders.remove(path); + if (m_watchedByTimeoutFolders.isEmpty()) + m_watchTimer->stop(); + + m_watcher->addPath(path); + QTimer::singleShot(2000, this, [this, path]() { processWatchedFolder(path); }); + } + } + + m_watchedFolders[path] = options; +} + +#include "torrentfileswatcher.moc" diff --git a/src/base/torrentfileswatcher.h b/src/base/torrentfileswatcher.h new file mode 100644 index 000000000..f0c928d34 --- /dev/null +++ b/src/base/torrentfileswatcher.h @@ -0,0 +1,96 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * Copyright (C) 2010 Christian Kandeler, Christophe Dumez + * + * 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/bittorrent/addtorrentparams.h" + +class QThread; + +namespace BitTorrent +{ + class MagnetUri; +} + +/* + * Watches the configured directories for new .torrent files in order + * to add torrents to BitTorrent session. Supports Network File System + * watching (NFS, CIFS) on Linux and Mac OS. + */ +class TorrentFilesWatcher final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(TorrentFilesWatcher) + +public: + struct WatchedFolderOptions + { + BitTorrent::AddTorrentParams addTorrentParams; + bool recursive = false; + }; + + static void initInstance(); + static void freeInstance(); + static TorrentFilesWatcher *instance(); + + static QString makeCleanPath(const QString &path); + + QHash folders() const; + void setWatchedFolder(const QString &path, const WatchedFolderOptions &options); + void removeWatchedFolder(const QString &path); + +signals: + void watchedFolderSet(const QString &path, const WatchedFolderOptions &options); + void watchedFolderRemoved(const QString &path); + +private slots: + void onMagnetFound(const BitTorrent::MagnetUri &magnetURI, const BitTorrent::AddTorrentParams &addTorrentParams); + void onTorrentFound(const BitTorrent::TorrentInfo &torrentInfo, const BitTorrent::AddTorrentParams &addTorrentParams); + +private: + explicit TorrentFilesWatcher(QObject *parent = nullptr); + ~TorrentFilesWatcher() override; + + void load(); + void loadLegacy(); + void store() const; + + void doSetWatchedFolder(const QString &path, const WatchedFolderOptions &options); + + static TorrentFilesWatcher *m_instance; + + QHash m_watchedFolders; + + QThread *m_ioThread = nullptr; + + class Worker; + Worker *m_asyncWorker = nullptr; +}; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index d8af86fdc..78e899798 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -46,7 +46,6 @@ add_library(qbt_gui STATIC rss/feedlistwidget.h rss/htmlbrowser.h rss/rsswidget.h - scanfoldersdelegate.h search/pluginselectdialog.h search/pluginsourcedialog.h search/searchjobwidget.h @@ -78,6 +77,8 @@ add_library(qbt_gui STATIC tristatewidget.h uithememanager.h utils.h + watchedfolderoptionsdialog.h + watchedfoldersmodel.h # sources aboutdialog.cpp @@ -126,7 +127,6 @@ add_library(qbt_gui STATIC rss/feedlistwidget.cpp rss/htmlbrowser.cpp rss/rsswidget.cpp - scanfoldersdelegate.cpp search/pluginselectdialog.cpp search/pluginsourcedialog.cpp search/searchjobwidget.cpp @@ -158,6 +158,8 @@ add_library(qbt_gui STATIC tristatewidget.cpp uithememanager.cpp utils.cpp + watchedfolderoptionsdialog.cpp + watchedfoldersmodel.cpp # forms aboutdialog.ui @@ -188,6 +190,7 @@ add_library(qbt_gui STATIC torrentcreatordialog.ui torrentoptionsdialog.ui trackerentriesdialog.ui + watchedfolderoptionsdialog.ui ) target_sources(qbt_gui INTERFACE about.qrc) diff --git a/src/gui/gui.pri b/src/gui/gui.pri index f49e5e70f..6d8a1d84f 100644 --- a/src/gui/gui.pri +++ b/src/gui/gui.pri @@ -47,7 +47,6 @@ HEADERS += \ $$PWD/rss/feedlistwidget.h \ $$PWD/rss/htmlbrowser.h \ $$PWD/rss/rsswidget.h \ - $$PWD/scanfoldersdelegate.h \ $$PWD/search/pluginselectdialog.h \ $$PWD/search/pluginsourcedialog.h \ $$PWD/search/searchjobwidget.h \ @@ -78,7 +77,9 @@ HEADERS += \ $$PWD/tristateaction.h \ $$PWD/tristatewidget.h \ $$PWD/uithememanager.h \ - $$PWD/utils.h + $$PWD/utils.h \ + $$PWD/watchedfolderoptionsdialog.h \ + $$PWD/watchedfoldersmodel.h SOURCES += \ $$PWD/aboutdialog.cpp \ @@ -127,7 +128,6 @@ SOURCES += \ $$PWD/rss/feedlistwidget.cpp \ $$PWD/rss/htmlbrowser.cpp \ $$PWD/rss/rsswidget.cpp \ - $$PWD/scanfoldersdelegate.cpp \ $$PWD/search/pluginselectdialog.cpp \ $$PWD/search/pluginsourcedialog.cpp \ $$PWD/search/searchjobwidget.cpp \ @@ -158,7 +158,9 @@ SOURCES += \ $$PWD/tristateaction.cpp \ $$PWD/tristatewidget.cpp \ $$PWD/uithememanager.cpp \ - $$PWD/utils.cpp + $$PWD/utils.cpp \ + $$PWD/watchedfolderoptionsdialog.cpp \ + $$PWD/watchedfoldersmodel.cpp win32|macx { HEADERS += $$PWD/programupdater.h @@ -208,6 +210,7 @@ FORMS += \ $$PWD/torrentcategorydialog.ui \ $$PWD/torrentcreatordialog.ui \ $$PWD/torrentoptionsdialog.ui \ - $$PWD/trackerentriesdialog.ui + $$PWD/trackerentriesdialog.ui \ + $$PWD/watchedfolderoptionsdialog.ui RESOURCES += $$PWD/about.qrc diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 4de02aa9f..1bbd09dc6 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -43,6 +43,7 @@ #include #include "base/bittorrent/session.h" +#include "base/exceptions.h" #include "base/global.h" #include "base/net/dnsupdater.h" #include "base/net/portforwarder.h" @@ -50,8 +51,8 @@ #include "base/preferences.h" #include "base/rss/rss_autodownloader.h" #include "base/rss/rss_session.h" -#include "base/scanfoldersmodel.h" #include "base/torrentfileguard.h" +#include "base/torrentfileswatcher.h" #include "base/unicodestrings.h" #include "base/utils/fs.h" #include "base/utils/net.h" @@ -63,10 +64,11 @@ #include "banlistoptionsdialog.h" #include "ipsubnetwhitelistoptionsdialog.h" #include "rss/automatedrssdownloader.h" -#include "scanfoldersdelegate.h" #include "ui_optionsdialog.h" #include "uithememanager.h" #include "utils.h" +#include "watchedfolderoptionsdialog.h" +#include "watchedfoldersmodel.h" #define SETTINGS_KEY(name) "OptionsDialog/" name @@ -236,11 +238,12 @@ OptionsDialog::OptionsDialog(QWidget *parent) m_applyButton = m_ui->buttonBox->button(QDialogButtonBox::Apply); connect(m_applyButton, &QPushButton::clicked, this, &OptionsDialog::applySettings); + auto watchedFoldersModel = new WatchedFoldersModel(TorrentFilesWatcher::instance(), this); + connect(watchedFoldersModel, &QAbstractListModel::dataChanged, this, &ThisType::enableApplyButton); m_ui->scanFoldersView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); - m_ui->scanFoldersView->setModel(ScanFoldersModel::instance()); - m_ui->scanFoldersView->setItemDelegate(new ScanFoldersDelegate(this, m_ui->scanFoldersView)); - connect(ScanFoldersModel::instance(), &QAbstractListModel::dataChanged, this, &ThisType::enableApplyButton); - connect(m_ui->scanFoldersView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ThisType::handleScanFolderViewSelectionChanged); + m_ui->scanFoldersView->setModel(watchedFoldersModel); + connect(m_ui->scanFoldersView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ThisType::handleWatchedFolderViewSelectionChanged); + connect(m_ui->scanFoldersView, &QTreeView::doubleClicked, this, &ThisType::editWatchedFolderOptions); // Languages supported initializeLanguageCombo(); @@ -368,8 +371,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->actionTorrentFnOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkTempFolder, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkTempFolder, &QAbstractButton::toggled, m_ui->textTempPath, &QWidget::setEnabled); - connect(m_ui->addScanFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); - connect(m_ui->removeScanFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); + connect(m_ui->addWatchedFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); + connect(m_ui->removeWatchedFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); connect(m_ui->groupMailNotification, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->senderEmailTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->lineEditDestEmail, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); @@ -609,9 +612,6 @@ OptionsDialog::~OptionsDialog() hSplitterSizes.append(QString::number(size)); m_storeHSplitterSize = hSplitterSizes; - for (const QString &path : asConst(m_addedScanDirs)) - ScanFoldersModel::instance()->removePath(path); - ScanFoldersModel::instance()->configure(); // reloads "removed" paths delete m_ui; } @@ -736,11 +736,8 @@ void OptionsDialog::saveOptions() AddNewTorrentDialog::setTopLevel(m_ui->checkAdditionDialogFront->isChecked()); session->setAddTorrentPaused(addTorrentsInPause()); session->setTorrentContentLayout(static_cast(m_ui->contentLayoutComboBox->currentIndex())); - ScanFoldersModel::instance()->removeFromFSWatcher(m_removedScanDirs); - ScanFoldersModel::instance()->addToFSWatcher(m_addedScanDirs); - ScanFoldersModel::instance()->makePersistent(); - m_removedScanDirs.clear(); - m_addedScanDirs.clear(); + auto watchedFoldersModel = static_cast(m_ui->scanFoldersView->model()); + watchedFoldersModel->apply(); session->setTorrentExportDirectory(getTorrentExportDir()); session->setFinishedTorrentExportDirectory(getFinishedTorrentExportDir()); pref->setMailNotificationEnabled(m_ui->groupMailNotification->isChecked()); @@ -1648,57 +1645,85 @@ int OptionsDialog::getActionOnDblClOnTorrentFn() const return m_ui->actionTorrentFnOnDblClBox->currentIndex(); } -void OptionsDialog::on_addScanFolderButton_clicked() +void OptionsDialog::on_addWatchedFolderButton_clicked() { Preferences *const pref = Preferences::instance(); const QString dir = QFileDialog::getExistingDirectory(this, tr("Select folder to monitor"), - Utils::Fs::toNativePath(Utils::Fs::folderName(pref->getScanDirsLastPath()))); - if (!dir.isEmpty()) + Utils::Fs::toNativePath(Utils::Fs::folderName(pref->getScanDirsLastPath()))); + if (dir.isEmpty()) + return; + + auto dialog = new WatchedFolderOptionsDialog({}, this); + dialog->setModal(true); + dialog->setAttribute(Qt::WA_DeleteOnClose); + connect(dialog, &QDialog::accepted, this, [this, dialog, dir, pref]() { - const ScanFoldersModel::PathStatus status = ScanFoldersModel::instance()->addPath(dir, ScanFoldersModel::DEFAULT_LOCATION, QString(), false); - QString error; - switch (status) + try { - case ScanFoldersModel::AlreadyInList: - error = tr("Folder is already being monitored:"); - break; - case ScanFoldersModel::DoesNotExist: - error = tr("Folder does not exist:"); - break; - case ScanFoldersModel::CannotRead: - error = tr("Folder is not readable:"); - break; - default: + auto watchedFoldersModel = static_cast(m_ui->scanFoldersView->model()); + watchedFoldersModel->addFolder(dir, dialog->watchedFolderOptions()); + pref->setScanDirsLastPath(dir); - m_addedScanDirs << dir; - for (int i = 0; i < ScanFoldersModel::instance()->columnCount(); ++i) + + for (int i = 0; i < watchedFoldersModel->columnCount(); ++i) m_ui->scanFoldersView->resizeColumnToContents(i); + enableApplyButton(); } + catch (const RuntimeError &err) + { + QMessageBox::critical(this, tr("Adding entry failed"), err.message()); + } + }); - if (!error.isEmpty()) - QMessageBox::critical(this, tr("Adding entry failed"), QString::fromLatin1("%1\n%2").arg(error, dir)); - } + dialog->open(); +} + +void OptionsDialog::on_editWatchedFolderButton_clicked() +{ + const QModelIndex selected + = m_ui->scanFoldersView->selectionModel()->selectedIndexes().at(0); + + editWatchedFolderOptions(selected); } -void OptionsDialog::on_removeScanFolderButton_clicked() +void OptionsDialog::on_removeWatchedFolderButton_clicked() { const QModelIndexList selected = m_ui->scanFoldersView->selectionModel()->selectedIndexes(); - if (selected.isEmpty()) - return; - Q_ASSERT(selected.count() == ScanFoldersModel::instance()->columnCount()); + for (const QModelIndex &index : selected) - { - if (index.column() == ScanFoldersModel::WATCH) - m_removedScanDirs << index.data().toString(); - } - ScanFoldersModel::instance()->removePath(selected.first().row(), false); + m_ui->scanFoldersView->model()->removeRow(index.row()); +} + +void OptionsDialog::handleWatchedFolderViewSelectionChanged() +{ + const QModelIndexList selectedIndexes = m_ui->scanFoldersView->selectionModel()->selectedIndexes(); + m_ui->removeWatchedFolderButton->setEnabled(!selectedIndexes.isEmpty()); + m_ui->editWatchedFolderButton->setEnabled(selectedIndexes.count() == 1); } -void OptionsDialog::handleScanFolderViewSelectionChanged() +void OptionsDialog::editWatchedFolderOptions(const QModelIndex &index) { - m_ui->removeScanFolderButton->setEnabled(!m_ui->scanFoldersView->selectionModel()->selectedIndexes().isEmpty()); + if (!index.isValid()) + return; + + auto watchedFoldersModel = static_cast(m_ui->scanFoldersView->model()); + auto dialog = new WatchedFolderOptionsDialog(watchedFoldersModel->folderOptions(index.row()), this); + dialog->setModal(true); + dialog->setAttribute(Qt::WA_DeleteOnClose); + connect(dialog, &QDialog::accepted, this, [this, dialog, index, watchedFoldersModel]() + { + if (index.isValid()) + { + // The index could be invalidated while the dialog was displayed, + // for example, if you deleted the folder using the Web API. + watchedFoldersModel->setFolderOptions(index.row(), dialog->watchedFolderOptions()); + enableApplyButton(); + } + }); + + dialog->open(); } QString OptionsDialog::askForExportDir(const QString ¤tExportPath) diff --git a/src/gui/optionsdialog.h b/src/gui/optionsdialog.h index b9c374be3..b303dd789 100644 --- a/src/gui/optionsdialog.h +++ b/src/gui/optionsdialog.h @@ -99,14 +99,16 @@ private slots: void toggleComboRatioLimitAct(); void changePage(QListWidgetItem *, QListWidgetItem *); void loadSplitterState(); - void handleScanFolderViewSelectionChanged(); + void handleWatchedFolderViewSelectionChanged(); + void editWatchedFolderOptions(const QModelIndex &index); void on_IpFilterRefreshBtn_clicked(); void handleIPFilterParsed(bool error, int ruleCount); void on_banListButton_clicked(); void on_IPSubnetWhitelistButton_clicked(); void on_randomButton_clicked(); - void on_addScanFolderButton_clicked(); - void on_removeScanFolderButton_clicked(); + void on_addWatchedFolderButton_clicked(); + void on_editWatchedFolderButton_clicked(); + void on_removeWatchedFolderButton_clicked(); void on_registerDNSBtn_clicked(); void setLocale(const QString &localeStr); void webUIHttpsCertChanged(const QString &path, ShowError showError); @@ -184,8 +186,5 @@ private: AdvancedSettings *m_advancedSettings; - QList m_addedScanDirs; - QList m_removedScanDirs; - bool m_refreshingIpFilter = false; }; diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index f27359d15..8b394e971 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -1183,19 +1183,29 @@ Manual: Various torrent properties (e.g. save path) must be assigned manually - + - Add entry + Add... - + false - Remove entry + Options.. + + + + + + + false + + + Remove @@ -3469,8 +3479,9 @@ Use ';' to split multiple entries. Can use wildcard '*'. textTempPath checkAppendqB scanFoldersView - addScanFolderButton - removeScanFolderButton + addWatchedFolderButton + editWatchedFolderButton + removeWatchedFolderButton checkExportDir textExportDir checkExportDirFin diff --git a/src/gui/scanfoldersdelegate.cpp b/src/gui/scanfoldersdelegate.cpp deleted file mode 100644 index ec1a7fe45..000000000 --- a/src/gui/scanfoldersdelegate.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 sledgehammer999 - * - * 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 "scanfoldersdelegate.h" - -#include -#include -#include -#include - -#include "base/bittorrent/session.h" -#include "base/scanfoldersmodel.h" - -ScanFoldersDelegate::ScanFoldersDelegate(QObject *parent, QTreeView *foldersView) - : QStyledItemDelegate(parent) - , m_folderView(foldersView) -{ -} - -void ScanFoldersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - auto *combobox = static_cast(editor); - // Set combobox index - if (index.data(Qt::UserRole).toInt() == ScanFoldersModel::CUSTOM_LOCATION) - combobox->setCurrentIndex(4); // '4' is the index of the item after the separator in the QComboBox menu - else - combobox->setCurrentIndex(index.data(Qt::UserRole).toInt()); -} - -QWidget *ScanFoldersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const -{ - if (index.column() != ScanFoldersModel::DOWNLOAD) return nullptr; - - auto *editor = new QComboBox(parent); - - editor->setFocusPolicy(Qt::StrongFocus); - editor->addItem(ScanFoldersModel::pathTypeDisplayName(ScanFoldersModel::DOWNLOAD_IN_WATCH_FOLDER)); - editor->addItem(ScanFoldersModel::pathTypeDisplayName(ScanFoldersModel::DEFAULT_LOCATION)); - editor->addItem(ScanFoldersModel::pathTypeDisplayName(ScanFoldersModel::CUSTOM_LOCATION)); - if (index.data(Qt::UserRole).toInt() == ScanFoldersModel::CUSTOM_LOCATION) - { - editor->insertSeparator(3); - editor->addItem(index.data().toString()); - } - - connect(editor, qOverload(&QComboBox::currentIndexChanged) - , this, &ScanFoldersDelegate::comboboxIndexChanged); - return editor; -} - -void ScanFoldersDelegate::comboboxIndexChanged(int index) -{ - if (index == ScanFoldersModel::CUSTOM_LOCATION) - { - auto *w = static_cast(sender()); - if (w && w->parentWidget()) - w->parentWidget()->setFocus(); - } -} - -void ScanFoldersDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const -{ - auto *combobox = static_cast(editor); - int value = combobox->currentIndex(); - - switch (value) - { - case ScanFoldersModel::DOWNLOAD_IN_WATCH_FOLDER: - case ScanFoldersModel::DEFAULT_LOCATION: - model->setData(index, value, Qt::UserRole); - break; - - case ScanFoldersModel::CUSTOM_LOCATION: - model->setData( - index, - QFileDialog::getExistingDirectory( - nullptr, tr("Select save location"), - index.data(Qt::UserRole).toInt() == ScanFoldersModel::CUSTOM_LOCATION ? - index.data().toString() : - BitTorrent::Session::instance()->defaultSavePath()), - Qt::DisplayRole); - break; - - default: - break; - } -} - -void ScanFoldersDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const -{ - qDebug("UpdateEditor Geometry called"); - editor->setGeometry(option.rect); -} diff --git a/src/gui/watchedfolderoptionsdialog.cpp b/src/gui/watchedfolderoptionsdialog.cpp new file mode 100644 index 000000000..04bc69b23 --- /dev/null +++ b/src/gui/watchedfolderoptionsdialog.cpp @@ -0,0 +1,153 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * 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 "watchedfolderoptionsdialog.h" + +#include +#include + +#include "base/bittorrent/session.h" +#include "base/global.h" +#include "base/settingsstorage.h" +#include "base/utils/fs.h" +#include "ui_watchedfolderoptionsdialog.h" +#include "utils.h" + +#define SETTINGS_KEY(name) "WatchedFolderOptionsDialog/" name + +WatchedFolderOptionsDialog::WatchedFolderOptionsDialog( + const TorrentFilesWatcher::WatchedFolderOptions &watchedFolderOptions, QWidget *parent) + : QDialog {parent} + , m_ui {new Ui::WatchedFolderOptionsDialog} + , m_savePath {watchedFolderOptions.addTorrentParams.savePath} + , m_storeDialogSize {SETTINGS_KEY("DialogSize")} +{ + m_ui->setupUi(this); + + m_ui->savePath->setMode(FileSystemPathEdit::Mode::DirectorySave); + m_ui->savePath->setDialogCaption(tr("Choose save path")); + + connect(m_ui->comboTTM, qOverload(&QComboBox::currentIndexChanged), this, &WatchedFolderOptionsDialog::onTMMChanged); + connect(m_ui->categoryComboBox, qOverload(&QComboBox::currentIndexChanged), this, &WatchedFolderOptionsDialog::onCategoryChanged); + + m_ui->checkBoxRecursive->setChecked(watchedFolderOptions.recursive); + populateSavePathComboBox(); + + const auto *session = BitTorrent::Session::instance(); + const BitTorrent::AddTorrentParams &torrentParams = watchedFolderOptions.addTorrentParams; + m_ui->startTorrentCheckBox->setChecked(!torrentParams.addPaused.value_or(session->isAddTorrentPaused())); + m_ui->comboTTM->setCurrentIndex(torrentParams.useAutoTMM.value_or(!session->isAutoTMMDisabledByDefault())); + m_ui->contentLayoutComboBox->setCurrentIndex( + static_cast(torrentParams.contentLayout.value_or(session->torrentContentLayout()))); + + // Load categories + QStringList categories = session->categories().keys(); + std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan()); + + if (!torrentParams.category.isEmpty()) + m_ui->categoryComboBox->addItem(torrentParams.category); + m_ui->categoryComboBox->addItem(""); + + for (const QString &category : asConst(categories)) + { + if (category != torrentParams.category) + m_ui->categoryComboBox->addItem(category); + } + + loadState(); + + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus(); +} + +WatchedFolderOptionsDialog::~WatchedFolderOptionsDialog() +{ + saveState(); + delete m_ui; +} + +TorrentFilesWatcher::WatchedFolderOptions WatchedFolderOptionsDialog::watchedFolderOptions() const +{ + TorrentFilesWatcher::WatchedFolderOptions watchedFolderOptions; + watchedFolderOptions.recursive = m_ui->checkBoxRecursive->isChecked(); + + BitTorrent::AddTorrentParams ¶ms = watchedFolderOptions.addTorrentParams; + params.useAutoTMM = (m_ui->comboTTM->currentIndex() == 1); + if (!*params.useAutoTMM) + params.savePath = m_ui->savePath->selectedPath(); + params.category = m_ui->categoryComboBox->currentText();; + params.addPaused = !m_ui->startTorrentCheckBox->isChecked(); + params.contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); + + return watchedFolderOptions; +} + +void WatchedFolderOptionsDialog::loadState() +{ + Utils::Gui::resize(this, m_storeDialogSize); +} + +void WatchedFolderOptionsDialog::saveState() +{ + m_storeDialogSize = size(); +} + +void WatchedFolderOptionsDialog::onCategoryChanged(const int index) +{ + Q_UNUSED(index); + + const QString category = m_ui->categoryComboBox->currentText(); + if (m_ui->comboTTM->currentIndex() == 1) + { + const QString savePath = BitTorrent::Session::instance()->categorySavePath(category); + m_ui->savePath->setSelectedPath(Utils::Fs::toNativePath(savePath)); + } +} + +void WatchedFolderOptionsDialog::populateSavePathComboBox() +{ + const QString defSavePath {BitTorrent::Session::instance()->defaultSavePath()}; + m_ui->savePath->setSelectedPath(!m_savePath.isEmpty() ? m_savePath : defSavePath); +} + +void WatchedFolderOptionsDialog::onTMMChanged(const int index) +{ + if (index != 1) + { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode. + populateSavePathComboBox(); + m_ui->groupBoxSavePath->setEnabled(true); + m_ui->savePath->blockSignals(false); + } + else + { + m_ui->groupBoxSavePath->setEnabled(false); + m_ui->savePath->blockSignals(true); + m_savePath = m_ui->savePath->selectedPath(); + const QString savePath = BitTorrent::Session::instance()->categorySavePath(m_ui->categoryComboBox->currentText()); + m_ui->savePath->setSelectedPath(savePath); + } +} diff --git a/src/gui/scanfoldersdelegate.h b/src/gui/watchedfolderoptionsdialog.h similarity index 62% rename from src/gui/scanfoldersdelegate.h rename to src/gui/watchedfolderoptionsdialog.h index 454218670..a5389d5c7 100644 --- a/src/gui/scanfoldersdelegate.h +++ b/src/gui/watchedfolderoptionsdialog.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 sledgehammer999 + * Copyright (C) 2021 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,31 +28,35 @@ #pragma once -#include +#include -class QAbstractItemModel; -class QModelIndex; -class QPainter; -class QStyleOptionViewItem; -class QTreeView; +#include "base/settingvalue.h" +#include "base/torrentfileswatcher.h" -class PropertiesWidget; +namespace Ui +{ + class WatchedFolderOptionsDialog; +} -class ScanFoldersDelegate final : public QStyledItemDelegate +class WatchedFolderOptionsDialog final : public QDialog { Q_OBJECT + Q_DISABLE_COPY(WatchedFolderOptionsDialog) public: - ScanFoldersDelegate(QObject *parent, QTreeView *foldersView); + explicit WatchedFolderOptionsDialog(const TorrentFilesWatcher::WatchedFolderOptions &watchedFolderOptions, QWidget *parent); + ~WatchedFolderOptionsDialog() override; -private slots: - void comboboxIndexChanged(int index); + TorrentFilesWatcher::WatchedFolderOptions watchedFolderOptions() const; private: - void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const override; - void setEditorData(QWidget *editor, const QModelIndex &index) const override; - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const override; - - QTreeView *m_folderView; + void populateSavePathComboBox(); + void loadState(); + void saveState(); + void onTMMChanged(int index); + void onCategoryChanged(int index); + + Ui::WatchedFolderOptionsDialog *m_ui; + QString m_savePath; + SettingValue m_storeDialogSize; }; diff --git a/src/gui/watchedfolderoptionsdialog.ui b/src/gui/watchedfolderoptionsdialog.ui new file mode 100644 index 000000000..55db4bad8 --- /dev/null +++ b/src/gui/watchedfolderoptionsdialog.ui @@ -0,0 +1,315 @@ + + + WatchedFolderOptionsDialog + + + + 0 + 0 + 462 + 306 + + + + Watched Folder Options + + + + + + + + <html><head/><body><p>Will watch the folder and all its subfolders. In Manual torrent management mode it will also add subfolder name to the selected Save path.</p></body></html> + + + Recursive mode + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + false + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Torrent parameters + + + + + + + + Torrent Management Mode: + + + + + + + Automatic mode means that various torrent properties(eg save path) will be decided by the associated category + + + + Manual + + + + + Automatic + + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + + + + + + Save at + + + + + + + + + + + + + + Category: + + + + + + + + 0 + 0 + + + + true + + + QComboBox::InsertAtTop + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Start torrent + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Content layout: + + + + + + + 0 + + + + Original + + + + + Create subfolder + + + + + Don't create subfolder + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + FileSystemPathLineEdit + QWidget +
gui/fspathedit.h
+ 1 +
+
+ + + + buttonBox + accepted() + WatchedFolderOptionsDialog + accept() + + + 928 + 855 + + + 157 + 274 + + + + + buttonBox + rejected() + WatchedFolderOptionsDialog + reject() + + + 928 + 855 + + + 286 + 274 + + + + +
diff --git a/src/gui/watchedfoldersmodel.cpp b/src/gui/watchedfoldersmodel.cpp new file mode 100644 index 000000000..aa1d9459e --- /dev/null +++ b/src/gui/watchedfoldersmodel.cpp @@ -0,0 +1,178 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * 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 "watchedfoldersmodel.h" + +#include + +#include "base/exceptions.h" +#include "base/global.h" +#include "base/utils/fs.h" + +WatchedFoldersModel::WatchedFoldersModel(TorrentFilesWatcher *fsWatcher, QObject *parent) + : QAbstractListModel {parent} + , m_fsWatcher {fsWatcher} + , m_watchedFolders {m_fsWatcher->folders().keys()} + , m_watchedFoldersOptions {m_fsWatcher->folders()} +{ + connect(m_fsWatcher, &TorrentFilesWatcher::watchedFolderSet, this, &WatchedFoldersModel::onFolderSet); + connect(m_fsWatcher, &TorrentFilesWatcher::watchedFolderRemoved, this, &WatchedFoldersModel::onFolderRemoved); +} + +int WatchedFoldersModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_watchedFolders.count(); +} + +int WatchedFoldersModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 1; +} + +QVariant WatchedFoldersModel::data(const QModelIndex &index, const int role) const +{ + if (!index.isValid() || (index.row() >= rowCount()) || (index.column() >= columnCount())) + return {}; + + if (role == Qt::DisplayRole) + return Utils::Fs::toNativePath(m_watchedFolders.at(index.row())); + + return {}; +} + +QVariant WatchedFoldersModel::headerData(const int section, const Qt::Orientation orientation, const int role) const +{ + if ((orientation != Qt::Horizontal) || (role != Qt::DisplayRole) + || (section < 0) || (section >= columnCount())) + { + return {}; + } + + return tr("Watched Folder"); +} + +bool WatchedFoldersModel::removeRows(const int row, const int count, const QModelIndex &parent) +{ + if (parent.isValid() || (row < 0) || (row >= rowCount()) + || (count <= 0) || ((row + count) > rowCount())) + { + return false; + } + + const int firstRow = row; + const int lastRow = row + (count - 1); + + beginRemoveRows(parent, firstRow, lastRow); + for (int i = firstRow; i <= lastRow; ++i) + { + const QString folderPath = m_watchedFolders.takeAt(i); + m_watchedFoldersOptions.remove(folderPath); + m_deletedFolders.insert(folderPath); + } + endRemoveRows(); + + return true; +} + +void WatchedFoldersModel::addFolder(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options) +{ + if (path.isEmpty()) + throw InvalidArgument(tr("Watched folder path cannot be empty.")); + + const QDir watchDir {path}; + const QString canonicalWatchPath = watchDir.canonicalPath(); + if (m_watchedFoldersOptions.contains(canonicalWatchPath)) + throw RuntimeError(tr("Folder '%1' is already in watch list.").arg(path)); + if (!watchDir.exists()) + throw RuntimeError(tr("Folder '%1' doesn't exist.").arg(path)); + if (!watchDir.isReadable()) + throw RuntimeError(tr("Folder '%1' isn't readable.").arg(path)); + + m_deletedFolders.remove(canonicalWatchPath); + + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_watchedFolders.append(canonicalWatchPath); + m_watchedFoldersOptions[canonicalWatchPath] = options; + endInsertRows(); +} + +TorrentFilesWatcher::WatchedFolderOptions WatchedFoldersModel::folderOptions(const int row) const +{ + Q_ASSERT((row >= 0) && (row < rowCount())); + + const QString folderPath = m_watchedFolders.at(row); + return m_watchedFoldersOptions[folderPath]; +} + +void WatchedFoldersModel::setFolderOptions(const int row, const TorrentFilesWatcher::WatchedFolderOptions &options) +{ + Q_ASSERT((row >= 0) && (row < rowCount())); + + const QString folderPath = m_watchedFolders.at(row); + m_watchedFoldersOptions[folderPath] = options; +} + +void WatchedFoldersModel::apply() +{ + const QSet deletedFolders {m_deletedFolders}; + // We have to clear `m_deletedFolders` for optimization reason, otherwise + // it will be cleared one element at a time in `onFolderRemoved()` handler + m_deletedFolders.clear(); + for (const QString &path : deletedFolders) + m_fsWatcher->removeWatchedFolder(path); + + for (const QString &path : asConst(m_watchedFolders)) + m_fsWatcher->setWatchedFolder(path, m_watchedFoldersOptions.value(path)); +} + +void WatchedFoldersModel::onFolderSet(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options) +{ + if (!m_watchedFoldersOptions.contains(path)) + { + m_deletedFolders.remove(path); + + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_watchedFolders.append(path); + m_watchedFoldersOptions[path] = options; + endInsertRows(); + } + else + { + m_watchedFoldersOptions[path] = options; + } +} + +void WatchedFoldersModel::onFolderRemoved(const QString &path) +{ + const int row = m_watchedFolders.indexOf(path); + if (row >= 0) + removeRows(row, 1); + + m_deletedFolders.remove(path); +} diff --git a/src/base/filesystemwatcher.h b/src/gui/watchedfoldersmodel.h similarity index 50% rename from src/base/filesystemwatcher.h rename to src/gui/watchedfoldersmodel.h index 2f946e0a8..166f341b8 100644 --- a/src/base/filesystemwatcher.h +++ b/src/gui/watchedfoldersmodel.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 + * Copyright (C) 2021 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,43 +28,41 @@ #pragma once -#include -#include +#include +#include #include -#include -#include +#include +#include -/* - * Subclassing QFileSystemWatcher in order to support Network File - * System watching (NFS, CIFS) on Linux and Mac OS. - */ -class FileSystemWatcher final : public QFileSystemWatcher +#include "base/torrentfileswatcher.h" + +class WatchedFoldersModel final : public QAbstractListModel { Q_OBJECT - Q_DISABLE_COPY(FileSystemWatcher) + Q_DISABLE_COPY(WatchedFoldersModel) public: - explicit FileSystemWatcher(QObject *parent = nullptr); + explicit WatchedFoldersModel(TorrentFilesWatcher *fsWatcher, QObject *parent = nullptr); - QStringList directories() const; - void addPath(const QString &path); - void removePath(const QString &path); + int rowCount(const QModelIndex &parent = {}) const override; + int columnCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; -signals: - void torrentsAdded(const QStringList &pathList); + void addFolder(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options); -private slots: - void scanLocalFolder(const QString &path); - void processPartialTorrents(); - void scanNetworkFolders(); + TorrentFilesWatcher::WatchedFolderOptions folderOptions(int row) const; + void setFolderOptions(int row, const TorrentFilesWatcher::WatchedFolderOptions &options); -private: - void processTorrentsInDir(const QDir &dir); + void apply(); - // Partial torrents - QHash m_partialTorrents; - QTimer m_partialTorrentTimer; +private: + void onFolderSet(const QString &path, const TorrentFilesWatcher::WatchedFolderOptions &options); + void onFolderRemoved(const QString &path); - QVector m_watchedFolders; - QTimer m_watchTimer; + TorrentFilesWatcher *m_fsWatcher; + QStringList m_watchedFolders; + QHash m_watchedFoldersOptions; + QSet m_deletedFolders; }; diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 3a3c01b04..9866c2245 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -50,8 +50,8 @@ #include "base/preferences.h" #include "base/rss/rss_autodownloader.h" #include "base/rss/rss_session.h" -#include "base/scanfoldersmodel.h" #include "base/torrentfileguard.h" +#include "base/torrentfileswatcher.h" #include "base/utils/fs.h" #include "base/utils/misc.h" #include "base/utils/net.h" @@ -117,17 +117,26 @@ void AppController::preferencesAction() data["temp_path"] = Utils::Fs::toNativePath(session->tempPath()); data["export_dir"] = Utils::Fs::toNativePath(session->torrentExportDirectory()); data["export_dir_fin"] = Utils::Fs::toNativePath(session->finishedTorrentExportDirectory()); - // Automatically add torrents from - const QVariantHash dirs = pref->getScanDirs(); + + // TODO: The following code is deprecated. Delete it once replaced by updated API method. + // === BEGIN DEPRECATED CODE === // + TorrentFilesWatcher *fsWatcher = TorrentFilesWatcher::instance(); + const QHash watchedFolders = fsWatcher->folders(); QJsonObject nativeDirs; - for (auto i = dirs.cbegin(); i != dirs.cend(); ++i) + for (auto i = watchedFolders.cbegin(); i != watchedFolders.cend(); ++i) { - if (i.value().type() == QVariant::Int) - nativeDirs.insert(Utils::Fs::toNativePath(i.key()), i.value().toInt()); + const QString watchedFolder = i.key(); + const BitTorrent::AddTorrentParams params = i.value().addTorrentParams; + if (params.savePath.isEmpty()) + nativeDirs.insert(Utils::Fs::toNativePath(watchedFolder), 1); + else if (params.savePath == watchedFolder) + nativeDirs.insert(Utils::Fs::toNativePath(watchedFolder), 0); else - nativeDirs.insert(Utils::Fs::toNativePath(i.key()), Utils::Fs::toNativePath(i.value().toString())); + nativeDirs.insert(Utils::Fs::toNativePath(watchedFolder), Utils::Fs::toNativePath(params.savePath)); } data["scan_dirs"] = nativeDirs; + // === END DEPRECATED CODE === // + // Email notification upon download completion data["mail_notification_enabled"] = pref->isMailNotificationEnabled(); data["mail_notification_sender"] = pref->getMailNotificationSender(); @@ -390,49 +399,57 @@ void AppController::setPreferencesAction() session->setTorrentExportDirectory(it.value().toString()); if (hasKey("export_dir_fin")) session->setFinishedTorrentExportDirectory(it.value().toString()); - // Automatically add torrents from + + // TODO: The following code is deprecated. Delete it once replaced by updated API method. + // === BEGIN DEPRECATED CODE === // if (hasKey("scan_dirs")) { + QStringList scanDirs; + TorrentFilesWatcher *fsWatcher = TorrentFilesWatcher::instance(); + const QStringList oldScanDirs = fsWatcher->folders().keys(); const QVariantHash nativeDirs = it.value().toHash(); - const QVariantHash oldScanDirs = pref->getScanDirs(); - QVariantHash scanDirs; - ScanFoldersModel *model = ScanFoldersModel::instance(); - for (auto i = nativeDirs.cbegin(); i != nativeDirs.cend(); ++i) { - int downloadType = 0; - QString downloadPath; - if (i.value().type() == QVariant::String) + try { - downloadType = ScanFoldersModel::CUSTOM_LOCATION; - downloadPath = Utils::Fs::toUniformPath(i.value().toString()); + const QString watchedFolder = TorrentFilesWatcher::makeCleanPath(i.key()); + TorrentFilesWatcher::WatchedFolderOptions options = fsWatcher->folders().value(watchedFolder); + BitTorrent::AddTorrentParams ¶ms = options.addTorrentParams; + + bool isInt = false; + const int intVal = i.value().toInt(&isInt); + if (isInt) + { + if (intVal == 0) + { + params.savePath = watchedFolder; + params.useAutoTMM = false; + } + } + else + { + const QString customSavePath = i.value().toString(); + params.savePath = customSavePath; + params.useAutoTMM = false; + } + + fsWatcher->setWatchedFolder(watchedFolder, options); + scanDirs.append(watchedFolder); } - else + catch (...) { - downloadType = i.value().toInt(); - downloadPath = (downloadType == ScanFoldersModel::DEFAULT_LOCATION) - ? QLatin1String("Default folder") - : QLatin1String("Watch folder"); } - - const QString folder = Utils::Fs::toUniformPath(i.key()); - const ScanFoldersModel::PathStatus ec = !oldScanDirs.contains(folder) - ? model->addPath(folder, static_cast(downloadType), downloadPath) - : model->updatePath(folder, static_cast(downloadType), downloadPath); - if (ec == ScanFoldersModel::Ok) - scanDirs.insert(folder, ((downloadType == ScanFoldersModel::CUSTOM_LOCATION) ? QVariant(downloadPath) : QVariant(downloadType))); } // Update deleted folders - for (auto i = oldScanDirs.cbegin(); i != oldScanDirs.cend(); ++i) + for (const QString &path : oldScanDirs) { - const QString &folder = i.key(); - if (!scanDirs.contains(folder)) - model->removePath(folder); + if (!scanDirs.contains(path)) + fsWatcher->removeWatchedFolder(path); } - - pref->setScanDirs(scanDirs); } + // === END DEPRECATED CODE === // + // Email notification upon download completion if (hasKey("mail_notification_enabled")) pref->setMailNotificationEnabled(it.value().toBool());