Files
qBittorrent/src/gui/torrentcreatordialog.cpp
rcarpa bbac94cc95 Use QThreadPool for torrent creation
The change is in preparation for adding the possibility to create
torrent files via the API.

Rework TorrentCreatorThread to be a more lightweight QRunnable class.
The parameters are now defined on construction time and are fixed
throughout the lifecycle of the TorrentCreator. The lifecycle of the
object is not bound to the one of QDialog anymore; it is now handled
by the QThreadPool. This will enable easier queueing of multiple torrent
creation jobs without risk of spawning many threads.

PR #19500.
2023-08-27 00:27:11 +08:00

372 lines
14 KiB
C++

/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Mike Tzou (Chocobo1)
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
*
* 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 "torrentcreatordialog.h"
#include <QCloseEvent>
#include <QFileDialog>
#include <QMessageBox>
#include <QMimeData>
#include <QUrl>
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrentdescriptor.h"
#include "base/global.h"
#include "base/utils/fs.h"
#include "ui_torrentcreatordialog.h"
#include "utils.h"
#define SETTINGS_KEY(name) u"TorrentCreator/" name
namespace
{
// When the root file/directory of the created torrent is a symlink, we want to keep the symlink name in the torrent.
// On Windows, however, QFileDialog::DontResolveSymlinks disables shortcuts (.lnk files) expansion, making it impossible to pick a file if its path contains a shortcut.
// As of NTFS symlinks, they don't seem to be resolved anyways.
#ifndef Q_OS_WIN
const QFileDialog::Options FILE_DIALOG_OPTIONS {QFileDialog::DontResolveSymlinks};
#else
const QFileDialog::Options FILE_DIALOG_OPTIONS {};
#endif
}
TorrentCreatorDialog::TorrentCreatorDialog(QWidget *parent, const Path &defaultPath)
: QDialog(parent)
, m_ui(new Ui::TorrentCreatorDialog)
, m_threadPool(this)
, m_storeDialogSize(SETTINGS_KEY(u"Size"_s))
, m_storePieceSize(SETTINGS_KEY(u"PieceSize"_s))
, m_storePrivateTorrent(SETTINGS_KEY(u"PrivateTorrent"_s))
, m_storeStartSeeding(SETTINGS_KEY(u"StartSeeding"_s))
, m_storeIgnoreRatio(SETTINGS_KEY(u"IgnoreRatio"_s))
#ifdef QBT_USES_LIBTORRENT2
, m_storeTorrentFormat(SETTINGS_KEY(u"TorrentFormat"_s))
#else
, m_storeOptimizeAlignment(SETTINGS_KEY(u"OptimizeAlignment"_s))
, m_paddedFileSizeLimit(SETTINGS_KEY(u"PaddedFileSizeLimit"_s))
#endif
, m_storeLastAddPath(SETTINGS_KEY(u"LastAddPath"_s))
, m_storeTrackerList(SETTINGS_KEY(u"TrackerList"_s))
, m_storeWebSeedList(SETTINGS_KEY(u"WebSeedList"_s))
, m_storeComments(SETTINGS_KEY(u"Comments"_s))
, m_storeLastSavePath(SETTINGS_KEY(u"LastSavePath"_s))
, m_storeSource(SETTINGS_KEY(u"Source"_s))
{
m_ui->setupUi(this);
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create Torrent"));
m_ui->textInputPath->setMode(FileSystemPathEdit::Mode::ReadOnly);
connect(m_ui->addFileButton, &QPushButton::clicked, this, &TorrentCreatorDialog::onAddFileButtonClicked);
connect(m_ui->addFolderButton, &QPushButton::clicked, this, &TorrentCreatorDialog::onAddFolderButtonClicked);
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &TorrentCreatorDialog::onCreateButtonClicked);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_ui->buttonCalcTotalPieces, &QPushButton::clicked, this, &TorrentCreatorDialog::updatePiecesCount);
connect(m_ui->checkStartSeeding, &QCheckBox::clicked, m_ui->checkIgnoreShareLimits, &QWidget::setEnabled);
loadSettings();
updateInputPath(defaultPath);
m_threadPool.setMaxThreadCount(1);
#ifdef QBT_USES_LIBTORRENT2
m_ui->checkOptimizeAlignment->hide();
#else
m_ui->widgetTorrentFormat->hide();
#endif
}
TorrentCreatorDialog::~TorrentCreatorDialog()
{
saveSettings();
delete m_ui;
}
void TorrentCreatorDialog::updateInputPath(const Path &path)
{
if (path.isEmpty()) return;
m_ui->textInputPath->setSelectedPath(path);
updateProgressBar(0);
}
void TorrentCreatorDialog::onAddFolderButtonClicked()
{
const QString oldPath = m_ui->textInputPath->selectedPath().data();
const Path path {QFileDialog::getExistingDirectory(this, tr("Select folder")
, oldPath, (QFileDialog::ShowDirsOnly | FILE_DIALOG_OPTIONS))};
updateInputPath(path);
}
void TorrentCreatorDialog::onAddFileButtonClicked()
{
const QString oldPath = m_ui->textInputPath->selectedPath().data();
const Path path {QFileDialog::getOpenFileName(this, tr("Select file"), oldPath, QString(), nullptr, FILE_DIALOG_OPTIONS)};
updateInputPath(path);
}
int TorrentCreatorDialog::getPieceSize() const
{
const int pieceSizes[] = {0, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768}; // base unit in KiB
return pieceSizes[m_ui->comboPieceSize->currentIndex()] * 1024;
}
#ifdef QBT_USES_LIBTORRENT2
BitTorrent::TorrentFormat TorrentCreatorDialog::getTorrentFormat() const
{
switch (m_ui->comboTorrentFormat->currentIndex())
{
case 0:
return BitTorrent::TorrentFormat::V2;
case 1:
return BitTorrent::TorrentFormat::Hybrid;
case 2:
return BitTorrent::TorrentFormat::V1;
}
return BitTorrent::TorrentFormat::Hybrid;
}
#else
int TorrentCreatorDialog::getPaddedFileSizeLimit() const
{
const int value = m_ui->spinPaddedFileSizeLimit->value();
return ((value >= 0) ? (value * 1024) : -1);
}
#endif
void TorrentCreatorDialog::dropEvent(QDropEvent *event)
{
event->acceptProposedAction();
if (event->mimeData()->hasUrls())
{
// only take the first one
const QUrl firstItem = event->mimeData()->urls().first();
const Path path {
(firstItem.scheme().compare(u"file", Qt::CaseInsensitive) == 0)
? firstItem.toLocalFile() : firstItem.toString()
};
updateInputPath(path);
}
}
void TorrentCreatorDialog::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat(u"text/plain"_s) || event->mimeData()->hasFormat(u"text/uri-list"_s))
event->acceptProposedAction();
}
// Main function that create a .torrent file
void TorrentCreatorDialog::onCreateButtonClicked()
{
#ifdef Q_OS_WIN
// Resolve the path in case it contains a shortcut (otherwise, the following usages will consider it invalid)
const Path inputPath = Utils::Fs::toCanonicalPath(m_ui->textInputPath->selectedPath());
#else
const Path inputPath = m_ui->textInputPath->selectedPath();
#endif
// test if readable
if (!Utils::Fs::isReadable(inputPath))
{
QMessageBox::critical(this, tr("Torrent creation failed"), tr("Reason: Path to file/folder is not readable."));
return;
}
// get save path
const Path lastSavePath = (m_storeLastSavePath.get(Utils::Fs::homePath()) / Path(inputPath.filename() + u".torrent"));
Path destPath {QFileDialog::getSaveFileName(this, tr("Select where to save the new torrent"), lastSavePath.data(), tr("Torrent Files (*.torrent)"))};
if (destPath.isEmpty())
return;
if (!destPath.hasExtension(TORRENT_FILE_EXTENSION))
destPath += TORRENT_FILE_EXTENSION;
m_storeLastSavePath = destPath.parentPath();
// Disable dialog & set busy cursor
setInteractionEnabled(false);
setCursor(QCursor(Qt::WaitCursor));
const QStringList trackers = m_ui->trackersList->toPlainText().trimmed()
.replace(QRegularExpression(u"\n\n[\n]+"_s), u"\n\n"_s).split(u'\n');
const BitTorrent::TorrentCreatorParams params
{
m_ui->checkPrivate->isChecked()
#ifdef QBT_USES_LIBTORRENT2
, getTorrentFormat()
#else
, m_ui->checkOptimizeAlignment->isChecked()
, getPaddedFileSizeLimit()
#endif
, getPieceSize()
, inputPath
, destPath
, m_ui->txtComment->toPlainText()
, m_ui->lineEditSource->text()
, trackers
, m_ui->URLSeedsList->toPlainText().split(u'\n', Qt::SkipEmptyParts)
};
auto *torrentCreator = new BitTorrent::TorrentCreator(params);
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);
// run the torrentCreator in a thread
m_threadPool.start(torrentCreator);
}
void TorrentCreatorDialog::handleCreationFailure(const QString &msg)
{
// Remove busy cursor
setCursor(QCursor(Qt::ArrowCursor));
QMessageBox::information(this, tr("Torrent creation failed"), tr("Reason: %1").arg(msg));
setInteractionEnabled(true);
}
void TorrentCreatorDialog::handleCreationSuccess(const Path &path, const Path &branchPath)
{
// Remove busy cursor
setCursor(QCursor(Qt::ArrowCursor));
if (m_ui->checkStartSeeding->isChecked())
{
// Create save path temp data
const auto loadResult = BitTorrent::TorrentDescriptor::loadFromFile(path);
if (!loadResult)
{
QMessageBox::critical(this, tr("Torrent creation failed"), tr("Reason: Created torrent is invalid. It won't be added to download list."));
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);
}
QMessageBox::information(this, tr("Torrent creator")
, u"%1\n%2"_s.arg(tr("Torrent created:"), path.toString()));
setInteractionEnabled(true);
}
void TorrentCreatorDialog::updateProgressBar(int progress)
{
m_ui->progressBar->setValue(progress);
}
void TorrentCreatorDialog::updatePiecesCount()
{
const Path path = m_ui->textInputPath->selectedPath();
#ifdef QBT_USES_LIBTORRENT2
const int count = BitTorrent::TorrentCreator::calculateTotalPieces(
path, getPieceSize(), getTorrentFormat());
#else
const bool isAlignmentOptimized = m_ui->checkOptimizeAlignment->isChecked();
const int count = BitTorrent::TorrentCreator::calculateTotalPieces(path
, getPieceSize(), isAlignmentOptimized, getPaddedFileSizeLimit());
#endif
m_ui->labelTotalPieces->setText(QString::number(count));
}
void TorrentCreatorDialog::setInteractionEnabled(const bool enabled) const
{
m_ui->textInputPath->setEnabled(enabled);
m_ui->addFileButton->setEnabled(enabled);
m_ui->addFolderButton->setEnabled(enabled);
m_ui->trackersList->setEnabled(enabled);
m_ui->URLSeedsList->setEnabled(enabled);
m_ui->txtComment->setEnabled(enabled);
m_ui->comboPieceSize->setEnabled(enabled);
m_ui->buttonCalcTotalPieces->setEnabled(enabled);
m_ui->checkPrivate->setEnabled(enabled);
m_ui->checkStartSeeding->setEnabled(enabled);
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enabled);
m_ui->checkIgnoreShareLimits->setEnabled(enabled && m_ui->checkStartSeeding->isChecked());
#ifdef QBT_USES_LIBTORRENT2
m_ui->widgetTorrentFormat->setEnabled(enabled);
#else
m_ui->checkOptimizeAlignment->setEnabled(enabled);
m_ui->spinPaddedFileSizeLimit->setEnabled(enabled);
#endif
}
void TorrentCreatorDialog::saveSettings()
{
m_storeLastAddPath = m_ui->textInputPath->selectedPath();
m_storePieceSize = m_ui->comboPieceSize->currentIndex();
m_storePrivateTorrent = m_ui->checkPrivate->isChecked();
m_storeStartSeeding = m_ui->checkStartSeeding->isChecked();
m_storeIgnoreRatio = m_ui->checkIgnoreShareLimits->isChecked();
#ifdef QBT_USES_LIBTORRENT2
m_storeTorrentFormat = m_ui->comboTorrentFormat->currentIndex();
#else
m_storeOptimizeAlignment = m_ui->checkOptimizeAlignment->isChecked();
m_paddedFileSizeLimit = m_ui->spinPaddedFileSizeLimit->value();
#endif
m_storeTrackerList = m_ui->trackersList->toPlainText();
m_storeWebSeedList = m_ui->URLSeedsList->toPlainText();
m_storeComments = m_ui->txtComment->toPlainText();
m_storeSource = m_ui->lineEditSource->text();
m_storeDialogSize = size();
}
void TorrentCreatorDialog::loadSettings()
{
m_ui->textInputPath->setSelectedPath(m_storeLastAddPath.get(Utils::Fs::homePath()));
m_ui->comboPieceSize->setCurrentIndex(m_storePieceSize);
m_ui->checkPrivate->setChecked(m_storePrivateTorrent);
m_ui->checkStartSeeding->setChecked(m_storeStartSeeding);
m_ui->checkIgnoreShareLimits->setChecked(m_storeIgnoreRatio);
m_ui->checkIgnoreShareLimits->setEnabled(m_ui->checkStartSeeding->isChecked());
#ifdef QBT_USES_LIBTORRENT2
m_ui->comboTorrentFormat->setCurrentIndex(m_storeTorrentFormat.get(1));
#else
m_ui->checkOptimizeAlignment->setChecked(m_storeOptimizeAlignment.get(true));
m_ui->spinPaddedFileSizeLimit->setValue(m_paddedFileSizeLimit.get(-1));
#endif
m_ui->trackersList->setPlainText(m_storeTrackerList);
m_ui->URLSeedsList->setPlainText(m_storeWebSeedList);
m_ui->txtComment->setPlainText(m_storeComments);
m_ui->lineEditSource->setText(m_storeSource);
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize);
}