diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 2040408ca..e11620935 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -37,6 +37,8 @@ add_library(qbt_base STATIC bittorrent/torrent.h bittorrent/torrentcontenthandler.h bittorrent/torrentcontentlayout.h + bittorrent/torrentcreationmanager.h + bittorrent/torrentcreationtask.h bittorrent/torrentcreator.h bittorrent/torrentdescriptor.h bittorrent/torrentimpl.h @@ -140,6 +142,8 @@ add_library(qbt_base STATIC bittorrent/sslparameters.cpp bittorrent/torrent.cpp bittorrent/torrentcontenthandler.cpp + bittorrent/torrentcreationmanager.cpp + bittorrent/torrentcreationtask.cpp bittorrent/torrentcreator.cpp bittorrent/torrentdescriptor.cpp bittorrent/torrentimpl.cpp diff --git a/src/base/bittorrent/torrentcreationmanager.cpp b/src/base/bittorrent/torrentcreationmanager.cpp new file mode 100644 index 000000000..283a99780 --- /dev/null +++ b/src/base/bittorrent/torrentcreationmanager.cpp @@ -0,0 +1,134 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa + * + * 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 "torrentcreationmanager.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#define SETTINGS_KEY(name) u"TorrentCreator/Manager/" name + +namespace BitTorrent +{ + using namespace boost::multi_index; + + class TorrentCreationManager::TaskSet final : public boost::multi_index_container< + std::shared_ptr, + indexed_by< + ordered_unique, const_mem_fun>, + ordered_non_unique, composite_key< + TorrentCreationTask, + const_mem_fun, + const_mem_fun>>>> + { + }; +} + +BitTorrent::TorrentCreationManager::TorrentCreationManager(IApplication *app, QObject *parent) + : ApplicationComponent(app, parent) + , m_maxTasks {SETTINGS_KEY(u"MaxTasks"_s), 256} + , m_numThreads {SETTINGS_KEY(u"NumThreads"_s), 1} + , m_tasks {std::make_unique()} +{ + if (m_numThreads > 0) + m_threadPool.setMaxThreadCount(m_numThreads); +} + +BitTorrent::TorrentCreationManager::~TorrentCreationManager() = default; + +std::shared_ptr BitTorrent::TorrentCreationManager::createTask(const TorrentCreatorParams ¶ms, bool startSeeding) +{ + if (std::cmp_greater_equal(m_tasks->size(), m_maxTasks.get())) + { + // Try to delete old finished tasks to stay under target + auto &tasksByCompletion = m_tasks->get(); + auto [iter, endIter] = tasksByCompletion.equal_range(std::make_tuple(true)); + while ((iter != endIter) && std::cmp_greater_equal(m_tasks->size(), m_maxTasks.get())) + { + iter = tasksByCompletion.erase(iter); + } + } + if (std::cmp_greater_equal(m_tasks->size(), m_maxTasks.get())) + return {}; + + const QString taskID = generateTaskID(); + + auto *torrentCreator = new TorrentCreator(params, this); + auto creationTask = std::make_shared(app(), taskID, torrentCreator, startSeeding); + connect(creationTask.get(), &QObject::destroyed, torrentCreator, &BitTorrent::TorrentCreator::requestInterruption); + + m_tasks->get().insert(creationTask); + m_threadPool.start(torrentCreator); + + return creationTask; +} + +QString BitTorrent::TorrentCreationManager::generateTaskID() const +{ + const auto &tasksByID = m_tasks->get(); + QString taskID = QUuid::createUuid().toString(QUuid::WithoutBraces); + while (tasksByID.find(taskID) != tasksByID.end()) + taskID = QUuid::createUuid().toString(QUuid::WithoutBraces); + + return taskID; +} + +std::shared_ptr BitTorrent::TorrentCreationManager::getTask(const QString &id) const +{ + const auto &tasksByID = m_tasks->get(); + const auto iter = tasksByID.find(id); + if (iter == tasksByID.end()) + return nullptr; + + return *iter; +} + +QList> BitTorrent::TorrentCreationManager::tasks() const +{ + const auto &tasksByCompletion = m_tasks->get(); + return {tasksByCompletion.cbegin(), tasksByCompletion.cend()}; +} + +bool BitTorrent::TorrentCreationManager::deleteTask(const QString &id) +{ + auto &tasksByID = m_tasks->get(); + const auto iter = tasksByID.find(id); + if (iter == tasksByID.end()) + return false; + + tasksByID.erase(iter); + return true; +} diff --git a/src/base/bittorrent/torrentcreationmanager.h b/src/base/bittorrent/torrentcreationmanager.h new file mode 100644 index 000000000..9c9b26ea6 --- /dev/null +++ b/src/base/bittorrent/torrentcreationmanager.h @@ -0,0 +1,70 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include +#include +#include + +#include "base/applicationcomponent.h" +#include "base/settingvalue.h" +#include "torrentcreationtask.h" +#include "torrentcreator.h" + +namespace BitTorrent +{ + class TorrentCreationManager final : public ApplicationComponent + { + Q_OBJECT + Q_DISABLE_COPY_MOVE(TorrentCreationManager) + + public: + explicit TorrentCreationManager(IApplication *app, QObject *parent = nullptr); + ~TorrentCreationManager() override; + + std::shared_ptr createTask(const TorrentCreatorParams ¶ms, bool startSeeding = true); + std::shared_ptr getTask(const QString &id) const; + QList> tasks() const; + bool deleteTask(const QString &id); + + private: + QString generateTaskID() const; + + CachedSettingValue m_maxTasks; + CachedSettingValue m_numThreads; + + class TaskSet; + std::unique_ptr m_tasks; + + QThreadPool m_threadPool; + }; +} diff --git a/src/base/bittorrent/torrentcreationtask.cpp b/src/base/bittorrent/torrentcreationtask.cpp new file mode 100644 index 000000000..b1cb25f0c --- /dev/null +++ b/src/base/bittorrent/torrentcreationtask.cpp @@ -0,0 +1,149 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa + * + * 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 "torrentcreationtask.h" + +#include "base/addtorrentmanager.h" +#include "base/interfaces/iapplication.h" +#include "base/bittorrent/addtorrentparams.h" + +BitTorrent::TorrentCreationTask::TorrentCreationTask(IApplication *app, const QString &id + , TorrentCreator *torrentCreator, bool startSeeding, QObject *parent) + : ApplicationComponent(app, parent) + , m_id {id} + , m_params {torrentCreator->params()} + , m_timeAdded {QDateTime::currentDateTime()} +{ + Q_ASSERT(torrentCreator); + + connect(torrentCreator, &BitTorrent::TorrentCreator::started, this, [this] + { + m_timeStarted = QDateTime::currentDateTime(); + }); + + connect(torrentCreator, &BitTorrent::TorrentCreator::progressUpdated, this + , [this](const int progress) + { + m_progress = progress; + }); + + connect(torrentCreator, &BitTorrent::TorrentCreator::creationSuccess, this + , [this, app, startSeeding](const TorrentCreatorResult &result) + { + m_timeFinished = QDateTime::currentDateTime(); + m_result = result; + + if (!startSeeding) + return; + + BitTorrent::AddTorrentParams params; + params.savePath = result.savePath; + params.skipChecking = true; + params.useAutoTMM = false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path + + if (!app->addTorrentManager()->addTorrent(result.torrentFilePath.data(), params)) + m_errorMsg = tr("Failed to start seeding."); + }); + + connect(torrentCreator, &BitTorrent::TorrentCreator::creationFailure, this + , [this](const QString &errorMsg) + { + m_timeFinished = QDateTime::currentDateTime(); + m_errorMsg = errorMsg; + }); +} + +QString BitTorrent::TorrentCreationTask::id() const +{ + return m_id; +} + +const BitTorrent::TorrentCreatorParams &BitTorrent::TorrentCreationTask::params() const +{ + return m_params; +} + +BitTorrent::TorrentCreationTask::State BitTorrent::TorrentCreationTask::state() const +{ + if (m_timeStarted.isNull()) + return Queued; + if (m_timeFinished.isNull()) + return Running; + return Finished; +} + +bool BitTorrent::TorrentCreationTask::isQueued() const +{ + return (state() == Queued); +} + +bool BitTorrent::TorrentCreationTask::isRunning() const +{ + return (state() == Running); +} + +bool BitTorrent::TorrentCreationTask::isFinished() const +{ + return (state() == Finished); +} + +bool BitTorrent::TorrentCreationTask::isFailed() const +{ + return !m_errorMsg.isEmpty(); +} + +int BitTorrent::TorrentCreationTask::progress() const +{ + return m_progress; +} + +const BitTorrent::TorrentCreatorResult &BitTorrent::TorrentCreationTask::result() const +{ + return m_result; +} + +QString BitTorrent::TorrentCreationTask::errorMsg() const +{ + return m_errorMsg; +} + +QDateTime BitTorrent::TorrentCreationTask::timeAdded() const +{ + return m_timeAdded; +} + +QDateTime BitTorrent::TorrentCreationTask::timeStarted() const +{ + return m_timeStarted; +} + +QDateTime BitTorrent::TorrentCreationTask::timeFinished() const +{ + return m_timeFinished; +} diff --git a/src/base/bittorrent/torrentcreationtask.h b/src/base/bittorrent/torrentcreationtask.h new file mode 100644 index 000000000..2f24e8dd4 --- /dev/null +++ b/src/base/bittorrent/torrentcreationtask.h @@ -0,0 +1,81 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include +#include + +#include "base/applicationcomponent.h" +#include "torrentcreator.h" + +namespace BitTorrent +{ + class TorrentCreationTask final : public ApplicationComponent + { + Q_OBJECT + Q_DISABLE_COPY_MOVE(TorrentCreationTask) + + public: + enum State + { + Queued, + Running, + Finished + }; + + TorrentCreationTask(IApplication *app, const QString &id, TorrentCreator *torrentCreator + , bool startSeeding, QObject *parent = nullptr); + + QString id() const; + const TorrentCreatorParams ¶ms() const; + State state() const; + bool isQueued() const; + bool isRunning() const; + bool isFinished() const; + bool isFailed() const; + QDateTime timeAdded() const; + QDateTime timeStarted() const; + QDateTime timeFinished() const; + int progress() const; + const TorrentCreatorResult &result() const; + QString errorMsg() const; + + private: + QString m_id; + TorrentCreatorParams m_params; + QDateTime m_timeAdded; + QDateTime m_timeStarted; + QDateTime m_timeFinished; + int m_progress = 0; + TorrentCreatorResult m_result; + QString m_errorMsg; + }; +} diff --git a/src/base/bittorrent/torrentcreator.cpp b/src/base/bittorrent/torrentcreator.cpp index bfabc4b67..3cd26ba7c 100644 --- a/src/base/bittorrent/torrentcreator.cpp +++ b/src/base/bittorrent/torrentcreator.cpp @@ -1,5 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa * Copyright (C) 2010 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -28,7 +30,7 @@ #include "torrentcreator.h" -#include +#include #include #include @@ -41,7 +43,6 @@ #include "base/exceptions.h" #include "base/global.h" #include "base/utils/compare.h" -#include "base/utils/fs.h" #include "base/utils/io.h" #include "base/version.h" #include "lttypecast.h" @@ -82,7 +83,7 @@ TorrentCreator::TorrentCreator(const TorrentCreatorParams ¶ms, QObject *pare void TorrentCreator::sendProgressSignal(int currentPieceIdx, int totalPieces) { - emit updateProgress(static_cast((currentPieceIdx * 100.) / totalPieces)); + emit progressUpdated(static_cast((currentPieceIdx * 100.) / totalPieces)); } void TorrentCreator::checkInterruptionRequested() const @@ -103,25 +104,26 @@ bool TorrentCreator::isInterruptionRequested() const void TorrentCreator::run() { - emit updateProgress(0); + emit started(); + emit progressUpdated(0); try { - const Path parentPath = m_params.inputPath.parentPath(); + const Path parentPath = m_params.sourcePath.parentPath(); const Utils::Compare::NaturalLessThan naturalLessThan {}; // Adding files to the torrent lt::file_storage fs; - if (QFileInfo(m_params.inputPath.data()).isFile()) + if (QFileInfo(m_params.sourcePath.data()).isFile()) { - lt::add_files(fs, m_params.inputPath.toString().toStdString(), fileFilter); + lt::add_files(fs, m_params.sourcePath.toString().toStdString(), fileFilter); } else { // need to sort the file names by natural sort order - QStringList dirs = {m_params.inputPath.data()}; + QStringList dirs = {m_params.sourcePath.data()}; - QDirIterator dirIter {m_params.inputPath.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories}; + QDirIterator dirIter {m_params.sourcePath.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories}; while (dirIter.hasNext()) { const QString filePath = dirIter.next(); @@ -205,13 +207,29 @@ void TorrentCreator::run() checkInterruptionRequested(); - // create the torrent - const nonstd::expected result = Utils::IO::saveToFile(m_params.savePath, entry); + const auto result = std::invoke([torrentFilePath = m_params.torrentFilePath, entry]() -> nonstd::expected + { + if (!torrentFilePath.isValid()) + return Utils::IO::saveToTempFile(entry); + + const nonstd::expected result = Utils::IO::saveToFile(torrentFilePath, entry); + if (!result) + return nonstd::make_unexpected(result.error()); + + return torrentFilePath; + }); if (!result) throw RuntimeError(result.error()); - emit updateProgress(100); - emit creationSuccess(m_params.savePath, parentPath); + const BitTorrent::TorrentCreatorResult creatorResult + { + .torrentFilePath = result.value(), + .savePath = parentPath, + .pieceSize = newTorrent.piece_length() + }; + + emit progressUpdated(100); + emit creationSuccess(creatorResult); } catch (const RuntimeError &err) { @@ -223,6 +241,11 @@ void TorrentCreator::run() } } +const TorrentCreatorParams &TorrentCreator::params() const +{ + return m_params; +} + #ifdef QBT_USES_LIBTORRENT2 int TorrentCreator::calculateTotalPieces(const Path &inputPath, const int pieceSize, const TorrentFormat torrentFormat) #else diff --git a/src/base/bittorrent/torrentcreator.h b/src/base/bittorrent/torrentcreator.h index 79588b07f..e4377a573 100644 --- a/src/base/bittorrent/torrentcreator.h +++ b/src/base/bittorrent/torrentcreator.h @@ -1,5 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa * Copyright (C) 2010 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -53,18 +55,25 @@ namespace BitTorrent #ifdef QBT_USES_LIBTORRENT2 TorrentFormat torrentFormat = TorrentFormat::Hybrid; #else - bool isAlignmentOptimized; - int paddedFileSizeLimit; + bool isAlignmentOptimized = false; + int paddedFileSizeLimit = 0; #endif int pieceSize = 0; - Path inputPath; - Path savePath; + Path sourcePath; + Path torrentFilePath; QString comment; QString source; QStringList trackers; QStringList urlSeeds; }; + struct TorrentCreatorResult + { + Path torrentFilePath; + Path savePath; + int pieceSize; + }; + class TorrentCreator final : public QObject, public QRunnable { Q_OBJECT @@ -73,24 +82,26 @@ namespace BitTorrent public: explicit TorrentCreator(const TorrentCreatorParams ¶ms, QObject *parent = nullptr); - void run() override; - + const TorrentCreatorParams ¶ms() const; bool isInterruptionRequested() const; - public slots: - void requestInterruption(); + void run() override; #ifdef QBT_USES_LIBTORRENT2 static int calculateTotalPieces(const Path &inputPath, int pieceSize, TorrentFormat torrentFormat); #else - static int calculateTotalPieces(const Path &inputPath - , const int pieceSize, const bool isAlignmentOptimized, int paddedFileSizeLimit); + static int calculateTotalPieces(const Path &inputPath, const int pieceSize + , const bool isAlignmentOptimized, int paddedFileSizeLimit); #endif + public slots: + void requestInterruption(); + signals: + void started(); void creationFailure(const QString &msg); - void creationSuccess(const Path &path, const Path &branchPath); - void updateProgress(int progress); + void creationSuccess(const TorrentCreatorResult &result); + void progressUpdated(int progress); private: void sendProgressSignal(int currentPieceIdx, int totalPieces); diff --git a/src/base/net/downloadhandlerimpl.cpp b/src/base/net/downloadhandlerimpl.cpp index c96c3cd76..370347946 100644 --- a/src/base/net/downloadhandlerimpl.cpp +++ b/src/base/net/downloadhandlerimpl.cpp @@ -30,7 +30,6 @@ #include "downloadhandlerimpl.h" #include -#include #include #include "base/3rdparty/expected.hpp" @@ -49,19 +48,6 @@ const int MAX_REDIRECTIONS = 20; // the common value for web browsers -namespace -{ - nonstd::expected saveToTempFile(const QByteArray &data) - { - QTemporaryFile file {Utils::Fs::tempPath().data()}; - if (!file.open() || (file.write(data) != data.length()) || !file.flush()) - return nonstd::make_unexpected(file.errorString()); - - file.setAutoRemove(false); - return Path(file.fileName()); - } -} - Net::DownloadHandlerImpl::DownloadHandlerImpl(DownloadManager *manager , const DownloadRequest &downloadRequest, const bool useProxy) : DownloadHandler {manager} @@ -163,7 +149,7 @@ void Net::DownloadHandlerImpl::processFinishedDownload() const Path destinationPath = m_downloadRequest.destFileName(); if (destinationPath.isEmpty()) { - const nonstd::expected result = saveToTempFile(m_result.data); + const nonstd::expected result = Utils::IO::saveToTempFile(m_result.data); if (result) { m_result.filePath = result.value(); diff --git a/src/base/utils/io.cpp b/src/base/utils/io.cpp index 2be80549b..d62c31f62 100644 --- a/src/base/utils/io.cpp +++ b/src/base/utils/io.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include "base/path.h" #include "base/utils/fs.h" @@ -149,3 +150,27 @@ nonstd::expected Utils::IO::saveToFile(const Path &path, const lt return {}; } + +nonstd::expected Utils::IO::saveToTempFile(const QByteArray &data) +{ + QTemporaryFile file {(Utils::Fs::tempPath() / Path(u"file_"_s)).data()}; + if (!file.open() || (file.write(data) != data.length()) || !file.flush()) + return nonstd::make_unexpected(file.errorString()); + + file.setAutoRemove(false); + return Path(file.fileName()); +} + +nonstd::expected Utils::IO::saveToTempFile(const lt::entry &data) +{ + QTemporaryFile file {(Utils::Fs::tempPath() / Path(u"file_"_s)).data()}; + if (!file.open()) + return nonstd::make_unexpected(file.errorString()); + + const int bencodedDataSize = lt::bencode(Utils::IO::FileDeviceOutputIterator {file}, data); + if ((file.size() != bencodedDataSize) || !file.flush()) + return nonstd::make_unexpected(file.errorString()); + + file.setAutoRemove(false); + return Path(file.fileName()); +} diff --git a/src/base/utils/io.h b/src/base/utils/io.h index 1005410c5..6cd3a9057 100644 --- a/src/base/utils/io.h +++ b/src/base/utils/io.h @@ -103,4 +103,6 @@ namespace Utils::IO nonstd::expected saveToFile(const Path &path, const QByteArray &data); nonstd::expected saveToFile(const Path &path, const lt::entry &data); + nonstd::expected saveToTempFile(const QByteArray &data); + nonstd::expected saveToTempFile(const lt::entry &data); } diff --git a/src/gui/torrentcreatordialog.cpp b/src/gui/torrentcreatordialog.cpp index 280a0ffbc..d6b5bdf53 100644 --- a/src/gui/torrentcreatordialog.cpp +++ b/src/gui/torrentcreatordialog.cpp @@ -1,5 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa * Copyright (C) 2017 Mike Tzou (Chocobo1) * Copyright (C) 2010 Christophe Dumez * @@ -235,8 +237,8 @@ void TorrentCreatorDialog::onCreateButtonClicked() .paddedFileSizeLimit = getPaddedFileSizeLimit(), #endif .pieceSize = getPieceSize(), - .inputPath = inputPath, - .savePath = destPath, + .sourcePath = inputPath, + .torrentFilePath = destPath, .comment = m_ui->txtComment->toPlainText(), .source = m_ui->lineEditSource->text(), .trackers = trackers, @@ -247,7 +249,7 @@ void TorrentCreatorDialog::onCreateButtonClicked() connect(this, &QDialog::rejected, torrentCreator, &BitTorrent::TorrentCreator::requestInterruption); connect(torrentCreator, &BitTorrent::TorrentCreator::creationSuccess, this, &TorrentCreatorDialog::handleCreationSuccess); connect(torrentCreator, &BitTorrent::TorrentCreator::creationFailure, this, &TorrentCreatorDialog::handleCreationFailure); - connect(torrentCreator, &BitTorrent::TorrentCreator::updateProgress, this, &TorrentCreatorDialog::updateProgressBar); + connect(torrentCreator, &BitTorrent::TorrentCreator::progressUpdated, this, &TorrentCreatorDialog::updateProgressBar); // run the torrentCreator in a thread m_threadPool.start(torrentCreator); @@ -261,36 +263,36 @@ void TorrentCreatorDialog::handleCreationFailure(const QString &msg) setInteractionEnabled(true); } -void TorrentCreatorDialog::handleCreationSuccess(const Path &path, const Path &branchPath) +void TorrentCreatorDialog::handleCreationSuccess(const BitTorrent::TorrentCreatorResult &result) { setCursor(QCursor(Qt::ArrowCursor)); setInteractionEnabled(true); QMessageBox::information(this, tr("Torrent creator") - , u"%1\n%2"_s.arg(tr("Torrent created:"), path.toString())); + , u"%1\n%2"_s.arg(tr("Torrent created:"), result.torrentFilePath.toString())); if (m_ui->checkStartSeeding->isChecked()) { - const auto loadResult = BitTorrent::TorrentDescriptor::loadFromFile(path); - if (!loadResult) + if (const auto loadResult = BitTorrent::TorrentDescriptor::loadFromFile(result.torrentFilePath)) + { + BitTorrent::AddTorrentParams params; + params.savePath = result.savePath; + params.skipChecking = true; + if (m_ui->checkIgnoreShareLimits->isChecked()) + { + params.ratioLimit = BitTorrent::Torrent::NO_RATIO_LIMIT; + params.seedingTimeLimit = BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT; + params.inactiveSeedingTimeLimit = BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT; + } + params.useAutoTMM = false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path + + BitTorrent::Session::instance()->addTorrent(loadResult.value(), params); + } + else { const QString message = tr("Add torrent to transfer list failed.") + u'\n' + tr("Reason: \"%1\"").arg(loadResult.error()); QMessageBox::critical(this, tr("Add torrent failed"), message); - return; } - - BitTorrent::AddTorrentParams params; - params.savePath = branchPath; - params.skipChecking = true; - if (m_ui->checkIgnoreShareLimits->isChecked()) - { - params.ratioLimit = BitTorrent::Torrent::NO_RATIO_LIMIT; - params.seedingTimeLimit = BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT; - params.inactiveSeedingTimeLimit = BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT; - } - params.useAutoTMM = false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path - - BitTorrent::Session::instance()->addTorrent(loadResult.value(), params); } } diff --git a/src/gui/torrentcreatordialog.h b/src/gui/torrentcreatordialog.h index addda955d..283978184 100644 --- a/src/gui/torrentcreatordialog.h +++ b/src/gui/torrentcreatordialog.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Radu Carpa * Copyright (C) 2017 Mike Tzou (Chocobo1) * Copyright (C) 2010 Christophe Dumez * @@ -58,7 +59,7 @@ private slots: void onAddFileButtonClicked(); void onAddFolderButtonClicked(); void handleCreationFailure(const QString &msg); - void handleCreationSuccess(const Path &path, const Path &branchPath); + void handleCreationSuccess(const BitTorrent::TorrentCreatorResult &result); private: void dropEvent(QDropEvent *event) override; diff --git a/src/webui/CMakeLists.txt b/src/webui/CMakeLists.txt index 610774a72..9dfc12831 100644 --- a/src/webui/CMakeLists.txt +++ b/src/webui/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(qbt_webui STATIC api/rsscontroller.h api/searchcontroller.h api/synccontroller.h + api/torrentcreatorcontroller.h api/torrentscontroller.h api/transfercontroller.h api/serialize/serialize_torrent.h @@ -25,6 +26,7 @@ add_library(qbt_webui STATIC api/rsscontroller.cpp api/searchcontroller.cpp api/synccontroller.cpp + api/torrentcreatorcontroller.cpp api/torrentscontroller.cpp api/transfercontroller.cpp api/serialize/serialize_torrent.cpp diff --git a/src/webui/api/torrentcreatorcontroller.cpp b/src/webui/api/torrentcreatorcontroller.cpp new file mode 100644 index 000000000..ced5ec390 --- /dev/null +++ b/src/webui/api/torrentcreatorcontroller.cpp @@ -0,0 +1,247 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa + * + * 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 "torrentcreatorcontroller.h" + +#include +#include +#include + +#include "base/global.h" +#include "base/bittorrent/torrentcreationmanager.h" +#include "base/preferences.h" +#include "base/utils/io.h" +#include "base/utils/string.h" +#include "apierror.h" + +const QString KEY_COMMENT = u"comment"_s; +const QString KEY_ERROR_MESSAGE = u"errorMessage"_s; +const QString KEY_FORMAT = u"format"_s; +const QString KEY_OPTIMIZE_ALIGNMENT = u"optimizeAlignment"_s; +const QString KEY_PADDED_FILE_SIZE_LIMIT = u"paddedFileSizeLimit"_s; +const QString KEY_PIECE_SIZE = u"pieceSize"_s; +const QString KEY_PRIVATE = u"private"_s; +const QString KEY_PROGRESS = u"progress"_s; +const QString KEY_SOURCE = u"source"_s; +const QString KEY_SOURCE_PATH = u"sourcePath"_s; +const QString KEY_STATUS = u"status"_s; +const QString KEY_TASK_ID = u"taskID"_s; +const QString KEY_TIME_ADDED = u"timeAdded"_s; +const QString KEY_TIME_FINISHED = u"timeFinished"_s; +const QString KEY_TIME_STARTED = u"timeStarted"_s; +const QString KEY_TORRENT_FILE_PATH = u"torrentFilePath"_s; +const QString KEY_TRACKERS = u"trackers"_s; +const QString KEY_URL_SEEDS = u"urlSeeds"_s; + +namespace +{ + using Utils::String::parseBool; + using Utils::String::parseInt; + +#ifdef QBT_USES_LIBTORRENT2 + BitTorrent::TorrentFormat parseTorrentFormat(const QString &str) + { + if (str == u"v1") + return BitTorrent::TorrentFormat::V1; + if (str == u"v2") + return BitTorrent::TorrentFormat::V2; + return BitTorrent::TorrentFormat::Hybrid; + } + + QString torrentFormatToString(const BitTorrent::TorrentFormat torrentFormat) + { + switch (torrentFormat) + { + case BitTorrent::TorrentFormat::V1: + return u"v1"_s; + case BitTorrent::TorrentFormat::V2: + return u"v2"_s; + default: + return u"hybrid"_s; + } + } +#endif + + QString taskStatusString(const std::shared_ptr task) + { + if (task->isFailed()) + return u"Failed"_s; + + switch (task->state()) + { + case BitTorrent::TorrentCreationTask::Queued: + default: + return u"Queued"_s; + case BitTorrent::TorrentCreationTask::Running: + return u"Running"_s; + case BitTorrent::TorrentCreationTask::Finished: + return u"Finished"_s; + } + } +} + +TorrentCreatorController::TorrentCreatorController(BitTorrent::TorrentCreationManager *torrentCreationManager, IApplication *app, QObject *parent) + : APIController(app, parent) + , m_torrentCreationManager {torrentCreationManager} +{ +} + +void TorrentCreatorController::addTaskAction() +{ + requireParams({KEY_SOURCE_PATH}); + + const BitTorrent::TorrentCreatorParams createTorrentParams + { + .isPrivate = parseBool(params()[KEY_PRIVATE]).value_or(false), +#ifdef QBT_USES_LIBTORRENT2 + .torrentFormat = parseTorrentFormat(params()[KEY_FORMAT].toLower()), +#else + .isAlignmentOptimized = parseBool(params()[KEY_OPTIMIZE_ALIGNMENT]).value_or(true), + .paddedFileSizeLimit = parseInt(params()[KEY_PADDED_FILE_SIZE_LIMIT]).value_or(-1), +#endif + .pieceSize = parseInt(params()[KEY_PIECE_SIZE]).value_or(0), + .sourcePath = Path(params()[KEY_SOURCE_PATH]), + .torrentFilePath = Path(params()[KEY_TORRENT_FILE_PATH]), + .comment = params()[KEY_COMMENT], + .source = params()[KEY_COMMENT], + .trackers = params()[KEY_TRACKERS].split(u'|'), + .urlSeeds = params()[KEY_URL_SEEDS].split(u'|') + }; + + bool const startSeeding = parseBool(params()[u"startSeeding"_s]).value_or(createTorrentParams.torrentFilePath.isEmpty()); + + auto task = m_torrentCreationManager->createTask(createTorrentParams, startSeeding); + if (!task) + throw APIError(APIErrorType::Conflict, tr("Too many active tasks")); + + setResult(QJsonObject {{KEY_TASK_ID, task->id()}}); +} + +void TorrentCreatorController::statusAction() +{ + const QString id = params()[KEY_TASK_ID]; + + const auto singleTask = m_torrentCreationManager->getTask(id); + if (!id.isEmpty() && !singleTask) + throw APIError(APIErrorType::NotFound); + + const QList> tasks = id.isEmpty() + ? m_torrentCreationManager->tasks() + : QList> {singleTask}; + QJsonArray statusArray; + for (const auto &task : tasks) + { + QJsonObject taskJson { + {KEY_TASK_ID, task->id()}, + {KEY_SOURCE_PATH, task->params().sourcePath.toString()}, + {KEY_PIECE_SIZE, task->params().pieceSize}, + {KEY_PRIVATE, task->params().isPrivate}, + {KEY_TIME_ADDED, task->timeAdded().toString()}, +#ifdef QBT_USES_LIBTORRENT2 + {KEY_FORMAT, torrentFormatToString(task->params().torrentFormat)}, +#else + {KEY_OPTIMIZE_ALIGNMENT, task->params().isAlignmentOptimized}, + {KEY_PADDED_FILE_SIZE_LIMIT, task->params().paddedFileSizeLimit}, +#endif + {KEY_STATUS, taskStatusString(task)}, + }; + + if (!task->params().comment.isEmpty()) + taskJson[KEY_COMMENT] = task->params().comment; + + if (!task->params().torrentFilePath.isEmpty()) + taskJson[KEY_TORRENT_FILE_PATH] = task->params().torrentFilePath.toString(); + + if (!task->params().source.isEmpty()) + taskJson[KEY_SOURCE] = task->params().source; + + if (!task->params().trackers.isEmpty()) + taskJson[KEY_TRACKERS] = QJsonArray::fromStringList(task->params().trackers); + + if (!task->params().urlSeeds.isEmpty()) + taskJson[KEY_URL_SEEDS] = QJsonArray::fromStringList(task->params().urlSeeds); + + if (const QDateTime timeStarted = task->timeStarted(); !timeStarted.isNull()) + taskJson[KEY_TIME_STARTED] = timeStarted.toString(); + + if (const QDateTime timeFinished = task->timeFinished(); !task->timeFinished().isNull()) + taskJson[KEY_TIME_FINISHED] = timeFinished.toString(); + + if (task->isFinished()) + { + if (task->isFailed()) + { + taskJson[KEY_ERROR_MESSAGE] = task->errorMsg(); + } + else + { + taskJson[KEY_PIECE_SIZE] = task->result().pieceSize; + } + } + else if (task->isRunning()) + { + taskJson[KEY_PROGRESS] = task->progress(); + } + + statusArray.append(taskJson); + } + + setResult(statusArray); +} + +void TorrentCreatorController::torrentFileAction() +{ + requireParams({KEY_TASK_ID}); + const QString id = params()[KEY_TASK_ID]; + + const auto task = m_torrentCreationManager->getTask(id); + if (!task) + throw APIError(APIErrorType::NotFound); + + if (!task->isFinished()) + throw APIError(APIErrorType::Conflict, tr("Torrent creation is still unfinished.")); + + if (task->isFailed()) + throw APIError(APIErrorType::Conflict, tr("Torrent creation failed.")); + + const auto readResult = Utils::IO::readFile(task->result().torrentFilePath, Preferences::instance()->getTorrentFileSizeLimit()); + if (!readResult) + throw APIError(APIErrorType::Conflict, readResult.error().message); + + setResult(readResult.value(), u"application/x-bittorrent"_s, (id + u".torrent")); +} + +void TorrentCreatorController::deleteTaskAction() +{ + requireParams({KEY_TASK_ID}); + const QString id = params()[KEY_TASK_ID]; + + if (!m_torrentCreationManager->deleteTask(id)) + throw APIError(APIErrorType::NotFound); +} diff --git a/src/webui/api/torrentcreatorcontroller.h b/src/webui/api/torrentcreatorcontroller.h new file mode 100644 index 000000000..7d6916e6b --- /dev/null +++ b/src/webui/api/torrentcreatorcontroller.h @@ -0,0 +1,55 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa + * + * 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 "apicontroller.h" + +namespace BitTorrent +{ + class TorrentCreationManager; +} + +class TorrentCreatorController final : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(TorrentCreatorController) + +public: + TorrentCreatorController(BitTorrent::TorrentCreationManager *torrentCreationManager, IApplication *app, QObject *parent = nullptr); + +private slots: + void addTaskAction(); + void statusAction(); + void torrentFileAction(); + void deleteTaskAction(); + +private: + BitTorrent::TorrentCreationManager *m_torrentCreationManager = nullptr; +}; diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index b615728ca..1394e1e46 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -1,6 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014, 2022-2023 Vladimir Golovnev + * Copyright (C) 2014-2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -46,6 +47,7 @@ #include #include "base/algorithm.h" +#include "base/bittorrent/torrentcreationmanager.h" #include "base/http/httperror.h" #include "base/logger.h" #include "base/preferences.h" @@ -62,6 +64,7 @@ #include "api/rsscontroller.h" #include "api/searchcontroller.h" #include "api/synccontroller.h" +#include "api/torrentcreatorcontroller.h" #include "api/torrentscontroller.h" #include "api/transfercontroller.h" #include "freediskspacechecker.h" @@ -112,9 +115,8 @@ namespace if (contentType.startsWith(u"image/")) return u"private, max-age=604800"_s; // 1 week - if ((contentType == Http::CONTENT_TYPE_CSS) - || (contentType == Http::CONTENT_TYPE_JS)) - { + if ((contentType == Http::CONTENT_TYPE_CSS) || (contentType == Http::CONTENT_TYPE_JS)) + { // short interval in case of program update return u"private, max-age=43200"_s; // 12 hrs } @@ -162,6 +164,7 @@ WebApplication::WebApplication(IApplication *app, QObject *parent) , m_workerThread {new QThread} , m_freeDiskSpaceChecker {new FreeDiskSpaceChecker} , m_freeDiskSpaceCheckingTimer {new QTimer(this)} + , m_torrentCreationManager {new BitTorrent::TorrentCreationManager(app, this)} { declarePublicAPI(u"auth/login"_s); @@ -724,16 +727,18 @@ void WebApplication::sessionStart() m_currentSession = new WebSession(generateSid(), app()); m_sessions[m_currentSession->id()] = m_currentSession; - m_currentSession->registerAPIController(u"app"_s); - m_currentSession->registerAPIController(u"log"_s); - m_currentSession->registerAPIController(u"rss"_s); - m_currentSession->registerAPIController(u"search"_s); - m_currentSession->registerAPIController(u"torrents"_s); - m_currentSession->registerAPIController(u"transfer"_s); + m_currentSession->registerAPIController(u"app"_s, new AppController(app(), this)); + m_currentSession->registerAPIController(u"log"_s, new LogController(app(), this)); + m_currentSession->registerAPIController(u"torrentcreator"_s, new TorrentCreatorController(m_torrentCreationManager, app(), this)); + m_currentSession->registerAPIController(u"rss"_s, new RSSController(app(), this)); + m_currentSession->registerAPIController(u"search"_s, new SearchController(app(), this)); + m_currentSession->registerAPIController(u"torrents"_s, new TorrentsController(app(), this)); + m_currentSession->registerAPIController(u"transfer"_s, new TransferController(app(), this)); - auto *syncController = m_currentSession->registerAPIController(u"sync"_s); + auto *syncController = new SyncController(app(), this); syncController->updateFreeDiskSpace(m_freeDiskSpaceChecker->lastResult()); connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace); + m_currentSession->registerAPIController(u"sync"_s, syncController); QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toUtf8()}; cookie.setHttpOnly(true); @@ -908,6 +913,12 @@ void WebSession::updateTimestamp() m_timer.start(); } +void WebSession::registerAPIController(const QString &scope, APIController *controller) +{ + Q_ASSERT(controller); + m_apiControllers[scope] = controller; +} + APIController *WebSession::getAPIController(const QString &scope) const { return m_apiControllers.value(scope); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index aeee95863..a1887e387 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -1,6 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014, 2017, 2022-2023 Vladimir Golovnev + * Copyright (C) 2014-2024 Vladimir Golovnev + * Copyright (C) 2024 Radu Carpa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -62,6 +63,11 @@ class AuthController; class FreeDiskSpaceChecker; class WebApplication; +namespace BitTorrent +{ + class TorrentCreationManager; +} + class WebSession final : public ApplicationComponent, public ISession { public: @@ -72,15 +78,7 @@ public: bool hasExpired(qint64 seconds) const; void updateTimestamp(); - template - T *registerAPIController(const QString &scope) - { - static_assert(std::is_base_of_v, "Class should be derived from APIController."); - auto *controller = new T(app(), this); - m_apiControllers[scope] = controller; - return controller; - } - + void registerAPIController(const QString &scope, APIController *controller); APIController *getAPIController(const QString &scope) const; private: @@ -172,6 +170,8 @@ private: {{u"search"_s, u"stop"_s}, Http::METHOD_POST}, {{u"search"_s, u"uninstallPlugin"_s}, Http::METHOD_POST}, {{u"search"_s, u"updatePlugins"_s}, Http::METHOD_POST}, + {{u"torrentcreator"_s, u"addTask"_s}, Http::METHOD_POST}, + {{u"torrentcreator"_s, u"deleteTask"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"add"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"addPeers"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"addTags"_s}, Http::METHOD_POST}, @@ -254,4 +254,5 @@ private: Utils::Thread::UniquePtr m_workerThread; FreeDiskSpaceChecker *m_freeDiskSpaceChecker = nullptr; QTimer *m_freeDiskSpaceCheckingTimer = nullptr; + BitTorrent::TorrentCreationManager *m_torrentCreationManager = nullptr; };