Skip to content

Commit

Permalink
Revamp torrent file copies management
Browse files Browse the repository at this point in the history
  • Loading branch information
glassez committed Dec 17, 2024
1 parent d96ab6b commit 696854a
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 158 deletions.
5 changes: 5 additions & 0 deletions src/base/bittorrent/bencoderesumedatastorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre

torrentParams.stopCondition = Utils::String::toEnum(
fromLTString(resumeDataRoot.dict_find_string_value("qBt-stopCondition")), Torrent::StopCondition::None);

torrentParams.torrentFileCopyPath = Profile::instance()->fromPortablePath(
Path(fromLTString(resumeDataRoot.dict_find_string_value("qBt-torrentFileCopyPath"))));

torrentParams.sslParameters =
{
.certificate = QSslCertificate(toByteArray(resumeDataRoot.dict_find_string_value(KEY_SSL_CERTIFICATE))),
Expand Down Expand Up @@ -428,6 +432,7 @@ void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, co
data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString();
data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority;
data["qBt-stopCondition"] = Utils::String::fromEnum(resumeData.stopCondition).toStdString();
data["qBt-torrentFileCopyPath"] = Profile::instance()->toPortablePath(resumeData.torrentFileCopyPath).data().toStdString();

if (!resumeData.sslParameters.certificate.isNull())
data[KEY_SSL_CERTIFICATE] = resumeData.sslParameters.certificate.toPem().toStdString();
Expand Down
11 changes: 10 additions & 1 deletion src/base/bittorrent/dbresumedatastorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ namespace
{
const QString DB_CONNECTION_NAME = u"ResumeDataStorage"_s;

const int DB_VERSION = 8;
const int DB_VERSION = 9;

const QString DB_TABLE_META = u"meta"_s;
const QString DB_TABLE_TORRENTS = u"torrents"_s;
Expand Down Expand Up @@ -143,6 +143,7 @@ namespace
const Column DB_COLUMN_OPERATING_MODE = makeColumn(u"operating_mode"_s);
const Column DB_COLUMN_STOPPED = makeColumn(u"stopped"_s);
const Column DB_COLUMN_STOP_CONDITION = makeColumn(u"stop_condition"_s);
const Column DB_COLUMN_TORRENT_FILE_COPY_PATH = makeColumn(u"torrent_file_copy_path"_s);
const Column DB_COLUMN_SSL_CERTIFICATE = makeColumn(u"ssl_certificate"_s);
const Column DB_COLUMN_SSL_PRIVATE_KEY = makeColumn(u"ssl_private_key"_s);
const Column DB_COLUMN_SSL_DH_PARAMS = makeColumn(u"ssl_dh_params"_s);
Expand Down Expand Up @@ -243,6 +244,8 @@ namespace
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
resumeData.stopCondition = Utils::String::toEnum(
query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
resumeData.torrentFileCopyPath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TORRENT_FILE_COPY_PATH.name).toString()));
resumeData.sslParameters =
{
.certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
Expand Down Expand Up @@ -547,6 +550,7 @@ void BitTorrent::DBResumeDataStorage::createDB() const
makeColumnDefinition(DB_COLUMN_OPERATING_MODE, u"TEXT NOT NULL"_s),
makeColumnDefinition(DB_COLUMN_STOPPED, u"INTEGER NOT NULL"_s),
makeColumnDefinition(DB_COLUMN_STOP_CONDITION, u"TEXT NOT NULL DEFAULT `None`"_s),
makeColumnDefinition(DB_COLUMN_TORRENT_FILE_COPY_PATH, u"TEXT"_s),
makeColumnDefinition(DB_COLUMN_SSL_CERTIFICATE, u"TEXT"_s),
makeColumnDefinition(DB_COLUMN_SSL_PRIVATE_KEY, u"TEXT"_s),
makeColumnDefinition(DB_COLUMN_SSL_DH_PARAMS, u"TEXT"_s),
Expand Down Expand Up @@ -652,6 +656,9 @@ void BitTorrent::DBResumeDataStorage::updateDB(const int fromVersion) const
throw RuntimeError(query.lastError().text());
}

if (fromVersion <= 8)
addColumn(DB_TABLE_TORRENTS, DB_COLUMN_TORRENT_FILE_COPY_PATH, "TEXT");

const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
if (!query.prepare(updateMetaVersionQuery))
throw RuntimeError(query.lastError().text());
Expand Down Expand Up @@ -836,6 +843,7 @@ namespace
DB_COLUMN_OPERATING_MODE,
DB_COLUMN_STOPPED,
DB_COLUMN_STOP_CONDITION,
DB_COLUMN_TORRENT_FILE_COPY_PATH,
DB_COLUMN_SSL_CERTIFICATE,
DB_COLUMN_SSL_PRIVATE_KEY,
DB_COLUMN_SSL_DH_PARAMS,
Expand Down Expand Up @@ -899,6 +907,7 @@ namespace
query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(m_resumeData.operatingMode));
query.bindValue(DB_COLUMN_STOPPED.placeholder, m_resumeData.stopped);
query.bindValue(DB_COLUMN_STOP_CONDITION.placeholder, Utils::String::fromEnum(m_resumeData.stopCondition));
query.bindValue(DB_COLUMN_TORRENT_FILE_COPY_PATH.placeholder, Profile::instance()->toPortablePath(m_resumeData.torrentFileCopyPath).data());
query.bindValue(DB_COLUMN_SSL_CERTIFICATE.placeholder, QString::fromLatin1(m_resumeData.sslParameters.certificate.toPem()));
query.bindValue(DB_COLUMN_SSL_PRIVATE_KEY.placeholder, QString::fromLatin1(m_resumeData.sslParameters.privateKey.toPem()));
query.bindValue(DB_COLUMN_SSL_DH_PARAMS.placeholder, QString::fromLatin1(m_resumeData.sslParameters.dhParams));
Expand Down
1 change: 1 addition & 0 deletions src/base/bittorrent/loadtorrentparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ namespace BitTorrent
bool hasFinishedStatus = false;
bool stopped = false;
Torrent::StopCondition stopCondition = Torrent::StopCondition::None;
Path torrentFileCopyPath;

bool addToQueueTop = false; // only for new torrents

Expand Down
13 changes: 9 additions & 4 deletions src/base/bittorrent/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,15 @@ namespace BitTorrent
virtual void setRefreshInterval(int value) = 0;
virtual bool isPreallocationEnabled() const = 0;
virtual void setPreallocationEnabled(bool enabled) = 0;
virtual Path torrentExportDirectory() const = 0;
virtual void setTorrentExportDirectory(const Path &path) = 0;
virtual Path finishedTorrentExportDirectory() const = 0;
virtual void setFinishedTorrentExportDirectory(const Path &path) = 0;

virtual bool isCopyTorrentFileEnabled() const = 0;
virtual void setCopyTorrentFileEnabled(bool enabled) = 0;
virtual bool isCreateTorrentFileForMagnetEnabled() const = 0;
virtual void setCreateTorrentFileForMagnetEnabled(bool enabled) = 0;
virtual bool isDeleteTorrentFileCopyOnRemoveEnabled() const = 0;
virtual void setDeleteTorrentFileCopyOnRemoveEnabled(bool enabled) = 0;
virtual Path torrentFileCopyDirectory() const = 0;
virtual void setTorrentFileCopyDirectory(const Path &path) = 0;

virtual int globalDownloadSpeedLimit() const = 0;
virtual void setGlobalDownloadSpeedLimit(int limit) = 0;
Expand Down
159 changes: 123 additions & 36 deletions src/base/bittorrent/sessionimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,22 @@ namespace
break;
}
}

Path generateTorrentFilePath(const Path &folderPath, const QString &torrentName)
{
const QString validName = Utils::Fs::toValidFileName(torrentName);
QString torrentFileName = u"%1.torrent"_s.arg(validName);
Path torrentFilePath = folderPath / Path(torrentFileName);
int counter = 0;
while (torrentFilePath.exists())
{
// Append number to torrent name to make it unique
torrentFileName = u"%1 %2.torrent"_s.arg(validName).arg(++counter);
torrentFilePath = folderPath / Path(torrentFileName);
}

return torrentFilePath;
}
}

struct BitTorrent::SessionImpl::ResumeSessionContext final : public QObject
Expand Down Expand Up @@ -476,6 +492,10 @@ SessionImpl::SessionImpl(QObject *parent)
, m_isPreallocationEnabled(BITTORRENT_SESSION_KEY(u"Preallocation"_s), false)
, m_torrentExportDirectory(BITTORRENT_SESSION_KEY(u"TorrentExportDirectory"_s))
, m_finishedTorrentExportDirectory(BITTORRENT_SESSION_KEY(u"FinishedTorrentExportDirectory"_s))
, m_isCopyTorrentFileEnabled(BITTORRENT_SESSION_KEY(u"CopyTorrentFile"_s))
, m_isCreateTorrentFileForMagnetEnabled(BITTORRENT_SESSION_KEY(u"CreateTorrentFileForMagnet"_s))
, m_isDeleteTorrentFileCopyOnRemoveEnabled(BITTORRENT_SESSION_KEY(u"DeleteTorrentFileCopyOnRemove"_s))
, m_torrentFileCopyDirectory(BITTORRENT_SESSION_KEY(u"TorrentFileCopyDirectory"_s))
, m_globalDownloadSpeedLimit(BITTORRENT_SESSION_KEY(u"GlobalDLSpeedLimit"_s), 0, lowerLimited(0))
, m_globalUploadSpeedLimit(BITTORRENT_SESSION_KEY(u"GlobalUPSpeedLimit"_s), 0, lowerLimited(0))
, m_altGlobalDownloadSpeedLimit(BITTORRENT_SESSION_KEY(u"AlternativeGlobalDLSpeedLimit"_s), 10, lowerLimited(0))
Expand Down Expand Up @@ -803,26 +823,48 @@ void SessionImpl::setPreallocationEnabled(const bool enabled)
m_isPreallocationEnabled = enabled;
}

Path SessionImpl::torrentExportDirectory() const
bool SessionImpl::isCopyTorrentFileEnabled() const
{
return m_isCopyTorrentFileEnabled;
}

void SessionImpl::setCopyTorrentFileEnabled(const bool enabled)
{
if (enabled != m_isCopyTorrentFileEnabled)
m_isCopyTorrentFileEnabled = enabled;
}

bool SessionImpl::isCreateTorrentFileForMagnetEnabled() const
{
return m_isCreateTorrentFileForMagnetEnabled;
}

void SessionImpl::setCreateTorrentFileForMagnetEnabled(const bool enabled)
{
return m_torrentExportDirectory;
if (enabled != m_isCreateTorrentFileForMagnetEnabled)
m_isCreateTorrentFileForMagnetEnabled = enabled;
}

void SessionImpl::setTorrentExportDirectory(const Path &path)
bool SessionImpl::isDeleteTorrentFileCopyOnRemoveEnabled() const
{
if (path != torrentExportDirectory())
m_torrentExportDirectory = path;
return m_isDeleteTorrentFileCopyOnRemoveEnabled;
}

Path SessionImpl::finishedTorrentExportDirectory() const
void SessionImpl::setDeleteTorrentFileCopyOnRemoveEnabled(const bool enabled)
{
return m_finishedTorrentExportDirectory;
if (enabled != m_isDeleteTorrentFileCopyOnRemoveEnabled)
m_isDeleteTorrentFileCopyOnRemoveEnabled = enabled;
}

void SessionImpl::setFinishedTorrentExportDirectory(const Path &path)
Path SessionImpl::torrentFileCopyDirectory() const
{
if (path != finishedTorrentExportDirectory())
m_finishedTorrentExportDirectory = path;
return m_torrentFileCopyDirectory;
}

void SessionImpl::setTorrentFileCopyDirectory(const Path &path)
{
if (path != torrentFileCopyDirectory())
m_torrentFileCopyDirectory = path;
}

Path SessionImpl::savePath() const
Expand Down Expand Up @@ -2927,6 +2969,7 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
p.storage = customStorageConstructor;
#endif

m_addingTorrents.insert(id, source);
m_loadingTorrents.insert(id, loadTorrentParams);
if (infoHash.isHybrid())
m_hybridTorrentsByAltID.insert(altID, nullptr);
Expand Down Expand Up @@ -3088,28 +3131,30 @@ bool SessionImpl::downloadMetadata(const TorrentDescriptor &torrentDescr)
return true;
}

void SessionImpl::exportTorrentFile(const Torrent *torrent, const Path &folderPath)
nonstd::expected<Path, QString> SessionImpl::createTorrentFile(const Torrent *torrent, const Path &folderPath)
{
if (!folderPath.exists() && !Utils::Fs::mkpath(folderPath))
return;
return nonstd::make_unexpected(tr("Unable to create folder. Path: \"%1\".").arg(folderPath.toString()));

const QString validName = Utils::Fs::toValidFileName(torrent->name());
QString torrentExportFilename = u"%1.torrent"_s.arg(validName);
Path newTorrentPath = folderPath / Path(torrentExportFilename);
int counter = 0;
while (newTorrentPath.exists())
{
// Append number to torrent name to make it unique
torrentExportFilename = u"%1 %2.torrent"_s.arg(validName).arg(++counter);
newTorrentPath = folderPath / Path(torrentExportFilename);
}
const Path torrentFilePath = generateTorrentFilePath(folderPath, torrent->name());
const auto result = torrent->exportToFile(torrentFilePath);
if (result)
return torrentFilePath;

const nonstd::expected<void, QString> result = torrent->exportToFile(newTorrentPath);
if (!result)
{
LogMsg(tr("Failed to export torrent. Torrent: \"%1\". Destination: \"%2\". Reason: \"%3\"")
.arg(torrent->name(), newTorrentPath.toString(), result.error()), Log::WARNING);
}
return nonstd::make_unexpected(result.error());
}

nonstd::expected<Path, QString> SessionImpl::createTorrentFile(const TorrentDescriptor &torrentDescr, const Path &folderPath)
{
if (!folderPath.exists() && !Utils::Fs::mkpath(folderPath))
return nonstd::make_unexpected(tr("Unable to create folder. Path: \"%1\".").arg(folderPath.toString()));

const Path torrentFilePath = generateTorrentFilePath(folderPath, torrentDescr.name());
const auto saveResult = torrentDescr.saveToFile(torrentFilePath);
if (saveResult)
return torrentFilePath;

return nonstd::make_unexpected(saveResult.error());
}

void SessionImpl::generateResumeData()
Expand Down Expand Up @@ -5098,8 +5143,22 @@ void SessionImpl::handleTorrentUrlSeedsRemoved(TorrentImpl *const torrent, const

void SessionImpl::handleTorrentMetadataReceived(TorrentImpl *const torrent)
{
if (!torrentExportDirectory().isEmpty())
exportTorrentFile(torrent, torrentExportDirectory());
if (const Path copyDir = torrentFileCopyDirectory();
isCopyTorrentFileEnabled() && isCreateTorrentFileForMagnetEnabled() && !copyDir.isEmpty())
{
if (const auto result = createTorrentFile(torrent, copyDir))
{
const Path torrentFilePath = result.value();
torrent->setTorrentFileCopyPath(torrentFilePath);
LogMsg(tr("Created .torrent file. Torrent: \"%1\". Destination: \"%2\".")
.arg(torrent->name(), torrentFilePath.toString()));
}
else
{
LogMsg(tr("Failed to create .torrent file. Torrent: \"%1\". Destination: \"%2\". Reason: \"%3\"")
.arg(torrent->name(), result.value().toString(), result.error()), Log::WARNING);
}
}

emit torrentMetadataReceived(torrent);
}
Expand Down Expand Up @@ -5271,13 +5330,28 @@ void SessionImpl::processPendingFinishedTorrents()
if (m_pendingFinishedTorrents.isEmpty())
return;

const Path copyDir = torrentFileCopyDirectory();
const bool needCreateTorrentFileForMagnet = isCopyTorrentFileEnabled() && isCreateTorrentFileForMagnetEnabled() && !copyDir.isEmpty();
for (TorrentImpl *torrent : asConst(m_pendingFinishedTorrents))
{
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
emit torrentFinished(torrent);

if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
exportTorrentFile(torrent, exportPath);
if (needCreateTorrentFileForMagnet)
{
if (const auto result = createTorrentFile(torrent, copyDir))
{
const Path torrentFilePath = result.value();
torrent->setTorrentFileCopyPath(torrentFilePath);
LogMsg(tr("Created .torrent file. Torrent: \"%1\". Destination: \"%2\".")
.arg(torrent->name(), torrentFilePath.toString()));
}
else
{
LogMsg(tr("Failed to create .torrent file. Torrent: \"%1\". Destination: \"%2\". Reason: \"%3\"")
.arg(torrent->name(), result.value().toString(), result.error()), Log::WARNING);
}
}

processTorrentShareLimits(torrent);
}
Expand Down Expand Up @@ -5776,11 +5850,24 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle,

torrent->requestResumeData(lt::torrent_handle::save_info_dict);

// The following is useless for newly added magnet
if (torrent->hasMetadata())
const TorrentDescriptor torrentSource = m_addingTorrents.take(torrent->id());
const Path copyDir = torrentFileCopyDirectory();

// The following is useless for torrents added without metadata
if (torrentSource.info().has_value() && isCopyTorrentFileEnabled() && !copyDir.isEmpty())
{
if (!torrentExportDirectory().isEmpty())
exportTorrentFile(torrent, torrentExportDirectory());
if (const auto result = createTorrentFile(torrentSource, copyDir))
{
const Path torrentFilePath = result.value();
torrent->setTorrentFileCopyPath(torrentFilePath);
LogMsg(tr("Created copy of .torrent file. Torrent: \"%1\". Destination: \"%2\".")
.arg(torrent->name(), torrentFilePath.toString()));
}
else
{
LogMsg(tr("Failed to copy .torrent file. Torrent: \"%1\". Destination: \"%2\". Reason: \"%3\"")
.arg(torrent->name(), result.value().toString(), result.error()), Log::WARNING);
}
}
}

Expand Down
22 changes: 17 additions & 5 deletions src/base/bittorrent/sessionimpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,15 @@ namespace BitTorrent
void setRefreshInterval(int value) override;
bool isPreallocationEnabled() const override;
void setPreallocationEnabled(bool enabled) override;
Path torrentExportDirectory() const override;
void setTorrentExportDirectory(const Path &path) override;
Path finishedTorrentExportDirectory() const override;
void setFinishedTorrentExportDirectory(const Path &path) override;

bool isCopyTorrentFileEnabled() const override;
void setCopyTorrentFileEnabled(bool enabled) override;
bool isCreateTorrentFileForMagnetEnabled() const override;
void setCreateTorrentFileForMagnetEnabled(bool enabled) override;
bool isDeleteTorrentFileCopyOnRemoveEnabled() const override;
void setDeleteTorrentFileCopyOnRemoveEnabled(bool enabled) override;
Path torrentFileCopyDirectory() const override;
void setTorrentFileCopyDirectory(const Path &path) override;

int globalDownloadSpeedLimit() const override;
void setGlobalDownloadSpeedLimit(int limit) override;
Expand Down Expand Up @@ -558,7 +563,9 @@ namespace BitTorrent
bool addTorrent_impl(const TorrentDescriptor &source, const AddTorrentParams &addTorrentParams);

void updateSeedingLimitTimer();
void exportTorrentFile(const Torrent *torrent, const Path &folderPath);

nonstd::expected<Path, QString> createTorrentFile(const Torrent *torrent, const Path &folderPath);
nonstd::expected<Path, QString> createTorrentFile(const TorrentDescriptor &torrentDescr, const Path &folderPath);

void handleAlert(const lt::alert *alert);
void dispatchTorrentAlert(const lt::torrent_alert *alert);
Expand Down Expand Up @@ -689,6 +696,10 @@ namespace BitTorrent
CachedSettingValue<bool> m_isPreallocationEnabled;
CachedSettingValue<Path> m_torrentExportDirectory;
CachedSettingValue<Path> m_finishedTorrentExportDirectory;
CachedSettingValue<bool> m_isCopyTorrentFileEnabled;
CachedSettingValue<bool> m_isCreateTorrentFileForMagnetEnabled;
CachedSettingValue<bool> m_isDeleteTorrentFileCopyOnRemoveEnabled;
CachedSettingValue<Path> m_torrentFileCopyDirectory;
CachedSettingValue<int> m_globalDownloadSpeedLimit;
CachedSettingValue<int> m_globalUploadSpeedLimit;
CachedSettingValue<int> m_altGlobalDownloadSpeedLimit;
Expand Down Expand Up @@ -788,6 +799,7 @@ namespace BitTorrent

QHash<TorrentID, TorrentImpl *> m_torrents;
QHash<TorrentID, TorrentImpl *> m_hybridTorrentsByAltID;
QHash<TorrentID, TorrentDescriptor> m_addingTorrents;
QHash<TorrentID, LoadTorrentParams> m_loadingTorrents;
QHash<TorrentID, RemovingTorrentData> m_removingTorrents;
QHash<TorrentID, TorrentID> m_changedTorrentIDs;
Expand Down
Loading

0 comments on commit 696854a

Please sign in to comment.